> **本文回答**：DN15 内螺纹黄铜截止阀机加工图 = **主视剖视 + 侧视外形** 双视图。把 [autodraw/overview](../overview.md) 五步骨架用在机械图上，重点示范：① 剖切区 `HatchEnt` 用 polyline `bulgePoints.clone()` 喂 `Edge`；② 螺纹示意 `LineEnt` 循环；③ 中心线用 `lineType='CENTER'`；④ 视图间距通过**绝对坐标分块**（不是变换平移）。

## 一、需求拆解

> 生成一个 DN15 内螺纹黄铜截止阀阀体机加工图纸，并合理布局各视图（主视、侧视、剖视），标注关键尺寸。

DN15 = 公称通径 15 mm，内螺纹 G1/2"。简化后的关键尺寸：

| 参数 | 值 |
|---|---|
| 法兰对法兰长度 L | 65 |
| 阀体最大直径 D | 50 |
| 螺纹端长 Lt | 12 |
| 阀腔内径（DN） | 15 |
| 阀杆 + 手轮高 | 80（其中手轮 R = 25） |

| 自然语言要素 | 图层 | 实体 | 数量 |
|---|---|---|---|
| 阀体外形（剖面侧轮廓） | `OUTLINE` | `PolylineEnt`（带 bulge） | 1 |
| 阀腔内壁（内孔） | `OUTLINE` | `PolylineEnt`（带 bulge） | 1 |
| 阀体壁 Hatch | `HATCH` | `HatchEnt` × 2（上 / 下壁） | 2 |
| 内螺纹示意 | `THREAD` | `LineEnt` × 20（4 段 × 5 线） | 20 |
| 阀座 / 阀瓣 | `OUTLINE` | `PolylineEnt` × 2 | 2 |
| 阀杆 | `OUTLINE` | `PolylineEnt`（细矩形） | 1 |
| 阀盖（bonnet） | `OUTLINE` | `PolylineEnt` 矩形 | 1 |
| 手轮 | `OUTLINE` | `CircleEnt` × 2 + `LineEnt` × 4 | 6 |
| 中心线（水平） | `CENTER` | `LineEnt` | 1 |
| 中心线（垂直） | `CENTER` | `LineEnt` | 1 |
| **侧视图** 外圆 | `OUTLINE` | `CircleEnt` | 1 |
| **侧视图** 内孔（DN15） | `OUTLINE` | `CircleEnt` | 1 |
| **侧视图** 中心十字线 | `CENTER` | `LineEnt` × 2 | 2 |
| 标注 | `DIM` | `LinearDimensionEnt` × 5 + `DiametricDimensionEnt` × 2 + `MLeaderEnt`（DN15）| 8 |

总实体 ≈ 50。

## 二、视图布局

```
 主视-剖视              侧视-外形
[x: -45..45]          [x: 175..225]
[y: -30..105]         [y: -30..30]
   原点(0,0)             原点(200,0)
```

视图间距 = 200 - 45 = 155 ≥ 阀体直径 50 + 100，符合 GB 视图布置规范。

## 三、生成步骤

```mermaid
flowchart LR
    L[ensureLayers]
    L --> M[buildMainView<br/>外形+内孔+Hatch+螺纹+阀杆+手轮+中心线]
    M --> S[buildSideView<br/>外圆+内孔+十字中心线]
    S --> D[buildDims<br/>5 Linear+2 Diametric+1 MLeader]
    D --> C[commit<br/>addEntities+regen+zoomExtents]
```

## 四、完整可运行代码

