示例 A:80×60 圆角板 + 5 孔 + 标注
本文回答:把 autodraw/overview 的"五步骨架"用在两个具体需求上:① 工程类——80×60 圆角板 + 5 个孔 + 标注;② 艺术类——细节丰富的小猫。两例都按
图层 → 外形 → 特征 → 填充 → 标注 / 装饰 → addEntities的同一节奏写,所以一份代码骨架两次复用。
合一的目的:让用户看到同一套通用范式既能画严格规格化的工程图,也能画自由构造的艺术图——决定差异的只有"图层规划"和"实体清单",骨架完全一样。
一、需求拆解
画一个长 80 宽 60 的板,周围带四个 Φ8 孔,中心带一个 Φ16 孔,四角圆角 R5,并进行长度标注。
| 自然语言要素 | 图形要素 | 图层 | 实体类 | 数量 |
|---|---|---|---|---|
| 长 80 宽 60 板 + R5 圆角 | 外形 | OUTLINE |
PolylineEnt (闭合 + bulge) |
1 |
| 中心 Φ16 | 孔 | HOLE |
CircleEnt |
1 |
| 周围 4 个 Φ8 | 孔阵列 | HOLE |
CircleEnt × 4 |
4 |
| 长度标注 | 标注 | DIM |
LinearDimensionEnt |
4 |
| Φ 标注 | 标注 | DIM |
DiametricDimensionEnt |
2 |
| R5 圆角标注 | 标注 | DIM |
MLeaderEnt |
1 |
总实体 ≈ 13 个。"周围"按工程惯例 = 四角 —— 假设要在 writeMessage 中输出让用户复核(详见 autodraw/overview 第八节红旗 14)。
二、生成步骤
flowchart LR
L["1.ensureLayers<br/>OUTLINE/HOLE/DIM"] --> O["2.圆角矩形<br/>PolylineEnt+bulge"]
O --> H["3.孔<br/>CircleEnt×5"]
H --> D["4.标注<br/>4 Linear+2 Diametric+1 MLeader"]
D --> C["5.commit<br/>addEntities+regen+zoomExtents"]
三、完整可运行代码
import {
Engine, PolylineEnt, BulgePoint, Point2D,
CircleEnt, LinearDimensionEnt, DiametricDimensionEnt,
MLeaderEnt, MLeaderContentType, writeMessage
} from 'vjcad';
await cadView.onLoad(); // 见 setup/install-and-mcp 与 core/public-api
drawPlate({ w: 80, h: 60, r: 5, holeR: 4, centerHoleR: 8 });
function drawPlate(spec) {
Engine.startUndoRecord('AUTO_DRAW_PLATE');
try {
ensurePlateLayers();
const ents = [];
ents.push(buildOutline(spec));
ents.push(...buildHoles(spec));
ents.push(...buildDims(spec));
Engine.addEntities(ents);
writeMessage(`板生成完成,共 ${ents.length} 个实体(假设孔位为四角)`);
} finally {
Engine.endUndoRecord();
}
Engine.regen();
Engine.zoomExtents();
}
function ensurePlateLayers() {
const defs = [
{ name: 'OUTLINE', color: 7, lineType: 'CONTINUOUS' },
{ name: 'HOLE', color: 1, lineType: 'CONTINUOUS' },
{ name: 'DIM', color: 3, lineType: 'CONTINUOUS' }
];
for (const d of defs) {
if (!Engine.currentDoc.layers.itemByName(d.name)) {
Engine.createLayer(d.name, d);
}
}
}
function buildOutline({ w, h, r }) {
Engine.setCurrentLayer('OUTLINE');
Engine.CECOLOR = 256;
Engine.CELTYPE = 'ByLayer';
const bulge = Math.tan(Math.PI / 8); // 90° 圆角
const minX = 0, maxX = w, minY = 0, maxY = h;
const rect = new PolylineEnt();
rect.bulgePoints.add(new BulgePoint(new Point2D(minX + r, minY), 0));
rect.bulgePoints.add(new BulgePoint(new Point2D(maxX - r, minY), bulge));
rect.bulgePoints.add(new BulgePoint(new Point2D(maxX, minY + r), 0));
rect.bulgePoints.add(new BulgePoint(new Point2D(maxX, maxY - r), bulge));
rect.bulgePoints.add(new BulgePoint(new Point2D(maxX - r, maxY), 0));
rect.bulgePoints.add(new BulgePoint(new Point2D(minX + r, maxY), bulge));
rect.bulgePoints.add(new BulgePoint(new Point2D(minX, maxY - r), 0));
rect.bulgePoints.add(new BulgePoint(new Point2D(minX, minY + r), bulge));
rect.isClosed = true;
rect.setDefaults();
return rect;
}
function buildHoles({ w, h, holeR, centerHoleR }) {
Engine.setCurrentLayer('HOLE');
const out = [];
const cx = w / 2, cy = h / 2;
const center = new CircleEnt([cx, cy], centerHoleR);
center.setDefaults();
out.push(center);
const corners = [[12, 12], [w - 12, 12], [w - 12, h - 12], [12, h - 12]];
for (const [x, y] of corners) {
const c = new CircleEnt([x, y], holeR);
c.setDefaults();
out.push(c);
}
return out;
}
function buildDims({ w, h, holeR, centerHoleR, r }) {
Engine.setCurrentLayer('DIM');
const dims = [];
const dimLen = new LinearDimensionEnt(
new Point2D(0, 0), new Point2D(w, 0), new Point2D(w / 2, -15), 0);
dimLen.setDefaults();
dims.push(dimLen);
const dimWid = new LinearDimensionEnt(
new Point2D(0, 0), new Point2D(0, h), new Point2D(-15, h / 2), Math.PI / 2);
dimWid.setDefaults();
dims.push(dimWid);
const dimXPitch = new LinearDimensionEnt(
new Point2D(12, 0), new Point2D(w - 12, 0), new Point2D(w / 2, -30), 0);
dimXPitch.setDefaults();
dims.push(dimXPitch);
const dimYPitch = new LinearDimensionEnt(
new Point2D(w, 12), new Point2D(w, h - 12), new Point2D(w + 15, h / 2), Math.PI / 2);
dimYPitch.setDefaults();
dims.push(dimYPitch);
const dimD16 = new DiametricDimensionEnt(
new Point2D(w / 2 - centerHoleR, h / 2),
new Point2D(w / 2 + centerHoleR, h / 2));
dimD16.setDefaults();
dims.push(dimD16);
const dimD8 = new DiametricDimensionEnt(
new Point2D(12 - holeR, 12), new Point2D(12 + holeR, 12));
dimD8.setDefaults();
dims.push(dimD8);
const ml = new MLeaderEnt();
ml.contentType = MLeaderContentType.MText;
ml.textContent = `R${r}`;
ml.textPosition = new Point2D(w + 8, h + 8);
ml.textHeight = 5;
ml.addLeaderLine(new Point2D(w - r * 0.3, h - r * 0.3));
ml.setDefaults();
dims.push(ml);
return dims;
}
四、关键技术点
| 点 | 说明 |
|---|---|
| 圆角矩形 8 顶点 | 每个 90° 直角拐点 → 2 个切点 + bulge=tan(π/8),共 8 顶点(详 entity/polyline 第五节) |
| 角孔位置 vs 圆角间隙 | 角孔 [12,12] 到圆角圆心 [5,5] 距离 ≈ 9.9 mm;孔半径 4 + 圆角 5 = 9 → 仅余 0.9 mm(极紧),实际项目要把孔往内移到 [15,15] 之类 |
DiametricDimensionEnt 取直径两端 |
圆心 ± 半径 的两个点,注意不是 (center, radius) |
LinearDimensionEnt 第四参 |
测量方向弧度:水平 0、垂直 Math.PI/2 |
| MLeader R5 | 引线箭头落到圆角弧上(这里取拐点处 [w-r·0.3, h-r·0.3],避免落到弧外) |
五、标注布局规则
- 总长 dim line offset:
y = -15 - 孔间距 dim line offset:
y = -30(同方向,叠加step = 15) - 总宽 dim line offset:
x = -15 - 纵向孔间距 dim line offset:
x = w + 15= 95 - 字高建议 5~8 mm;
step ≥ 字高 × 2防重叠
六、红旗 / 反例
- 角孔写在圆角弧内:上面的 [12,12] 已离弧仅 0.9 mm;R 越大、坐标要越往内挪。
- bulge 写成度数 / 弧度:bulge 是几何凸度量,不是角度;90° 弧固定
tan(π/8) ≈ 0.4142。 - 孔实体落在 OUTLINE 层:用户要"分别上色 / 单独消隐 / 单独导出"时无法操作;必须
setCurrentLayer('HOLE')后再创建。 Φ16用RadialDimensionEnt:得到 R8 而不是 Φ16;直径必须DiametricDimensionEnt(p1, p2)。writeMessage没输出"四角"假设:用户复核不到,可能本来想要"四边中点"。
示例 B:可爱小猫(细节丰富)
一、需求拆解
绘制一只可爱的小猫,细节要丰富。
艺术类需求没有刚性规格,所以拆解时要做"清单决策"——列出可识别的部位,每个部位选一种最简单的实体表达:
| 部位 | 图层 | 实体 | 数量 |
|---|---|---|---|
| 头(椭圆) | OUTLINE |
EllipseEnt |
1 |
| 耳朵(外) | OUTLINE |
SolidEnt 三角 |
2 |
| 耳朵(内粉) | DECOR |
SolidEnt 小三角 |
2 |
| 眼睛(白) | EYE |
EllipseEnt |
2 |
| 瞳孔 | EYE |
CircleEnt |
2 |
| 鼻 | NOSE |
SolidEnt 倒三角 |
1 |
| 嘴 (W 形) | OUTLINE |
ArcEnt × 2 |
2 |
| 胡须 | WHISKER |
LineEnt × 6 |
6 |
| 身体 | OUTLINE |
EllipseEnt |
1 |
| 前爪(2 个) | OUTLINE |
EllipseEnt × 2 |
2 |
| 尾 | OUTLINE |
SplineEnt |
1 |
| 蝴蝶结左右翼 | DECOR |
PolylineEnt × 2 + HatchEnt × 2 |
4 |
| 蝴蝶结结心 | DECOR |
PolylineEnt 闭合矩形 |
1 |
| 装饰:腮红 | DECOR |
CircleEnt × 2 |
2 |
总 ≈ 28-30 实体,6 个图层。
二、生成步骤
flowchart LR
L["1.ensureLayers<br/>OUTLINE/EYE/NOSE/<br/>WHISKER/DECOR"]
L --> H["2.head<br/>椭圆+耳朵+腮红"]
H --> F["3.face<br/>眼+鼻+嘴+胡须"]
F --> B["4.body<br/>身体+爪+尾"]
B --> A["5.accent<br/>蝴蝶结(Polyline+Hatch)"]
A --> C["6.commit<br/>addEntities+regen+zoomExtents"]
三、完整可运行代码
import {
Engine, EllipseEnt, CircleEnt, ArcEnt, LineEnt, SolidEnt, SplineEnt,
PolylineEnt, HatchEnt, Edge, Edges, EdgeType, Point2D,
ColorConverter, writeMessage
} from 'vjcad';
await cadView.onLoad();
drawCat();
function drawCat() {
Engine.startUndoRecord('AUTO_DRAW_CAT');
try {
ensureCatLayers();
const ents = [];
ents.push(...buildHeadAndEars());
ents.push(...buildFace());
ents.push(...buildBodyTail());
ents.push(...buildBowtie());
Engine.addEntities(ents);
writeMessage(`小猫共 ${ents.length} 个实体`);
} finally {
Engine.endUndoRecord();
}
Engine.regen();
Engine.zoomExtents();
}
function ensureCatLayers() {
const PINK = ColorConverter.createRgbColorIndexFromHexStr('#FFB7C5');
const YELLOW = ColorConverter.createRgbColorIndexFromHexStr('#FFD43A');
const defs = [
{ name: 'OUTLINE', color: 7, lineType: 'CONTINUOUS' },
{ name: 'EYE', color: 5, lineType: 'CONTINUOUS' },
{ name: 'NOSE', color: PINK, lineType: 'CONTINUOUS' },
{ name: 'WHISKER', color: 8, lineType: 'CONTINUOUS' },
{ name: 'DECOR', color: YELLOW, lineType: 'CONTINUOUS' }
];
for (const d of defs) {
if (!Engine.currentDoc.layers.itemByName(d.name)) {
Engine.createLayer(d.name, d);
}
}
}
function buildHeadAndEars() {
Engine.setCurrentLayer('OUTLINE');
Engine.CECOLOR = 256;
Engine.CELTYPE = 'ByLayer';
const out = [];
const head = new EllipseEnt([0, 40], 30, 25, 0);
head.setDefaults(); out.push(head);
const earL = new SolidEnt([-25, 60], [-8, 55], [-22, 80], [-22, 80]);
const earR = new SolidEnt([ 25, 60], [ 8, 55], [ 22, 80], [ 22, 80]);
earL.setDefaults(); earR.setDefaults();
out.push(earL, earR);
Engine.setCurrentLayer('DECOR');
const innerL = new SolidEnt([-21, 62], [-12, 58], [-19, 73], [-19, 73]);
const innerR = new SolidEnt([ 21, 62], [ 12, 58], [ 19, 73], [ 19, 73]);
innerL.setDefaults(); innerR.setDefaults();
out.push(innerL, innerR);
const blushL = new CircleEnt([-18, 35], 4);
const blushR = new CircleEnt([ 18, 35], 4);
blushL.setDefaults(); blushR.setDefaults();
out.push(blushL, blushR);
return out;
}
function buildFace() {
const out = [];
Engine.setCurrentLayer('EYE');
const eyeL = new EllipseEnt([-10, 45], 5, 7, 0);
const eyeR = new EllipseEnt([ 10, 45], 5, 7, 0);
eyeL.setDefaults(); eyeR.setDefaults();
out.push(eyeL, eyeR);
const pupilL = new CircleEnt([-10, 46], 2);
const pupilR = new CircleEnt([ 10, 46], 2);
pupilL.setDefaults(); pupilR.setDefaults();
pupilL.color = 0; pupilR.color = 0;
out.push(pupilL, pupilR);
Engine.setCurrentLayer('NOSE');
const nose = new SolidEnt([-3, 35], [3, 35], [0, 31], [0, 31]);
nose.setDefaults();
out.push(nose);
Engine.setCurrentLayer('OUTLINE');
const mouthL = new ArcEnt([-3, 31], 2, Math.PI, 2 * Math.PI);
const mouthR = new ArcEnt([ 3, 31], 2, Math.PI, 2 * Math.PI);
mouthL.setDefaults(); mouthR.setDefaults();
out.push(mouthL, mouthR);
Engine.setCurrentLayer('WHISKER');
const whiskers = [
[[-22, 36], [-40, 38]], [[-22, 33], [-40, 33]], [[-22, 30], [-40, 28]],
[[ 22, 36], [ 40, 38]], [[ 22, 33], [ 40, 33]], [[ 22, 30], [ 40, 28]]
];
for (const [a, b] of whiskers) {
const ln = new LineEnt(a, b);
ln.setDefaults();
out.push(ln);
}
return out;
}
function buildBodyTail() {
Engine.setCurrentLayer('OUTLINE');
const out = [];
const body = new EllipseEnt([0, -10], 32, 22, 0);
body.setDefaults(); out.push(body);
const pawL = new EllipseEnt([-15, -30], 6, 4, 0);
const pawR = new EllipseEnt([ 15, -30], 6, 4, 0);
pawL.setDefaults(); pawR.setDefaults();
out.push(pawL, pawR);
const tail = new SplineEnt();
tail.setControlPoints([[28, -10], [40, 0], [50, 15], [55, 30], [50, 40]]);
tail.setDefaults();
out.push(tail);
return out;
}
function buildBowtie() {
Engine.setCurrentLayer('DECOR');
const out = [];
const wingL = new PolylineEnt();
wingL.setPoints([[-3, 12], [-15, 18], [-15, 6]]);
wingL.isClosed = true;
wingL.setDefaults();
const wingR = new PolylineEnt();
wingR.setPoints([[3, 12], [15, 18], [15, 6]]);
wingR.isClosed = true;
wingR.setDefaults();
const knot = new PolylineEnt();
knot.setPoints([[-3, 14], [3, 14], [3, 10], [-3, 10]]);
knot.isClosed = true;
knot.setDefaults();
out.push(wingL, wingR, knot);
for (const wing of [wingL, wingR]) {
const h = new HatchEnt();
h.patternName = 'ANSI31';
const edge = new Edge();
edge.edgeType = EdgeType.Polyline;
edge.bulgePoints = wing.bulgePoints.clone();
const edges = new Edges();
edges.add(edge);
h.setLoops(edges);
h.setDefaults();
out.push(h);
}
return out;
}
四、关键技术点
| 点 | 说明 |
|---|---|
| 椭圆旋转 = 0 | EllipseEnt(center, major, minor, 0):第二参 major / 第三参 minor,搞反会得到"竖椭圆" |
SolidEnt 三角 |
让 p3 === p4 退化为三角形(参 entity/hatch-solid 第三节);4 点要按四边形顺序,否则沙漏 |
| 眼瞳填充 | 简化用 pupil.color = 0(不带填充看起来仍像瞳),更精细可加 HatchEnt + patternName='SOLID' |
| 嘴 W 形 | 2 段 ArcEnt:(center, r, π, 2π) = 下半圆,左右两个组成 W 微笑 |
| 蝴蝶结填充 | 用 wing.bulgePoints.clone() 喂 Edge.bulgePoints(详 entity/hatch-solid 第一节) |
| 粉 / 黄色用 RGB | 经 ColorConverter.createRgbColorIndexFromHexStr('#FFB7C5') 得到带 RGB 标记的色号(详 property/color-linetype) |
五、镜像替代方案(可选)
胡须、耳朵、眼睛、爪子都左右对称。若想减少手写量,可只构造"左半"再镜像:
const leftWhiskers = [/* 3 条左侧 */];
const rightWhiskers = leftWhiskers.map(ln => {
const c = ln.clone();
c.mirror(new Point2D(0, 0), new Point2D(0, 100)); // 关于 Y 轴
return c;
});
ents.push(...leftWhiskers, ...rightWhiskers);
mirror(p1, p2)推荐用new Point2D(...)(详 edit/mirror 反例)。
六、红旗 / 反例
- 所有部位都堆
OUTLINE层 → 全一种颜色,看不清结构;至少分EYE / NOSE / DECOR / WHISKER4 层。 SolidEnt(p1, p3, p2, p4)顺序错 → 沙漏形(自相交),三角形让p3 === p4。- 椭圆 major/minor 搞反 → 头变窄(细高),猫看起来像狐狸。
- 粉色 RGB 写成
0xFFB7C5当作 ACI 索引:会被裁成普通 ACI;要走ColorConverter.createRgbColorIndexFromHexStr('#FFB7C5')加标记位。 HatchEnt.setLoops漏掉 → 蝴蝶结只有边线没有填充。- 嘴 ArcEnt 起止角写反 → 弧朝上变成"难过"表情:下弯笑用
(π, 2π),上弯哭用(0, π)。
参考(官方文档 MCP)
- 通用骨架:autodraw/overview(五步、图层方案、撤销组、
writeMessage) - 实体:entity/polyline、entity/line-arc-circle、entity/spline-ellipse、entity/hatch-solid、entity/dimension-mleader
- 图层 / 属性:layer/create-switch、property/color-linetype、property/set-defaults
- 编辑:edit/mirror
- 撤销:undo/group
- 视图:view/zoom
- 文档:
vjcad/doc/entity/polyline-ent.md、vjcad/doc/entity/circle-ent.md、vjcad/doc/entity/arc-ent.md、vjcad/doc/entity/ellipse-ent.md、vjcad/doc/entity/solid-ent.md、vjcad/doc/entity/hatch-ent.md、vjcad/doc/command/dimension-commands.md、vjcad/doc/layer/layers.md - 示例:
vjcad/example/entity/all/01entities.js、vjcad/example/entity/polyline/02rectangle.js、vjcad/example/entity/dimension/01linear.js、vjcad/example/entity/dimension/06diametric.js、vjcad/example/entity/mleader/01create.js、vjcad/example/entity/circle/01create.js、vjcad/example/entity/arc/01create.js、vjcad/example/entity/ellipse/01create.js、vjcad/example/entity/solid/01create.js、vjcad/example/entity/spline/01create.js、vjcad/example/entity/hatch/01create.js、vjcad/example/edit/05mirror.js、vjcad/example/property/01color.js、vjcad/example/layer/01create.js、vjcad/example/undo/03group.js