> **本文回答**：把 [autodraw/overview](../overview.md) 的"五步骨架"用在两个具体需求上：① 工程类——80×60 圆角板 + 5 个孔 + 标注；② 艺术类——细节丰富的小猫。两例都按 `图层 → 外形 → 特征 → 填充 → 标注 / 装饰 → addEntities` 的同一节奏写，所以一份代码骨架两次复用。

合一的目的：让用户看到**同一套通用范式**既能画**严格规格化**的工程图，也能画**自由构造**的艺术图——决定差异的只有"图层规划"和"实体清单"，骨架完全一样。

---

# 示例 A：80×60 圆角板 + 5 孔 + 标注

## 一、需求拆解

> 画一个长 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](../overview.md) 第八节红旗 14）。

## 二、生成步骤

```mermaid
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"]
```

## 三、完整可运行代码

```js
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](../../entity/polyline.md) 第五节） |
| **角孔位置 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 个图层。

## 二、生成步骤

```mermaid
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"]
```

## 三、完整可运行代码

```js
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](../../entity/hatch-solid.md) 第三节）；4 点要按四边形顺序，否则沙漏 |
| **眼瞳填充** | 简化用 `pupil.color = 0`（不带填充看起来仍像瞳），更精细可加 `HatchEnt` + `patternName='SOLID'` |
| **嘴 W 形** | 2 段 `ArcEnt`：`(center, r, π, 2π)` = 下半圆，左右两个组成 W 微笑 |
| **蝴蝶结填充** | 用 `wing.bulgePoints.clone()` 喂 `Edge.bulgePoints`（详 [entity/hatch-solid](../../entity/hatch-solid.md) 第一节） |
| **粉 / 黄色用 RGB** | 经 `ColorConverter.createRgbColorIndexFromHexStr('#FFB7C5')` 得到带 RGB 标记的色号（详 [property/color-linetype](../../property/color-linetype.md)） |

## 五、镜像替代方案（可选）

胡须、耳朵、眼睛、爪子都左右对称。若想减少手写量，可只构造"左半"再镜像：

```js
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](../../edit/mirror.md) 反例）。

## 六、红旗 / 反例

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](../overview.md)（五步、图层方案、撤销组、`writeMessage`）
- 实体：[entity/polyline](../../entity/polyline.md)、[entity/line-arc-circle](../../entity/line-arc-circle.md)、[entity/spline-ellipse](../../entity/spline-ellipse.md)、[entity/hatch-solid](../../entity/hatch-solid.md)、[entity/dimension-mleader](../../entity/dimension-mleader.md)
- 图层 / 属性：[layer/create-switch](../../layer/create-switch.md)、[property/color-linetype](../../property/color-linetype.md)、[property/set-defaults](../../property/set-defaults.md)
- 编辑：[edit/mirror](../../edit/mirror.md)
- 撤销：[undo/group](../../undo/group.md)
- 视图：[view/zoom](../../view/zoom.md)
- 文档：`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`