```js
import {
  Engine, PolylineEnt, BulgePoint, Point2D, CircleEnt, LineEnt,
  HatchEnt, Edge, Edges, EdgeType,
  LinearDimensionEnt, DiametricDimensionEnt, MLeaderEnt, MLeaderContentType,
  writeMessage
} from 'vjcad';

await cadView.onLoad();

drawValve();

function drawValve() {
  Engine.startUndoRecord('AUTO_DRAW_VALVE_DN15');
  try {
    ensureValveLayers();
    const ents = [];
    ents.push(...buildMainView());
    ents.push(...buildSideView({ ox: 200, oy: 0 }));
    ents.push(...buildDims());
    Engine.addEntities(ents);
    writeMessage(`截止阀生成完成，共 ${ents.length} 个实体`);
  } finally {
    Engine.endUndoRecord();
  }
  Engine.regen();
  Engine.zoomExtents();
}

function ensureValveLayers() {
  const defs = [
    { name: 'OUTLINE', color: 7, lineType: 'CONTINUOUS' },
    { name: 'HATCH',   color: 8, lineType: 'CONTINUOUS' },
    { name: 'THREAD',  color: 6, lineType: 'CONTINUOUS' },
    { name: 'CENTER',  color: 4, lineType: 'CENTER'     },
    { name: 'DIM',     color: 3, lineType: 'CONTINUOUS' }
  ];
  for (const d of defs) {
    if (!Engine.currentDoc.layers.itemByName(d.name)) {
      Engine.createLayer(d.name, d);
    }
  }
}
```

### 4.1 主视图（剖视）

```js
function buildMainView() {
  const out = [];
  Engine.setCurrentLayer('OUTLINE');
  Engine.CECOLOR = 256;
  Engine.CELTYPE = 'ByLayer';

  const bodyOuter = buildBodyOuter();
  const bodyInner = buildBodyInner();
  out.push(bodyOuter, bodyInner);

  out.push(...buildBonnetAndStem());
  out.push(...buildHandwheel({ cx: 0, cy: 90, r: 25 }));
  out.push(...buildSeatAndDisc());

  Engine.setCurrentLayer('CENTER');
  const cx = new LineEnt([-50, 0], [50, 0]);   cx.setDefaults();
  const cy = new LineEnt([0, -30], [0, 120]);  cy.setDefaults();
  out.push(cx, cy);

  Engine.setCurrentLayer('THREAD');
  out.push(...buildThreads({ x0: -32, x1: -20, yTop: 8, yBot: -8 }));
  out.push(...buildThreads({ x0:  20, x1:  32, yTop: 8, yBot: -8 }));

  Engine.setCurrentLayer('HATCH');
  out.push(...buildBodyHatches(bodyOuter, bodyInner));

  return out;
}

function buildBodyOuter() {
  const b = Math.tan(Math.PI / 8);   // 90° 圆角
  const pl = new PolylineEnt();
  pl.bulgePoints.add(new BulgePoint(new Point2D(-32, -11), 0));
  pl.bulgePoints.add(new BulgePoint(new Point2D(-20, -11), 0));
  pl.bulgePoints.add(new BulgePoint(new Point2D(-15, -25), b));
  pl.bulgePoints.add(new BulgePoint(new Point2D( 15, -25), 0));
  pl.bulgePoints.add(new BulgePoint(new Point2D( 20, -11), b));
  pl.bulgePoints.add(new BulgePoint(new Point2D( 32, -11), 0));
  pl.bulgePoints.add(new BulgePoint(new Point2D( 32,  11), 0));
  pl.bulgePoints.add(new BulgePoint(new Point2D( 20,  11), 0));
  pl.bulgePoints.add(new BulgePoint(new Point2D( 15,  25), b));    // 逆时针轮廓：外凸统一 +b
  pl.bulgePoints.add(new BulgePoint(new Point2D( -15, 25), 0));
  pl.bulgePoints.add(new BulgePoint(new Point2D(-20,  11), b));
  pl.bulgePoints.add(new BulgePoint(new Point2D(-32,  11), 0));
  pl.isClosed = true;
  pl.setDefaults();
  return pl;
}

function buildBodyInner() {
  const pl = new PolylineEnt();
  pl.setPoints([
    [-32, -7.5], [-12, -7.5], [-7, -16], [7, -16], [12, -7.5], [32, -7.5],
    [ 32,  7.5], [ 12,  7.5], [ 7,  16], [-7,  16], [-12,  7.5], [-32,  7.5]
  ]);
  pl.isClosed = true;
  pl.setDefaults();
  return pl;
}

function buildBonnetAndStem() {
  const out = [];
  const bonnet = new PolylineEnt();
  bonnet.setPoints([[-10, 25], [10, 25], [10, 40], [-10, 40]]);
  bonnet.isClosed = true;
  bonnet.setDefaults();

  const stem = new PolylineEnt();
  stem.setPoints([[-2, 18], [2, 18], [2, 65], [-2, 65]]);
  stem.isClosed = true;
  stem.setDefaults();

  out.push(bonnet, stem);
  return out;
}

function buildHandwheel({ cx, cy, r }) {
  const out = [];
  const rim = new CircleEnt([cx, cy], r);
  const hub = new CircleEnt([cx, cy], 5);
  rim.setDefaults();  hub.setDefaults();
  out.push(rim, hub);

  for (let i = 0; i < 4; i++) {
    const a = (i * Math.PI) / 2;
    const ln = new LineEnt(
      [cx + 5 * Math.cos(a), cy + 5 * Math.sin(a)],
      [cx + r * Math.cos(a), cy + r * Math.sin(a)]
    );
    ln.setDefaults();
    out.push(ln);
  }
  return out;
}

function buildSeatAndDisc() {
  const out = [];
  const seat = new PolylineEnt();
  seat.setPoints([[-7, -1], [7, -1], [7, 1], [-7, 1]]);
  seat.isClosed = true;
  seat.setDefaults();

  const disc = new PolylineEnt();
  disc.setPoints([[-5, 1], [5, 1], [3, 8], [-3, 8]]);
  disc.isClosed = true;
  disc.setDefaults();

  out.push(seat, disc);
  return out;
}

function buildThreads({ x0, x1, yTop, yBot }) {
  const out = [];
  const N = 5;
  const dx = (x1 - x0) / N;
  for (let i = 0; i <= N; i++) {
    const x = x0 + i * dx;
    const top = new LineEnt([x, yTop], [x + dx * 0.5, yTop - 1.5]);
    const bot = new LineEnt([x, yBot], [x + dx * 0.5, yBot + 1.5]);
    top.setDefaults();  bot.setDefaults();
    out.push(top, bot);
  }
  return out;
}

function buildBodyHatches(outer, inner) {
  const out = [];
  const wallTop = new PolylineEnt();
  wallTop.setPoints([[-32, 7.5], [32, 7.5], [32, 11], [-32, 11]]);
  wallTop.isClosed = true;
  wallTop.setDefaults();

  const wallBot = new PolylineEnt();
  wallBot.setPoints([[-32, -11], [32, -11], [32, -7.5], [-32, -7.5]]);
  wallBot.isClosed = true;
  wallBot.setDefaults();

  for (const wall of [wallTop, wallBot]) {
    const h = new HatchEnt();
    h.patternName = 'ANSI31';
    const e = new Edge();
    e.edgeType    = EdgeType.Polyline;
    e.bulgePoints = wall.bulgePoints.clone();
    const edges = new Edges();
    edges.add(e);
    h.setLoops(edges);
    h.setDefaults();
    out.push(wall, h);
  }
  return out;
}
```

