在云端CAD中实现AutoCAD的多段线功能和接口调用
前言
多段线既能承载直线段与圆弧段,又自带宽度与几何属性。它是真实的工程图纸中使用率最高的图元,如建筑墙体、市政管线、道路边线、地块轮廓。本文作为基础绘图的进阶篇,将带大家深入多段线,并在 Web 端复刻 AutoCAD PLINE 命令交互。

一、多段线概念
多段线(Polyline)是由多个顶点依次连接而成的单一实体,相邻顶点之间既可以是直线段,也可以是圆弧段。它与零散直线段最本质的区别在于:无论包含多少段,它在图纸数据库中始终是"一个对象"。
-
选择与编辑以整体为单位: 一条包含 200 个顶点的管线,点选即整体,移动、删除都是一次操作;
-
自带几何属性:可直接查询总长度与包围面积,是工程量统计的基础;
-
支持整体变换: 偏移(画双线墙体)、打碎(还原为基本图元)等操作天然以它为对象。
在 mxcad 中,多段线实体由 McDbPolyline 类表示。绘制一条多段线的最简形式,是依次添加顶点后提交:
import { McDbPolyline, MxCpp } from "mxcad";
const pl = new McDbPolyline();
pl.addVertexAt(pt1); // 依次添加顶点
pl.addVertexAt(pt2);
pl.addVertexAt(pt3);
pl.addVertexAt(pt4);
pl.isClosed = true; // 首尾闭合
MxCpp.getCurrentMxCAD().drawEntity(pl); // 提交绘制
addVertexAt 方法
mxcad 提供的 addVertexAt 方法除了写入顶点坐标,还能为每个顶点设置凸度与宽度两类属性:
-
顶点:多段线的基本组成单元,决定路径走向;
-
凸度:控制相邻两顶点之间是直线还是圆弧;
-
宽度:控制线段的粗细,支持渐变。
/** * 在指定位置添加顶点 * @param pt 顶点坐标 * @param bulge 凸度(默认 0,即直线段) * @param startWidth 起始宽度(默认 0) * @param endWidth 结束宽度(默认 0) * @param index 插入位置(默认追加到末尾) */ addVertexAt(pt, bulge?, startWidth?, endWidth?, index?): boolean
二、多段线的三大核心要素
1. 顶点
顶点按添加顺序连接成路径,是多段线最基本的操作单元。围绕顶点的常用 API 如下:
const num = pl.numVerts(); // 顶点总数 const pt = pl.getPointAt(2).val; // 读取第 2 号顶点坐标 pl.setPointAt(2, newPt); // 移动第 2 号顶点 pl.removeVertexAt(2); // 删除第 2 号顶点 pl.isClosed = true; // 闭合:末顶点自动连回首顶点
需要特别注意的是,顶点是有序的,顶点的顺序决定了多段线的方向。比如,多段线中圆弧的切向计算就是依赖顶点的先后顺序。
2. 凸度
与对象捕捉使用数值编码捕捉模式类似,多段线使用一个数值(凸度)完整描述一段圆弧。
凸度的定义: 凸度等于圆弧包含角(圆心角)四分之一的正切值,即 bulge = tan(θ/4)。
其中 θ 为该弧段所对的圆心角(弧度制)。围绕定义有三条规则:
-
符号表方向:
bulge > 0为逆时针弧,bulge < 0为顺时针弧; -
特征值:
bulge = 0是直线段;|bulge| = 1恰好是半圆(tan45° = 1); -
存放位置: 凸度挂在弧段的起始顶点上,用于描述当前顶点到下一顶点中间的这一段。
日常开发记住这张速查表即可:
| 圆弧包含角 θ | bulge = tan(θ/4) | 说明 |
|---|---|---|
| 0° | 0 | 直线段 |
| 90° | √2 − 1 ≈ 0.4142 | 四分之一圆,倒圆角常用 |
| 180° | 1 | 半圆 |
| 270° | √2 + 1 ≈ 2.4142 | 优弧 |

