唯杰地图 logo唯杰地图
VJMAP
EN

示例 A:80×60 圆角板 + 5 孔 + 标注

vjcad/references/autodraw/examples/plate-and-cat.md 17.5 KB452 行
原文

本文回答:把 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 防重叠

六、红旗 / 反例

  1. 角孔写在圆角弧内:上面的 [12,12] 已离弧仅 0.9 mm;R 越大、坐标要越往内挪。
  2. bulge 写成度数 / 弧度:bulge 是几何凸度量,不是角度;90° 弧固定 tan(π/8) ≈ 0.4142。
  3. 孔实体落在 OUTLINE 层:用户要"分别上色 / 单独消隐 / 单独导出"时无法操作;必须 setCurrentLayer('HOLE') 后再创建。
  4. Φ16 用 RadialDimensionEnt:得到 R8 而不是 Φ16;直径必须 DiametricDimensionEnt(p1, p2)。
  5. 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 反例)。

六、红旗 / 反例

  1. 所有部位都堆 OUTLINE 层 → 全一种颜色,看不清结构;至少分 EYE / NOSE / DECOR / WHISKER 4 层。
  2. SolidEnt(p1, p3, p2, p4) 顺序错 → 沙漏形(自相交),三角形让 p3 === p4。
  3. 椭圆 major/minor 搞反 → 头变窄(细高),猫看起来像狐狸。
  4. 粉色 RGB 写成 0xFFB7C5 当作 ACI 索引:会被裁成普通 ACI;要走 ColorConverter.createRgbColorIndexFromHexStr('#FFB7C5') 加标记位。
  5. HatchEnt.setLoops 漏掉 → 蝴蝶结只有边线没有填充。
  6. 嘴 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