### 4.2 侧视图

```js
function buildSideView({ ox, oy }) {
  const out = [];

  Engine.setCurrentLayer('OUTLINE');
  const outer = new CircleEnt([ox, oy], 25);    // 阀体外径
  const bore  = new CircleEnt([ox, oy], 7.5);   // DN15 内孔
  outer.setDefaults();  bore.setDefaults();
  out.push(outer, bore);

  Engine.setCurrentLayer('CENTER');
  const hLine = new LineEnt([ox - 32, oy], [ox + 32, oy]);
  const vLine = new LineEnt([ox, oy - 32], [ox, oy + 32]);
  hLine.setDefaults();  vLine.setDefaults();
  out.push(hLine, vLine);

  return out;
}
```

### 4.3 标注

```js
function buildDims() {
  Engine.setCurrentLayer('DIM');
  const out = [];

  const dimL = new LinearDimensionEnt(
    new Point2D(-32, -25), new Point2D(32, -25), new Point2D(0, -45), 0);
  dimL.setDefaults();
  out.push(dimL);

  const dimD = new LinearDimensionEnt(
    new Point2D(0, -25), new Point2D(0, 25), new Point2D(-45, 0), Math.PI / 2);
  dimD.setDefaults();
  out.push(dimD);

  const dimH = new LinearDimensionEnt(
    new Point2D(0, 0), new Point2D(0, 115), new Point2D(60, 60), Math.PI / 2);
  dimH.setDefaults();
  out.push(dimH);

  const dimT = new LinearDimensionEnt(
    new Point2D(-32, 11), new Point2D(-20, 11), new Point2D(-26, 30), 0);
  dimT.setDefaults();
  out.push(dimT);

  const dimW = new DiametricDimensionEnt(
    new Point2D(-25, 90), new Point2D(25, 90));
  dimW.setDefaults();
  out.push(dimW);

  const dimSideOuter = new DiametricDimensionEnt(
    new Point2D(200 - 25, 0), new Point2D(200 + 25, 0));
  dimSideOuter.setDefaults();
  out.push(dimSideOuter);

  const ml = new MLeaderEnt();
  ml.contentType  = MLeaderContentType.MText;
  ml.textContent  = 'G1/2" 内螺纹（DN15）';
  ml.textPosition = new Point2D(-70, 25);
  ml.textHeight   = 4;
  ml.addLeaderLine(new Point2D(-26, 8));
  ml.setDefaults();
  out.push(ml);

  return out;
}
```