由于手动换算凸度既繁琐又易错,因此 mxcad 提供了现成的工具函数 MxCADUtility.calcBulge(),只需要给出圆弧的起点、弧上任意一点、端点,即可直接得到目标圆弧的凸度值:
import { MxCADUtility } from "mxcad";
const ret = MxCADUtility.calcBulge(startPt, arcMidPt, endPt);
if (ret.ret) {
console.log(ret.val); // 凸度值,可直接作为 addVertexAt 的第 2 参数
}
对已经绘制在图纸上的多段线,还可以按索引读写凸度:
const bulge = pl.getBulgeAt(2); // 读取第 2 号顶点凸度 pl.setBulgeAt(2, 0); // 置 0:该段变为直线
3. 宽度
多段线的宽度属性常被忽视,但它是示意图级绘图的关键。宽度分两个层级:
-
全局宽度(constantWidth): 整条多段线统一粗细,只需设置一个属性:
pl.constantWidth = 10; // 整条多段线宽 10
-
逐顶点宽度(addVertexAt):
addVertexAt的第 3、4 个参数分别是该顶点的起始宽度与结束宽度,线段宽度从起始值线性渐变到结束值。利用这一点,几行代码就能画出带箭头的标注线:const pl = new McDbPolyline(); pl.addVertexAt(pt1, 0, 4, 4); // 等宽段:箭杆 pl.addVertexAt(pt2, 0, 12, 0); // 渐变段:宽度从 12 收拢到 0,形成箭头 pl.addVertexAt(pt3);
对已经绘制在图纸上的多段线,用 setWidthsAt / getWidthsAt 按索引读写宽度。
// 设置第 0 号顶点段的宽度:起始宽度 10、结束宽度 10(等宽段) pl.setWidthsAt(0, 10, 10); // 读取第 0 号顶点段的宽度信息 const width = pl.getWidthsAt(0); if (width.ret) { // val1 为起始宽度,val2 为结束宽度 console.log(width.val1, width.val2); }
三、构建完整的多段线绘制命令
命令的实现分为五步:规划交互选项、构建数据收集与实体生成的基本结构、分别实现直线模式与圆弧模式、完成收尾处理,最后将所有部分整合为一个完整的命令并注册。

1. 交互设计
编写代码前,先规划命令的交互形态。MxCAD 对齐 AutoCAD 的操作习惯,因此直接采用 PLINE 的选项体系。其中,PLINED的实现主要分为以下的两种模式:
-
直线模式:围绕顶点 + 宽度;
-
圆弧模式:围绕凸度。
根据 AutoCAD 中绘制多段线的操作,可以将命令的实现步骤分为:先收集顶点、构建实体,再处理宽度,最后处理凸度。

2. 命令的基本结构
2.1 顶点数据的收集
命令需要用数组记录用户确认的每一个顶点。回看 addVertexAt 的参数——坐标、凸度、起始宽度、结束宽度,数组中每条记录包含这四个字段。这样设计的好处是:生成最终实体时,逐条调用 addVertexAt 即可:
// 顶点数据:坐标 + 凸度 + 起止宽度 interface VertexData { pt: McGePoint3d; dBulge: number; dStartWidth: number; dEndWidth: number; } const vecData: VertexData[] = []; // 添加起点:直线段凸度为 0,宽度默认 0 vecData.push({ pt: firstPoint, dBulge: 0, dStartWidth: 0, dEndWidth: 0 });
2.2 临时实体:实时显示与回退
交互过程中,每确认一段需要立刻显示图形,回退时需要能够撤销。因此引入临时实体:每确认一段,用最近两个顶点绘制一条两点多段线临时显示,其 ID 记入 vecTmpObjectId;回退时删除对应的临时实体;命令结束时删除全部临时实体,用完整的顶点数据一次性生成最终多段线:
// 提交一段:用最近两个顶点画临时多段线 function commitSegment() { const len = vecData.length; if (len < 2) return; const seg = new McDbPolyline(); seg.addVertexAt(vecData[len - 2].pt, vecData[len - 2].dBulge, vecData[len - 2].dStartWidth, vecData[len - 2].dEndWidth); seg.addVertexAt(vecData[len - 1].pt); vecTmpObjectId.push(mxcad.drawEntity(seg)); } // 回退(U):弹出顶点数据 + 删除最近临时实体 function undoSegment() { if (vecData.length <= 1) return; vecData.pop(); vecTmpObjectId[vecTmpObjectId.length - 1].erase(); vecTmpObjectId.pop(); }
数据收集与实体生成相互分离:顶点数组保存数据,临时实体用于实时显示,最终实体在命令结束时生成。
2.3 主循环与模式切换
命令的主体是一个循环。用布尔变量 isDrawLine 标记当前处于直线模式还是圆弧模式,循环据此切换两套交互分支,关键字 A(圆弧)与 L(直线)用于切换模式:
while (true) { if (isDrawLine) { // ---- 直线模式(见第 3 小节)---- } else { // ---- 圆弧模式(见第 4 小节)---- } }
基本结构确定后,下面分别实现两个分支的逻辑。
3. 直线模式:顶点收集与宽度设置
直线模式的主要流程是:取点、保存顶点数据、提交临时段;关键字分支处理宽度、长度、回退、闭合:
const getNextPoint = new MxCADUiPrPoint(); getNextPoint.setMessage("\n指定下一个点:"); // 关键字随顶点数动态变化:≥3 个顶点才允许闭合(与 AutoCAD 一致) let sKeyWord = "[圆弧(A)/宽度(W)/长度(L)/半宽(H)]"; if (vecData.length >= 3) sKeyWord = "[回退(U)/圆弧(A)/宽度(W)/长度(L)/半宽(H)/闭合(C)]"; else if (vecData.length >= 2) sKeyWord = "[回退(U)/圆弧(A)/宽度(W)/长度(L)/半宽(H)]"; getNextPoint.setKeyWords(sKeyWord); // 动态预览:光标移动时实时画出"上一顶点 → 光标"的橡皮筋段 const drawData = vecData[vecData.length - 1]; getNextPoint.setUserDraw((currentPoint, pWorldDraw) => { const pl = new McDbPolyline(); pl.addVertexAt(drawData.pt, drawData.dBulge, drawData.dStartWidth, drawData.dEndWidth); pl.addVertexAt(currentPoint); pWorldDraw.drawMcDbEntity(pl, true); // 第二参数 true:仅预览,不入数据库 }); const ptNext = await getNextPoint.go(); if (ptNext !== null) { // 取点成功:保存新顶点数据(凸度 0),提交临时段 vecData.push({ pt: ptNext, dBulge: 0, dStartWidth, dEndWidth }); commitSegment(); } else if (getNextPoint.getStatus() === MrxDbgUiPrBaseReturn.kKeyWord) { if (getNextPoint.isKeyWordPicked("A")) { isDrawLine = false; // 切换圆弧模式 } else if (getNextPoint.isKeyWordPicked("W")) { // 宽度:起点宽、端点宽两步输入 const getWidth = new MxCADUiPrDist(); getWidth.setMessage("指定起点宽度"); if (await getWidth.go() === null) break; dStartWidth = getWidth.value(); getWidth.setMessage("指定端点宽度"); if (await getWidth.go() === null) break; dEndWidth = getWidth.value(); // 宽度写回当前顶点,作用于下一段;端点宽延续为后续默认值 vecData[vecData.length - 1].dStartWidth = dStartWidth; vecData[vecData.length - 1].dEndWidth = dEndWidth; dStartWidth = dEndWidth; } else if (getNextPoint.isKeyWordPicked("C")) { isClose = true; break; // 闭合 } else if (getNextPoint.isKeyWordPicked("U")) { undoSegment(); // 回退 } // H 半宽(输入值 ×2)、L 长度(沿上一段方向推算顶点)逻辑类似,此处省略 } else { break; // 右键/回车结束 }
这里有两个关键细节:
-
动态预览通过
setUserDraw回调实现,第二参数传true表示仅预览、不写入数据库,因此预览图形不会写入图纸; -
宽度的生效规则:宽度写回的是当前顶点(作用于即将绘制的下一段),且端点宽度会延续为后续段的默认起始宽度,与 AutoCAD PLINE 宽度持续生效的规则一致。
4. 圆弧模式:切向继承与凸度计算
4.1 切向继承:保证圆弧平滑衔接
切换到圆弧模式后,首先需要计算当前弧段的起点切向。AutoCAD 规定,圆弧段默认与上一段相切衔接——上一段是直线,切向就是直线方向;上一段是圆弧,切向就是该弧终点的切线方向。据此实现:
let vecArcTangent = new McGeVector3d(); if (vecData.length < 2) { // 第一段就是圆弧:无参照,切向取水平方向 vecArcTangent.copy(McGeVector3d.kXAxis); } else { const size = vecData.length; const pt1 = vecData[size - 2].pt; // 上一段起点 const dBulge = vecData[size - 2].dBulge; // 上一段凸度 const pt2 = vecData[size - 1].pt; // 上一段终点(当前弧起点) if (dBulge === 0) { // 上一段是直线:切向 = 直线方向 vecArcTangent = pt2.sub(pt1); } else { // 上一段是圆弧:构造临时多段线,求终点一阶导数(切向量) const tmpPl = new McDbPolyline(); tmpPl.addVertexAt(pt1, dBulge); tmpPl.addVertexAt(pt2); const tmpVec = tmpPl.getFirstDeriv(new McGePoint3d(pt2.x, pt2.y, 0)); if (tmpVec.ret) vecArcTangent = tmpVec.val; else vecArcTangent.copy(McGeVector3d.kXAxis); } }
如上文所述,顶点的顺序决定了多段线的方向。vecData 中顶点的先后次序决定了这里"上一段"的方向;根据该切向,新弧段即可与已有路径平滑衔接。
4.2 默认方式:直接指定端点(切向定弧)
确定切向后,用户只需指定圆弧端点——起点、端点、起点切向三者唯一确定一段圆弧。用辅助函数 CalcArcBulge 由这三者计算凸度,核心思路是:圆心既在弦的垂直平分线上、又在起点切线的垂线上,两线求交得到圆心;选取与切向一致的弧中点;最后三点调用 calcBulge:
// ---------- 辅助函数:由起点、端点、起点切向计算凸度 ---------- // 思路:圆心同时在弦的垂直平分线与起点切线的垂线上,两线求交得圆心; // 选取与切向一致的弧中点后,用三点调用 calcBulge function CalcArcBulge(firstPoint: McGePoint3d, nextPoint: McGePoint3d, vecArcTangent: McGeVector3d): number { if (firstPoint.isEqualTo(nextPoint)) return 0; // 弦中点,及弦的垂直平分线 const midPt = firstPoint.c().addvec(nextPoint.c().sub(firstPoint).mult(0.5)); const vecMid = nextPoint.c().sub(firstPoint); vecMid.rotateBy(Math.PI / 2, McGeVector3d.kZAxis); const tmpMidLine = new McDbLine(midPt, midPt.c().addvec(vecMid)); // 起点切线的垂线 const vecVertical = vecArcTangent.c(); vecVertical.rotateBy(Math.PI / 2, McGeVector3d.kZAxis); const tmpVerticalLine = new McDbLine(firstPoint, firstPoint.c().addvec(vecVertical)); // 两线求交得圆心 const aryPoint = tmpMidLine.IntersectWith(tmpVerticalLine, McDb.Intersect.kExtendBoth); if (aryPoint.isEmpty()) return 0; const arcCenPoint = aryPoint.at(0); const dR = arcCenPoint.distanceTo(firstPoint); // 从圆心出发有两个候选弧中点,选取与切向夹角更小的一个 vecMid.normalize(); vecMid.mult(dR); const arcMidPt1 = arcCenPoint.c().addvec(vecMid); const arcMidPt2 = arcCenPoint.c().subvec(vecMid); const vecArcDir1 = arcMidPt1.c().sub(firstPoint); const vecArcDir2 = arcMidPt2.c().sub(firstPoint); const arcMidPt = (vecArcDir1.angleTo1(vecArcTangent) > vecArcDir2.angleTo1(vecArcTangent)) ? arcMidPt2 : arcMidPt1; return MxCADUtility.calcBulge(firstPoint, arcMidPt, nextPoint).val; } getNextPoint.setUserDraw((currentPoint, pWorldDraw) => { // Ctrl 键按下时切向取反 → 弧的凹凸方向实时翻转 const tangent = isCtrl ? vecArcTangent.clone().negate() : vecArcTangent; const dBulge = CalcArcBulge(startPt, currentPoint, tangent); const pl = new McDbPolyline(); pl.addVertexAt(startPt, dBulge, dStartWidth, dEndWidth); pl.addVertexAt(currentPoint); pWorldDraw.drawMcDbEntity(pl, true); }); const ptNext = await getNextPoint.go(); if (ptNext !== null) { // 确认:凸度写回当前顶点,新端点入数组 // 凸度写回的是弧段的起始顶点,新端点的凸度保持 0 vecData[vecData.length - 1].dBulge = CalcArcBulge( startPt, ptNext, isCtrl ? vecArcTangent.clone().negate() : vecArcTangent); vecData.push({ pt: ptNext, dBulge: 0, dStartWidth, dEndWidth }); commitSegment(); }
4.3 角度 (A):由夹角解直角三角形
如果用户先输入圆弧的包含角、再指定端点,可以借助直角三角形求解:把半弦长作为对边,结合夹角解出半径与弦心距,进而定位弧中点,最后交给 calcBulge:
const getBulge = (pt: McGePoint3d) => { // 弦中点与半弦长(直角三角形对边) const midPt = new McGePoint3d((pt.x + startPoint.x) / 2, (pt.y + startPoint.y) / 2); const oppositeSide = midPt.distanceTo(startPoint); // 解直角三角形:半径 = 斜边,弦心距 = 邻边 const angleA = Math.PI / 2 - angle / 2; const radius = oppositeSide / Math.sin(angleA); const adjacentEdge = oppositeSide / Math.tan(angleA); // 弦中点沿垂线偏移"矢高"(半径 − 弦心距)得弧中点;Ctrl 切换优弧方向 const vet = midPt.sub(startPoint).rotateBy(Math.PI / 2).normalize() .mult(isCtrl ? -radius - adjacentEdge : radius - adjacentEdge); const midPoint = midPt.addvec(vet); return MxCADUtility.calcBulge(startPoint, midPoint, pt).val; };
4.4 其余定弧方式
用同样的思路,还可以实现其余几种定弧方式:
-
第二个点 (S): 起点、弧上点、端点直接定弧,预览阶段用
McDbArc.computeArc实时拟合,确认后转算凸度; -
圆心 (CE): 圆心 + 端点(或角度、弦长)定弧,用向量夹角区分优弧劣弧;弦长超过直径时提示无效;
-
半径 (R):已知半径反解圆心位置,端点距起点超过 2 倍半径同样判为无效;
-
方向 (D):让用户手动指定起点切向,覆盖自动继承的切向,用于需要强制指定出弧方向的场景。
这些方式的实现目标相同:计算凸度,并写入当前顶点。
4.5 Ctrl 键:Web 端的方向切换交互
桌面 CAD 中切换弧的凹凸方向需要走命令行子选项,而 Web 端的命令可以直接利用浏览器键盘事件——按住 Ctrl 键即可实时翻转弧方向:切向定弧时对切向取反,角度模式改取优弧,三点模式把弧上点关于圆心对称。光标移动过程中按住/松开 Ctrl,预览图形实时翻转:
let isCtrl = false; window.addEventListener("keydown", (e) => { if (e.key === "Control") isCtrl = true; }); window.addEventListener("keyup", () => isCtrl = false);
5. 收尾:闭合、清理与最终提交
命令结束时需要完成三项处理:
// 1. 圆弧模式下闭合:闭合段同样遵循切向约束,按当前切向计算凸度 if (getNextPoint.isKeyWordPicked("C")) { vecData[vecData.length - 1].dBulge = CalcArcBulge( currentPt, firstPoint, isCtrl ? vecArcTangent.clone().negate() : vecArcTangent); isClose = true; break; } // 2. 清空全部临时实体 for (let i = 0; i < vecTmpObjectId.length; i++) { vecTmpObjectId[i].erase(); } // 3. 用完整的顶点数据一次性生成最终多段线 if (vecData.length > 1) { const pNew = new McDbPolyline(); for (let i = 0; i < vecData.length; i++) { pNew.addVertexAt(vecData[i].pt, vecData[i].dBulge, vecData[i].dStartWidth, vecData[i].dEndWidth); } pNew.isClosed = isClose; return mxcad.drawEntity(pNew); }
最后,把命令注册进 MxCAD,使其与内置命令一样可以通过命令行调用。
6. 整合:完整的 Mx_Pline 命令
前五个小节分别实现了命令的各个组成部分。最后一步是把它们按执行顺序组装起来,形成完整的 drawPolyLine 函数。各部分的对应关系如下:
| 代码位置 | 对应小节 | 内容 |
|---|---|---|
| 导入与辅助函数 | 4.2 | CalcArcBulge:由起点、端点、切向计算凸度 |
| 状态变量 | 2.1 / 4.5 | 顶点数组 vecData、临时实体数组、宽度变量、模式开关、Ctrl 监听 |
| 主循环 - 直线分支 | 3 | 取点、宽度设置、回退、闭合、切换圆弧模式 |
| 主循环 - 圆弧分支 | 4 | 切向继承、按切向定弧、凸度写回起始顶点 |
| 循环结束后 | 5 | 删除临时实体、生成最终多段线 |
完整代码如下:
import { McDb, McDbLine, McDbPolyline, McGePoint3d, McGeVector3d, McObjectId, MxCADUiPrPoint, MxCADUiPrDist, MxCADUtility, MxCpp, MxFun } from "mxcad"; export async function drawPolyLine() { const mxcad = MxCpp.getCurrentMxCAD(); // 【2.1】顶点数组;【2.2】临时实体数组 const vecData: VertexData[] = []; const vecTmpObjectId: McObjectId[] = []; // 宽度变量:当前段宽度,同时延续为后续段的默认值 let dStartWidth = 0; let dEndWidth = 0; // 【2.3】模式开关与闭合标志 let isDrawLine = true; let isClose = false; // 【4.5】Ctrl 键监听:按住时实时翻转弧方向 let isCtrl = false; const onKeydown = (e: KeyboardEvent) => { if (e.key === "Control") isCtrl = true; }; const onKeyup = () => isCtrl = false; window.addEventListener("keydown", onKeydown); window.addEventListener("keyup", onKeyup); // 【2.2】提交一段:用最近两个顶点画临时多段线 function commitSegment() { const len = vecData.length; if (len < 2) return; const seg = new McDbPolyline(); seg.addVertexAt(vecData[len - 2].pt, vecData[len - 2].dBulge, vecData[len - 2].dStartWidth, vecData[len - 2].dEndWidth); seg.addVertexAt(vecData[len - 1].pt); vecTmpObjectId.push(mxcad.drawEntity(seg)); } // 【2.2】回退(U):弹出顶点数据 + 删除最近临时实体 function undoSegment(getNextPoint: MxCADUiPrPoint) { if (vecData.length <= 1) return; vecData.pop(); vecTmpObjectId[vecTmpObjectId.length - 1].erase(); vecTmpObjectId.pop(); getNextPoint.setLastInputPoint(vecData[vecData.length - 1].pt); } // 取起点,加入顶点数组 const getFirstPoint = new MxCADUiPrPoint(); getFirstPoint.setMessage("\n指定起点:"); const firstPoint = await getFirstPoint.go(); if (!firstPoint) return; vecData.push({ pt: firstPoint, dBulge: 0, dStartWidth: 0, dEndWidth: 0 }); // 【2.3】主循环:根据模式切换交互分支 while (true) { const getNextPoint = new MxCADUiPrPoint(); if (isDrawLine) { // 【3】直线模式 getNextPoint.setMessage("\n指定下一个点:"); // 关键字随顶点数动态变化:≥3 个顶点才允许闭合(与 AutoCAD 一致) let sKeyWord = "[圆弧(A)/宽度(W)/长度(L)/半宽(H)]"; if (vecData.length >= 3) sKeyWord = "[回退(U)/圆弧(A)/宽度(W)/长度(L)/半宽(H)/闭合(C)]"; else if (vecData.length >= 2) sKeyWord = "[回退(U)/圆弧(A)/宽度(W)/长度(L)/半宽(H)]"; getNextPoint.setKeyWords(sKeyWord); // 动态预览:光标移动时实时画出"上一顶点 → 光标"的橡皮筋段 const drawData = vecData[vecData.length - 1]; getNextPoint.setUserDraw((currentPoint, pWorldDraw) => { const pl = new McDbPolyline(); pl.addVertexAt(drawData.pt, drawData.dBulge, drawData.dStartWidth, drawData.dEndWidth); pl.addVertexAt(currentPoint); pWorldDraw.drawMcDbEntity(pl, true); }); const ptNext = await getNextPoint.go(); if (ptNext !== null) { // 取点成功:保存新顶点数据(凸度 0),提交临时段 vecData.push({ pt: ptNext, dBulge: 0, dStartWidth, dEndWidth }); commitSegment(); } else if (getNextPoint.isKeyWordPicked("A")) { isDrawLine = false; // 切换圆弧模式 } else if (getNextPoint.isKeyWordPicked("W")) { // 宽度:起点宽、端点宽两步输入 const getWidth = new MxCADUiPrDist(); getWidth.setMessage("指定起点宽度"); if (await getWidth.go() === null) break; dStartWidth = getWidth.value(); getWidth.setMessage("指定端点宽度"); if (await getWidth.go() === null) break; dEndWidth = getWidth.value(); // 宽度写回当前顶点,作用于下一段;端点宽延续为后续默认值 vecData[vecData.length - 1].dStartWidth = dStartWidth; vecData[vecData.length - 1].dEndWidth = dEndWidth; dStartWidth = dEndWidth; } else if (getNextPoint.isKeyWordPicked("H")) { // 半宽:与宽度相同流程,输入值 ×2(实现省略) } else if (getNextPoint.isKeyWordPicked("L")) { // 长度:沿上一段方向推算新顶点(实现省略) } else if (getNextPoint.isKeyWordPicked("C")) { isClose = true; break; // 闭合 } else if (getNextPoint.isKeyWordPicked("U")) { undoSegment(getNextPoint); // 回退 } else { break; // 右键/回车结束 } } else { // 【4】圆弧模式 // 【4.1】切向继承:上一段为直线取直线方向,为圆弧取终点切线方向 let vecArcTangent = new McGeVector3d(); if (vecData.length < 2) { vecArcTangent.copy(McGeVector3d.kXAxis); } else { const size = vecData.length; const pt1 = vecData[size - 2].pt; const dBulge = vecData[size - 2].dBulge; const pt2 = vecData[size - 1].pt; if (dBulge === 0) { vecArcTangent = pt2.sub(pt1); } else { const tmpPl = new McDbPolyline(); tmpPl.addVertexAt(pt1, dBulge); tmpPl.addVertexAt(pt2); const tmpVec = tmpPl.getFirstDeriv(new McGePoint3d(pt2.x, pt2.y, 0)); if (tmpVec.ret) vecArcTangent = tmpVec.val; else vecArcTangent.copy(McGeVector3d.kXAxis); } } // 本节仅实现默认方式(切向定弧); // 角度(A)、第二个点(S)、圆心(CE)、半径(R)、方向(D) 见 4.3 ~ 4.4 小节 getNextPoint.setMessage("\n指定圆弧的端点(按住 Ctrl 键以切换方向)"); getNextPoint.setKeyWords("[直线(L)/宽度(W)/半宽(H)]"); const startPt = vecData[vecData.length - 1].pt; // 【4.2】动态预览:光标位置即端点,实时计算凸度并预览 getNextPoint.setUserDraw((currentPoint, pWorldDraw) => { const tangent = isCtrl ? vecArcTangent.clone().negate() : vecArcTangent; const dBulge = CalcArcBulge(startPt, currentPoint, tangent); const pl = new McDbPolyline(); pl.addVertexAt(startPt, dBulge, dStartWidth, dEndWidth); pl.addVertexAt(currentPoint); pWorldDraw.drawMcDbEntity(pl, true); }); const ptNext = await getNextPoint.go(); if (ptNext !== null) { // 【4.2】凸度写回弧段起始顶点,新端点凸度保持 0 vecData[vecData.length - 1].dBulge = CalcArcBulge( startPt, ptNext, isCtrl ? vecArcTangent.clone().negate() : vecArcTangent); vecData.push({ pt: ptNext, dBulge: 0, dStartWidth, dEndWidth }); commitSegment(); } else if (getNextPoint.isKeyWordPicked("L")) { isDrawLine = true; // 切回直线模式 } else if (getNextPoint.isKeyWordPicked("W")) { // 宽度设置,与直线模式相同(实现省略) } else if (getNextPoint.isKeyWordPicked("H")) { // 半宽设置,与直线模式相同(实现省略) } else if (getNextPoint.isKeyWordPicked("C")) { // 【5】圆弧模式闭合:闭合段按当前切向计算凸度 vecData[vecData.length - 1].dBulge = CalcArcBulge( startPt, firstPoint, isCtrl ? vecArcTangent.clone().negate() : vecArcTangent); isClose = true; break; } else if (getNextPoint.isKeyWordPicked("U")) { undoSegment(getNextPoint); // 回退 } else { break; // 右键/回车结束 } } } //【5】收尾处理 // 1. 删除全部临时实体 for (let i = 0; i < vecTmpObjectId.length; i++) { vecTmpObjectId[i].erase(); } // 2. 用完整的顶点数据一次性生成最终多段线 if (vecData.length > 1) { const pNew = new McDbPolyline(); for (let i = 0; i < vecData.length; i++) { pNew.addVertexAt(vecData[i].pt, vecData[i].dBulge, vecData[i].dStartWidth, vecData[i].dEndWidth); } pNew.isClosed = isClose; mxcad.drawEntity(pNew); } // 3. 移除键盘监听 window.removeEventListener("keydown", onKeydown); window.removeEventListener("keyup", onKeyup); } // 注册命令 MxFun.on("init", () => { MxFun.addCommand("Mx_Pline", drawPolyLine); });
几点说明:
-
完整度边界:上方代码是可直接运行的完整框架,覆盖取点、宽度、回退、闭合、切向定弧与动态预览;半宽与长度选项标记为"实现省略",其余定弧方式(角度、第二个点、圆心、半径、方向)可参照 4.3、4.4 小节的思路逐一补齐;
-
组装顺序: 辅助函数 → 状态变量 → 两个局部函数(
commitSegment/undoSegment)→ 取起点 → 主循环 → 收尾,即"先定义、后使用"的自然顺序; -
局部函数的好处:
commitSegment与undoSegment定义为drawPolyLine内部的函数,可以直接访问vecData与vecTmpObjectId,不需要额外传参。
至此,一个与 AutoCAD PLINE 交互一致、支持直线/圆弧混合绘制、宽度可变、可回退、可闭合、带实时动态预览的多段线命令就完成了。

四、总结
我们从零构建了一个完整的多段线绘制命令,整个命令的实现遵循一套可复用的思路:规划交互选项 → 数据收集与实体生成分离 → 按模式分别实现 → 收尾处理 → 整合注册
浙公网安备 33010602011771号