## 五、关键技术点

### 5.1 剖切区 Hatch：单回路 vs 多回路

vjcad 已验证示例（[entity/hatch-solid](../../entity/hatch-solid.md)）只展示**单回路**：1 个 `Edge` 进 1 个 `Edges`。本例阀体壁本应是"外轮廓 + 内孔"双回路；为避开未验证特性，**拆成上 / 下两块矩形墙各自单回路**——视觉效果几乎一致，且 100% 复用已知 API。

```js
const e = new Edge();
e.edgeType    = EdgeType.Polyline;
e.bulgePoints = wall.bulgePoints.clone();   // ⚠️ 必须 clone
```

### 5.2 螺纹示意：循环 + 短斜线

DN15 内螺纹用"齿尖" 5~7 段平行短斜线表示。每条斜线长度 ≈ 螺距/2，斜率 ≈ 螺纹半角。关键是**单独建 `THREAD` 图层 / 颜色 6**，否则与外轮廓粘成一团。

```js
for (let i = 0; i <= N; i++) {
  const x = x0 + i * dx;
  out.push(new LineEnt([x, yTop], [x + dx * 0.5, yTop - 1.5]));
  out.push(new LineEnt([x, yBot], [x + dx * 0.5, yBot + 1.5]));
}
```

### 5.3 中心线：图层 lineType='CENTER'

```js
Engine.setCurrentLayer('CENTER');
const cx = new LineEnt([-50, 0], [50, 0]);
cx.setDefaults();      // ⬅ 自动吸 'CENTER' 层 → 应用 CENTER 线型
```

> 若图纸里 `CENTER` 线型显示成实线，是因为该 `.dwg` 模板没加载 CENTER 线型；解决方案见 [property/color-linetype](../../property/color-linetype.md)。

### 5.4 视图间偏移：绝对坐标 vs 变换平移

| 方式 | 适用 |
|---|---|
| **绝对坐标分块**（本例采用） | 视图坐标已知；最简，调试时便于直接量 |
| **`entity.move([0,0],[dx,dy])` 批平移** | 视图先在原点构造，最后整组挪到目标位置；适合复用同样的"构造函数" |

本例侧视图的 `buildSideView({ ox, oy })` 形参化使两种风格都能切换：传 `{ ox: 200, oy: 0 }` 是绝对坐标；想改用变换平移就把 `ox/oy = 0` 然后对返回的实体数组循环 `e.move([0,0],[200,0])`。

### 5.5 弧 / 圆角的 bulge 符号

**bulge 正负 = 弧的旋转方向**（>0 逆时针，<0 顺时针，详见 [entity/polyline](../../entity/polyline.md) 第二节），换成几何直觉：**凸度 > 0 时弧向行进方向右侧鼓出**（`ArcEnt.initForPlineArc` / `HatchEnt` 离散化同此约定）。外轮廓 `buildBodyOuter` 按**逆时针**遍历（下边左→右、上边右→左），行进方向右侧恰好是轮廓外侧，所以要整体外凸就**各段统一用 `+b`**；上半用 `-b` 会让弧向内凹。顺时针遍历的轮廓则相反，统一用 `-b`。

## 六、标注布局规则

| 标注 | 位置 | step |
|---|---|---|
| 总长 65 | 阀体下方 y=-45 | 主基线 |
| 阀体高 50 | 阀体左侧 x=-45 | 主基线 |
| 总高 115 | 整图右侧 x=60 | 与阀体高错开 ≥ 30 mm |
| 螺纹长 12 | 阀体上方 y=30，靠左 | 局部尺寸单独偏移 |
| 手轮 Φ50 | 手轮平面线 | 直径标注 |
| 侧视外径 Φ50 | 侧视图中线 | 直径标注 |
| DN15 引线 | x=-70 | MLeader 单独引出 |

> 字高默认 5；多条平行标注 `step ≥ 字高 × 2 = 10` 防止文字与尺寸线重叠。

## 七、红旗 / 反例

1. **Hatch 边界用了"外轮廓 PolylineEnt"自身 `bulgePoints`，没 `clone()`**：阀体被错误打剖；源 polyline 与 hatch 边界共享后修改 polyline 直接影响 hatch。
2. **螺纹画在 OUTLINE 层**：与阀体外轮廓粘连，分不清；必须 `setCurrentLayer('THREAD')`。
3. **侧视图忘了偏移 / 写在原点**：直接盖住主视图。**先确认 `ox` 大于主视图右边界**（本例主视右边 32，故 ox=200 远超）。
4. **bulge 符号按"上半/下半"分正负**：`buildBodyOuter` 是逆时针轮廓，上半写成 `-b` 会让上边的弧向 X 轴中心**凹**，上下不对称、像被捏了一下的"沙漏"——逆时针轮廓要外凸就全用 `+b`（凸度 > 0 = 向行进方向右侧鼓出 = 逆时针轮廓的外侧）。
5. **Φ 标注用 `LinearDimensionEnt`**：得到的是数值（弦长），不是 Φ 前缀。**直径必须 `DiametricDimensionEnt(p1, p2)`**，p1/p2 是直径两端点。
6. **手轮辐条 i 从 0 开始 + 间隔 π/2**：得到 4 条十字辐条，OK；若 N 改 6 要把 `(i * Math.PI) / 2` 改成 `(2 * Math.PI * i) / N`。
7. **`MLeaderEnt` 漏 `addLeaderLine`**：只有文字飘在画面上，没箭头指到螺纹。

## 八、扩展挑战（本例不实现，留给读者）

| 想增 | 提示 |
|---|---|
| 表面粗糙度符号 ▽ | 用 `BlockDefinition` 定义符号块，`InsertEnt` 复用（[entity/block-insert](../../entity/block-insert.md)） |
| 几何公差框 ⌖ | 块 + 块属性（attribute），`vjcad/example/entity/block/04attribute.js` |
| 俯视图 | 与侧视图同模板；按法兰孔做圆形阵列（详 [autodraw/overview](../overview.md) 第五节） |
| 比例缩放 1:2 / 1:5 | 整体 `scale([0,0], 0.5)` 后再标注；标注样式 `scaleFactor` 对应放大 |

## 参考（官方文档 MCP）

- 通用骨架：[autodraw/overview](../overview.md)
- 实体：[entity/polyline](../../entity/polyline.md)、[entity/line-arc-circle](../../entity/line-arc-circle.md)、[entity/hatch-solid](../../entity/hatch-solid.md)、[entity/dimension-mleader](../../entity/dimension-mleader.md)、[entity/block-insert](../../entity/block-insert.md)
- 图层 / 属性：[layer/create-switch](../../layer/create-switch.md)、[property/color-linetype](../../property/color-linetype.md)、[property/set-defaults](../../property/set-defaults.md)
- 编辑：[edit/move-copy](../../edit/move-copy.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/hatch-ent.md`、`vjcad/doc/command/dimension-commands.md`、`vjcad/doc/entity/index.md`、`vjcad/doc/property/index.md`、`vjcad/doc/layer/layers.md`
- 示例：`vjcad/example/entity/all/01entities.js`、`vjcad/example/entity/polyline/02rectangle.js`、`vjcad/example/entity/hatch/01create.js`、`vjcad/example/entity/hatch/03boundary.js`、`vjcad/example/entity/dimension/01linear.js`、`vjcad/example/entity/dimension/06diametric.js`、`vjcad/example/entity/mleader/01create.js`、`vjcad/example/property/03linetype.js`、`vjcad/example/layer/01create.js`、`vjcad/example/comprehensive/01section.js`、`vjcad/example/undo/03group.js`
