> **本文回答**：壁挂式通风过滤装置 CAD 三视图（主视、左视、俯视）按用户给定**绝对坐标**绘制；展示自动成图通用骨架在大型工程图中的伸缩性——同样是 [autodraw/overview](../overview.md) 五步走，只是图层多 2 个、实体多 130+、标注链长一些。重点示范：① 视图间距用**绝对坐标分块**；② 隐藏 / 中实 / 中心 / 粗实四种线型混排；③ 标注层叠的 **3 列分布**避免堆成一墙；④ `MTextEnt` 多行文字 + 视图名 / 风机细节 / dimstyle 三件套让"几何图"升级到"可交付图"（详第八节）。

## 一、需求拆解

> 通风过滤装置 CAD 三视图绘制指令（含完整标注与开口）  
> 单位：mm，绘图坐标比例 1:1  
> 线型：外轮廓粗实线，开口/百叶中实线，中心线点划线，隐藏结构虚线  
> 视图排布：三视图间距 ≥1500 mm，完全分离不重叠

| 部件 | 主视 | 左视 | 俯视 | 图层 |
|---|---|---|---|---|
| 箱体外框 | ✓ | ✓ | ✓ | `OUTLINE` |
| 滤芯口（百叶 + 法兰） | 3 个正面 | 3 个左侧 | 各面投影 | `OPENING` |
| 顶部出风口 Φ600/Φ700 | ✓ | – | ✓ | `OPENING` |
| 风机外形 + 内部细节（电机 + 4 辐条 + 4 角加强筋 + "风机"标签） | ✓ | – | – | `OUTLINE / TEXT` |
| 风机底座投影 | – | – | ✓ | `OPENING` |
| 背部支架 | – | ✓（虚线） | – | `HIDDEN` |
| 底面进风口 | – | ✓（虚线） | ✓（虚线） | `HIDDEN` |
| 中心线 | ✓ | ✓ | ✓ | `CENTER` |
| 标注 | ~12 | ~6 | ~8 | `DIM` |
| 视图标签（主视图 / 左视图 / 俯视图  1:100） | ✓ | ✓ | ✓ | `TEXT` |
| 技术说明（MText） + "比例 1:100" 题块 | – | – | 右下角 | `TEXT` |

总实体 ≈ 130。

## 二、视图坐标布局

```
俯视 [0,4000]──[2000,5200]
         │
         │ 间距 1500
         │
主视 [0,0]────[2000,3450]            左视 [4000,0]──[5200,2500]    技术说明 [6200,2400]→[9200,~1230]
         └─────── 间距 2000 ───────────┘                            └─ width 3000 / textAttachment=7 ─┘
                                                                    比例题块 [6200,800]→[9200,1100]
```

- 主视右边界 `X=2000` → 左视左边界 `X=4000`，水平间距 **2000 mm（≥1500 ✓）**
- 主视上边界 `Y=3450` → 俯视下边界 `Y=4000`（用风机区算到 `Y=2500` 时间距 1500，含风机时仍 ≥ 550，本例选 `Y=4000` 即从主视上边距 550 到俯视下边）
- 左视右边界 `X=5200` → 技术说明 MText 左边界 `X=6200`，水平间距 **1000 mm**（避免 MText 锚点漂移盖到左视图）
- 注：用户给的 "三视图间距 ≥ 1500" 指**箱体投影间距**——主视箱体顶 2500 → 俯视底 4000 = 1500 ✓

## 三、生成步骤

```mermaid
flowchart LR
    L["1.ensureLayers<br/>OUTLINE/OPENING/HIDDEN<br/>CENTER/DIM/TEXT"]
    L --> D["2.setupDimStyle<br/>DIMSCALE 50 → 1:100 可读"]
    D --> F["3.主视 buildFrontView<br/>(箱体+3 百叶+顶部出风口<br/>+风机[电机+支架+'风机'标签])"]
    F --> S["4.左视 buildLeftView<br/>(箱体+3 百叶+背支架+底进风口虚线)"]
    S --> T["5.俯视 buildTopView<br/>(投影外框+各面投影+顶出风口+风机底座)"]
    T --> V["6.buildViewLabels<br/>主视图 / 左视图 / 俯视图"]
    V --> N["7.技术说明 buildTitleNote<br/>(MText 高 150+'比例 1:100' 题块)"]
    N --> C["8.commit<br/>addEntities+regen+zoomExtents"]
```

## 四、完整可运行代码

### 4.1 顶层 + 通用工具

```js
import {
  Engine, PolylineEnt, LineEnt, CircleEnt, TextEnt, MTextEnt,
  LinearDimensionEnt, DiametricDimensionEnt, Point2D,
  writeMessage
} from 'vjcad';

await cadView.onLoad();

drawVentilation();

function drawVentilation() {
  Engine.startUndoRecord('VENTILATION_3_VIEWS');
  try {
    ensureVentLayers();
    setupDimStyle();                       // 让默认标注在 1:100 出图时仍可读
    const ents = [];
    ents.push(...buildFrontView());
    ents.push(...buildLeftView());
    ents.push(...buildTopView());
    ents.push(...buildViewLabels());       // 主视图 / 左视图 / 俯视图
    ents.push(...buildTitleNote());
    Engine.addEntities(ents);
    writeMessage(`通风装置三视图共 ${ents.length} 个实体`);
  } finally {
    Engine.endUndoRecord();
  }
  Engine.regen();
  Engine.zoomExtents();
}

function setupDimStyle() {
  // 代码 new 出来的标注默认用全局 DefaultDimensionStyle 的副本，改文档样式对象（dimensionStyles.itemByName）不生效；
  // 文档比例走 DIMSCALE，每个标注再调 applyCurrentDocStyle() 吸取（见 hDim / vDim）。
  Engine.currentDoc.DIMSCALE = 50;   // 整体放大 50× → 1:100 出图时字高 ≈ 2.5 mm/纸面
}

function ensureVentLayers() {
  const defs = [
    { name: 'OUTLINE', color: 7, lineType: 'CONTINUOUS' },
    { name: 'OPENING', color: 3, lineType: 'CONTINUOUS' },
    { name: 'HIDDEN',  color: 2, lineType: 'HIDDEN'     },
    { name: 'CENTER',  color: 4, lineType: 'CENTER'     },
    { name: 'DIM',     color: 1, lineType: 'CONTINUOUS' },
    { name: 'TEXT',    color: 7, lineType: 'CONTINUOUS' }
  ];
  for (const d of defs) {
    if (!Engine.currentDoc.layers.itemByName(d.name)) {
      Engine.createLayer(d.name, d);
    }
  }
}

function rectClosed(p1, p2) {
  const pl = new PolylineEnt();
  pl.setPoints([[p1[0], p1[1]], [p2[0], p1[1]], [p2[0], p2[1]], [p1[0], p2[1]]]);
  pl.isClosed = true;
  pl.setDefaults();
  return pl;
}

function crossDiagonals(p1, p2) {
  const a = new LineEnt(p1, p2);
  const b = new LineEnt([p1[0], p2[1]], [p2[0], p1[1]]);
  a.setDefaults(); b.setDefaults();
  return [a, b];
}

function centerCross(cx, cy, halfH, halfV) {
  const a = new LineEnt([cx - halfH, cy], [cx + halfH, cy]);
  const b = new LineEnt([cx, cy - halfV], [cx, cy + halfV]);
  a.setDefaults(); b.setDefaults();
  return [a, b];
}

function hDim(p1, p2, textPosY) {
  const d = new LinearDimensionEnt(
    new Point2D(p1[0], p1[1]), new Point2D(p2[0], p2[1]),
    new Point2D((p1[0] + p2[0]) / 2, textPosY), 0);
  d.setDefaults();
  d.applyCurrentDocStyle();   // 吸取文档当前标注样式，scaleFactor 取自 DIMSCALE
  return d;
}

function vDim(p1, p2, textPosX) {
  const d = new LinearDimensionEnt(
    new Point2D(p1[0], p1[1]), new Point2D(p2[0], p2[1]),
    new Point2D(textPosX, (p1[1] + p2[1]) / 2), Math.PI / 2);
  d.setDefaults();
  d.applyCurrentDocStyle();
  return d;
}
```

### 4.2 主视图

```js
function buildFrontView() {
  const ents = [];

  Engine.setCurrentLayer('OUTLINE');
  Engine.CECOLOR = 256;
  Engine.CELTYPE = 'ByLayer';
  ents.push(rectClosed([0, 0], [2000, 2500]));      // 箱体
  ents.push(...buildFan({ x0: 500, y0: 2500, x1: 1500, y1: 3450 }));

  Engine.setCurrentLayer('OPENING');
  const frontOpens = [
    [[664, 192],  [1336, 864]],
    [[664, 914],  [1336, 1586]],
    [[664, 1636], [1336, 2308]]
  ];
  for (const [p1, p2] of frontOpens) {
    ents.push(rectClosed(p1, p2));
    ents.push(...crossDiagonals(p1, p2));
  }

  const ventOuter = new CircleEnt([1000, 2500], 350);
  const ventInner = new CircleEnt([1000, 2500], 300);
  ventOuter.setDefaults();  ventInner.setDefaults();
  ents.push(ventOuter, ventInner);

  Engine.setCurrentLayer('CENTER');
  ents.push(...centerCross(1000, 2500, 400, 400));

  ents.push(...buildFrontDims());
  return ents;
}

function buildFan({ x0, y0, x1, y1 }) {
  const out = [];
  Engine.setCurrentLayer('OUTLINE');
  out.push(rectClosed([x0, y0], [x1, y1]));

  const cx = (x0 + x1) / 2, cy = (y0 + y1) / 2;
  const motor = new CircleEnt([cx, cy], 200);
  const hub   = new CircleEnt([cx, cy], 60);
  motor.setDefaults();  hub.setDefaults();
  out.push(motor, hub);

  for (let i = 0; i < 4; i++) {
    const a = Math.PI / 4 + i * Math.PI / 2;
    const ln = new LineEnt(
      [cx + 60 * Math.cos(a),  cy + 60 * Math.sin(a)],
      [cx + 200 * Math.cos(a), cy + 200 * Math.sin(a)]
    );
    ln.setDefaults();
    out.push(ln);
  }

  const corners = [
    [[x0, y0], [x0 + 80, y0 + 80]],
    [[x1, y0], [x1 - 80, y0 + 80]],
    [[x0, y1], [x0 + 80, y1 - 80]],
    [[x1, y1], [x1 - 80, y1 - 80]]
  ];
  for (const [a, b] of corners) {
    const ln = new LineEnt(a, b);
    ln.setDefaults();
    out.push(ln);
  }

  Engine.setCurrentLayer('TEXT');
  const label = new TextEnt();
  label.insertionPoint = [x0 + 220, cy - 75];
  label.text   = '风机';
  label.height = 150;
  label.styleName = 'simkai';
  label.setDefaults();
  out.push(label);

  Engine.setCurrentLayer('OUTLINE');
  return out;
}

function buildFrontDims() {
  Engine.setCurrentLayer('DIM');
  const out = [];

  out.push(hDim([0, 0], [2000, 0], -120));            // 箱体宽 2000
  out.push(hDim([500, 3450], [1500, 3450], 3580));    // 风机宽 1000
  out.push(hDim([664, 0], [1336, 0], -240));          // 百叶宽 672
  out.push(hDim([0, 0], [664, 0], -360));             // 左侧空 664
  out.push(hDim([1336, 0], [2000, 0], -360));         // 右侧空 664

  const cInner = 2150, cMid = 2350, cOuter = 2600;
  out.push(vDim([2000, 0],    [2000, 192],  cInner));   // 风口下空 192
  out.push(vDim([2000, 864],  [2000, 914],  cInner));   // 间距 50
  out.push(vDim([2000, 1586], [2000, 1636], cInner));   // 间距 50
  out.push(vDim([2000, 2308], [2000, 2500], cInner));   // 风口上空 192

  out.push(vDim([2000, 192],  [2000, 864],  cMid));     // 第 1 风口 672
  out.push(vDim([2000, 914],  [2000, 1586], cMid));     // 第 2 风口 672
  out.push(vDim([2000, 1636], [2000, 2308], cMid));     // 第 3 风口 672

  out.push(vDim([2000, 0],    [2000, 2500], cOuter));         // 箱体高 2500
  out.push(vDim([2000, 0],    [2000, 3450], cOuter + 200));   // 总高 3450
  out.push(vDim([1500, 2500], [1500, 3450], 1700));           // 风机高 950

  const dimD600 = new DiametricDimensionEnt(
    new Point2D(700, 2500), new Point2D(1300, 2500));
  const dimD700 = new DiametricDimensionEnt(
    new Point2D(650, 2500), new Point2D(1350, 2500));
  dimD600.setDefaults();  dimD700.setDefaults();
  dimD600.applyCurrentDocStyle();  dimD700.applyCurrentDocStyle();
  out.push(dimD600, dimD700);

  return out;
}
```

### 4.3 左视图

```js
function buildLeftView() {
  const ents = [];
  const ox = 4000;

  Engine.setCurrentLayer('OUTLINE');
  ents.push(rectClosed([ox, 0], [ox + 1200, 2500]));

  Engine.setCurrentLayer('OPENING');
  const leftOpens = [
    [[ox + 80, 192],  [ox + 752, 864]],
    [[ox + 80, 914],  [ox + 752, 1586]],
    [[ox + 80, 1636], [ox + 752, 2308]]
  ];
  for (const [p1, p2] of leftOpens) {
    ents.push(rectClosed(p1, p2));
    ents.push(...crossDiagonals(p1, p2));
  }

  Engine.setCurrentLayer('HIDDEN');
  ents.push(rectClosed([ox + 1200, 1150], [ox + 1700, 1350])); // 背支架 500×200
  ents.push(rectClosed([ox + 80, -50],   [ox + 752,  0]));     // 底面进风口
  ents.push(...crossDiagonals([ox + 80, -50], [ox + 752, 0]));

  ents.push(...buildLeftDims(ox));
  return ents;
}

function buildLeftDims(ox) {
  Engine.setCurrentLayer('DIM');
  const out = [];

  out.push(hDim([ox, 0], [ox + 1200, 0], -120));            // 深 1200
  out.push(hDim([ox + 80, 0], [ox + 752, 0], -240));        // 百叶 672

  const cx = ox + 1200 + 200;
  out.push(vDim([ox + 1200, 192],  [ox + 1200, 864],  cx));
  out.push(vDim([ox + 1200, 914],  [ox + 1200, 1586], cx));
  out.push(vDim([ox + 1200, 1636], [ox + 1200, 2308], cx));
  out.push(vDim([ox + 1200, 0],    [ox + 1200, 2500], cx + 200));

  return out;
}
```

### 4.4 俯视图

```js
function buildTopView() {
  const ents = [];
  const oy = 4000;

  Engine.setCurrentLayer('OUTLINE');
  ents.push(rectClosed([0, oy], [2000, oy + 1200]));

  const ventOuter = new CircleEnt([1000, oy + 600], 350);
  const ventInner = new CircleEnt([1000, oy + 600], 300);
  ventOuter.setDefaults();  ventInner.setDefaults();
  ents.push(ventOuter, ventInner);

  Engine.setCurrentLayer('CENTER');
  ents.push(...centerCross(1000, oy + 600, 400, 400));

  Engine.setCurrentLayer('OPENING');
  ents.push(rectClosed([500, oy + 200], [1500, oy + 1000]));   // 风机底座

  const frontProj = [[664, oy], [1336, oy + 672]];
  ents.push(rectClosed(frontProj[0], frontProj[1]));

  const leftProj  = [[0, oy + 208], [672, oy + 880]];
  ents.push(rectClosed(leftProj[0], leftProj[1]));

  const rightProj = [[1328, oy + 208], [2000, oy + 880]];
  ents.push(rectClosed(rightProj[0], rightProj[1]));

  Engine.setCurrentLayer('HIDDEN');
  const bottomHidden = [[664, oy + 672], [1336, oy + 1344]];   // 用户给值
  ents.push(rectClosed(bottomHidden[0], bottomHidden[1]));
  ents.push(...crossDiagonals(bottomHidden[0], bottomHidden[1]));

  ents.push(...buildTopDims(oy));
  return ents;
}

function buildTopDims(oy) {
  Engine.setCurrentLayer('DIM');
  const out = [];

  out.push(hDim([0, oy + 1200], [2000, oy + 1200], oy + 1320));   // 宽 2000
  out.push(hDim([0, oy], [2000, oy], oy - 120));                  // 宽 2000 复用一条
  out.push(vDim([0, oy], [0, oy + 1200], -120));                  // 深 1200

  const dimVent = new DiametricDimensionEnt(
    new Point2D(700, oy + 600), new Point2D(1300, oy + 600));
  dimVent.setDefaults();
  dimVent.applyCurrentDocStyle();
  out.push(dimVent);

  out.push(hDim([664, oy], [1336, oy], oy - 240));                // 正面投影宽 672
  out.push(vDim([0, oy + 208], [0, oy + 880], -240));             // 左 / 右投影深 672
  out.push(vDim([2000, oy + 208], [2000, oy + 880], 2120));

  return out;
}
```

### 4.5 视图标签（在三视图下方加 "主视图 / 左视图 / 俯视图"）

```js
function buildViewLabels() {
  Engine.setCurrentLayer('TEXT');
  const out = [];

  const labels = [
    { text: '主视图  1:100', insertion: [600,  -350], textAlignment: 7 },
    { text: '左视图  1:100', insertion: [4200, -350], textAlignment: 7 },
    { text: '俯视图  1:100', insertion: [600,  5400], textAlignment: 1 }
  ];
  for (const { text, insertion,textAlignment } of labels) {
    const t = new TextEnt();
    t.insertionPoint = insertion;
    t.text = text;
    t.textAlignment = textAlignment;
    t.height = 200;
    t.styleName = 'simkai';
    t.setDefaults();
    out.push(t);
  }
  return out;
}
```

> 视图名称用**单行 `TextEnt`**（高 200 mm），放在每个视图下方留白处；中文字体 `simkai` 必须先在 `MainView({ fonts })` 注册（详 [setup/fonts](../../setup/fonts.md)）。

### 4.6 技术说明（MText）+ 比例题块

```js
function buildTitleNote() {
  Engine.setCurrentLayer('TEXT');
  const out = [];

  const noteText =
    '壁挂式通风过滤装置，设计风量 10000m³/h，过滤等级 G4。\n' +
    '箱体 2000×1200×2500mm，1.5mm 镀锌钢板喷塑。\n' +
    '前、左、右立面各 3 个滤芯口，底面 1 个进风口，配固定百叶窗。\n' +
    '顶部 DN600 出风口，风机柔性软接。\n' +
    '前方检修空间 ≥800mm，两侧及顶部空间 ≥500mm，墙体承重 ≥350kg。\n' +
    '符合 GB/T 14295、GB 50243 标准。';

  const mt = new MTextEnt();
  mt.insertionPoint  = [6200, 2400];   // ⬅ 由 5800 挪到 6200：再加 400 mm 安全间距
  mt.textAttachment  = 7;              // ⬅ 7 = TopLeft（小键盘布局）：插入点 = 文本框左上角，向右下生长
  mt.text   = noteText;
  mt.height = 150;                     // 1:100 出图后纸面 1.5 mm 仍可读
  mt.maxWidth = 3000;                  // MTextEnt 无 width 字段；maxWidth=文本框最大宽度（refWidth 等价别名）
  mt.styleName = 'simkai';
  mt.setDefaults();
  out.push(mt);

  const scaleBox = new PolylineEnt();
  scaleBox.setPoints([[6200, 800], [9200, 800], [9200, 1100], [6200, 1100]]);
  scaleBox.isClosed = true;
  scaleBox.setDefaults();
  out.push(scaleBox);

  const scale = new TextEnt();
  scale.insertionPoint = [6500, 880];
  scale.text   = '比例  1 : 100';
  scale.height = 200;                  // 由 80 提到 200：明显成"题块"
  scale.styleName = 'simkai';
  scale.setDefaults();
  out.push(scale);

  return out;
}
```

> **关键**：`MTextEnt.textAttachment` 默认就是 `7`（`MTextAttachmentEnum.TopLeft`，插入点 = 文本框左上角，文字向右下生长）。vjcad 附着点按**小键盘布局**编号（7/8/9 = 上排，1/2/3 = 下排），**不同于 DXF 的 1=TopLeft**——写成 `1` 是 BottomLeft，文字会以插入点为左下角向上生长。这里仍**显式赋值**——一是代码可读（读者不用猜锚点），二是防御未来默认值变更。若误设成 `5`（MidCenter）会以插入点为中心向四周溢出，盖到临近视图。详见 [entity/text-mtext](../../entity/text-mtext.md) 第三节。

## 五、关键技术点

### 5.1 视图分块：绝对坐标 vs 偏移变换

用户给了所有视图的**绝对坐标**——本例直接用绝对坐标分块（每个 `buildXxxView` 内部所有点都是绝对值），最简单、便于调试 / 量距。

若同样的"通风装置"要画 1:5 缩小版，把所有坐标乘 0.2 即可；或先在原点构造，最后整组用 `entity.move([0,0],[ox,oy])` 平移（详 [edit/move-copy](../../edit/move-copy.md)）。

### 5.2 数据驱动 vs 硬编码

3 个百叶用 `frontOpens` 数组 + `for` 循环替代手写 3 次：

```js
const frontOpens = [
  [[664, 192],  [1336, 864]],
  [[664, 914],  [1336, 1586]],
  [[664, 1636], [1336, 2308]]
];
for (const [p1, p2] of frontOpens) {
  ents.push(rectClosed(p1, p2));
  ents.push(...crossDiagonals(p1, p2));
}
```

好处：① 改间距只动数据，不动代码；② 与"5 个 / 7 个百叶"扩展只改数组；③ 代码更短易审。

### 5.3 标注分列（避免链尺寸堆成一堵墙）

主视图右侧 7 段竖向尺寸如果都堆同一 `textPosX`，渲染后会变成"一堵密不透风的红墙"，看不清单条尺寸。本例改成**3 列错开**：

```
                  cInner    cMid     cOuter
y=2500 ─┬──────── ┐         ┬───── ┬─── ┐
        │   192   │         │ 2500 │3450│
y=2308 ─┴┐        │         │      │    │
         │   672  │ ←第3口  │      │    │
y=1636 ──┘        │         │      │    │
y=1586 ─┬─        │         │      │    │
        │    50   │ ←间距   │      │    │
y=1636 ─┴─        │         │      │    │
        │   672   │ ←第2口  │      │    │
y=914  ──         │         │      │    │
...
```

| 列 | textPosX | 谁去 |
|---|---|---|
| `cInner=2150`（内）| 4 段小缝（上下空 192 / 中间间距 50） |
| `cMid=2350`（中）| 3 段风口主尺寸 672 |
| `cOuter=2600 / 2800`（外） | 箱高 2500、总高 3450 |

`step ≥ 字高 × 2` 防文字重叠（详 [autodraw/overview](../overview.md) 第四节 4.5）。

### 5.4 隐藏 / 中实 / 中心 / 粗实 四线型

线型靠**图层** + `setDefaults()` 自动应用：

| 线型 | 图层 | `lineType` 字段 | 颜色 |
|---|---|---|---|
| 粗实 | OUTLINE | `CONTINUOUS` | 7（白 / 黑） |
| 中实 | OPENING | `CONTINUOUS` | 3（绿） |
| 虚线 | HIDDEN | `HIDDEN` | 2（黄） |
| 点划 | CENTER | `CENTER` | 4（青） |

**前提**：项目模板里已加载这些线型；vjcad 默认带 `CONTINUOUS / HIDDEN / CENTER`，自定义线型见 [property/color-linetype](../../property/color-linetype.md)。

> "粗实 / 中实"在 vjcad 里用**图层 ACI 颜色 + ACI 线宽**体现；想要严格的 lineWeight 区分（如打印时 0.7 / 0.35 mm），在创建后设图层字段：`const ly = Engine.createLayer('OUTLINE', { color: 7 }); ly.lineWeight = 70;`——`createLayer` 的 options 只认 `layerOn / color / lineType / isFrozen / isLocked`，传 `lineWeight` 会被静默丢弃。

### 5.5 中文字体 MText / TextEnt

`MTextEnt.styleName = 'simkai'` 必须配合 `MainView({ fonts: [{ name: 'simkai', path: '...' }] })` 在初始化时注册（详 [setup/fonts](../../setup/fonts.md)、[entity/text-mtext](../../entity/text-mtext.md)）。否则中文显示成方框 / 缺字。

字高与出图比例匹配（**1:100 出图，几何字高建议 ≥ 100 mm**，对应纸面 1 mm 起）：

| 用途 | 实体 | 几何字高 | 1:100 纸面 |
|---|---|---|---|
| 标题 / 视图名 | `TextEnt` | 200 mm | 2 mm |
| 比例题块 | `TextEnt` | 200 mm | 2 mm |
| 部件标签（风机） | `TextEnt` | 150 mm | 1.5 mm |
| 多行技术说明 | `MTextEnt` | 150 mm | 1.5 mm |
| 标注文字 | dimstyle `textHeight × scaleFactor` | 2.5×50=125 mm | 见 8.1 |

`MTextEnt.maxWidth = 3000` 决定换行宽度（属性名是 `maxWidth`，不是 `width`；`curWidth` 为只读实际宽度）；多行总高 ≈ `height × 行数 × 1.2 ~ 1.5`，6 行 × 150 ≈ 1170 mm。

**`textAttachment` 建议显式设**：默认虽已是 `7`（TopLeft），但多行说明文字块建议写死，避免复制来的代码段带其它锚点值，见红旗 #14：

```js
mt.textAttachment = 7;   // 小键盘布局：7=TopLeft / 8=TopCenter / 9=TopRight / 4=MidLeft / 5=MidCenter / 6=MidRight / 1=BottomLeft / 2=BottomCenter / 3=BottomRight
```

### 5.6 `buildFan` / `buildViewLabels` / `setupDimStyle` 三个"成品图"补丁

为把"几何图"升级到"可交付图"，本例增加三个独立小函数（详第八节）：

| 函数 | 解决 |
|---|---|
| `buildFan` | 风机不再是空白矩形，加电机 / 4 辐条 / 4 角加强筋 / "风机"标签 |
| `buildViewLabels` | 三个视图下方加"主视图 / 左视图 / 俯视图  1:100" `TextEnt` |
| `setupDimStyle` | 设 `Engine.currentDoc.DIMSCALE = 50`，配合每个标注 `applyCurrentDocStyle()`，让 1:100 出图后字高 ≈ 2.5 mm/纸面 |

### 5.7 `crossDiagonals` 的 4 点顺序

每个百叶画两条交叉对角线 = `LineEnt(p1, p2)` + `LineEnt([p1.x, p2.y], [p2.x, p1.y])`——两条线在矩形对角点对调 y 即可。这种"两点矩形对角对调"是图层 OPENING 的常见模式。

## 六、标注布局规则

主视右侧的纵向链尺寸**分三列**而非堆一列，避免文字 / 尺寸线打架：

| 列 | textPosX | 用途 | 数量 |
|---|---|---|---|
| 内层（gap 列） | `cInner = 2150` | 上下空 192 + 间距 50 | 4 |
| 中层（开口列） | `cMid = 2350`   | 3 个 672 风口 | 3 |
| 外层（总尺寸列） | `cOuter = 2600 / 2800` | 箱体高 2500、总高 3450 | 2 |

横向 / 三视图整体规则：

| 视图 | 横向标注 textPosY | 纵向标注 textPosX | step |
|---|---|---|---|
| 主视 | 上方 `y=3580`、下方 `y=-120 / -240 / -360` | `cInner / cMid / cOuter` 三列 | 120 |
| 左视 | 下方 `y=-120 / -240` | 右侧 `x=ox+1400`、`x=ox+1600` | 120 |
| 俯视 | 上方 `y=oy+1320`、下方 `y=oy-120 / oy-240` | 左侧 `x=-120 / -240`、右侧 `x=2120` | 120 |

> 字高默认 50（`DIMSCALE = 50` + 每个标注 `applyCurrentDocStyle()`，让 1:100 出图后纸面字高 ≈ 2.5 mm）；`step = 120 ≈ 字高 × 2.4` 防止文字与下一条尺寸线压叠。

## 七、红旗 / 反例

1. **左视图忘了加 `ox = 4000`** → 整组左视图盖在主视图上，几何相互压死；先 `console.log` 几个左视图实体的 `bbox` 看看是否在 `X >= 4000`。
2. **百叶画在 OUTLINE 层** → 与箱体外框同色 / 同线型，看不出"开口"语义；必须 `setCurrentLayer('OPENING')` 后再画百叶 + 对角线。
3. **HIDDEN 线显示成实线** → 模板没加载 `HIDDEN` 线型；修：`Engine.linetypeManager.registerLinetype(def)` 或 `loadFromLinDefinitions(lin文本)` 加载（小写 t，详 [trouble/source-conflicts](../../trouble/source-conflicts.md) §7；无 `.add()`）（详 [property/color-linetype](../../property/color-linetype.md)），或先用 `Engine.regen(true)` 强刷一次。
4. **`MTextEnt` 没设 `styleName='simkai'`** → 中文渲染成方框；先在 `MainView({ fonts: [...] })` 注册中文字体。
5. **`textPosX / textPosY` 写错让标注盖在几何上** → 调试时把 `textPos` 加 100 mm 移开看清，再回填精确值。
6. **直径标注 `DiametricDimensionEnt(中心, 圆周点)`** → vjcad 的直径标注**不是圆心+半径**而是**直径两端点**；本例外圆 Φ700：`new Point2D(650, 2500)` / `new Point2D(1350, 2500)`，差 700 = Φ700。
7. **`crossDiagonals` 写成 `LineEnt(p1, p2)` + `LineEnt(p1, p2)`**：复制粘贴漏改 → 两条线重合，看不到 X 形；第二条要把 y 对调：`[p1.x, p2.y]` ↔ `[p2.x, p1.y]`。
8. **`for (let i = 0; i < frontOpens.length; i++)` 内手动 `setCurrentLayer` 重复切层**：性能略差且容易写错；**进入循环前切一次层**就好。
9. **底面进风口 Y 范围 `[oy+672, oy+1344]` 超出箱体 Y 范围 `[oy, oy+1200]`**：用户原文如此，本例如实绘制；项目里真实交付前需要再核对一次。
10. **链尺寸全堆同一 `textPosX`**：渲染后视觉上挤成"一堵墙"，文字相互压叠；本例用 **3 列 `cInner / cMid / cOuter`** 错开（详第六节）。
11. **MText `height = 60` + 1:100 出图**：纸面字高 0.6 mm，几乎看不见；本例改用 `height = 150`（纸面 1.5 mm）；视图标签 `TextEnt.height = 200`（纸面 2 mm）。
12. **风机就一个空白矩形**：浪费 950 mm 高的占位；至少加电机圆 + 4 条安装支架短斜线 + "风机"文字标签（详 `buildFan` 函数）。
13. **没视图名 / 没比例题块**：图纸缺信息无法交付；本例增加 `buildViewLabels` 三个 TextEnt，比例标用单独的 `PolylineEnt` 闭合矩形 + 高 200 的 `TextEnt`。
14. **MText 误用居中锚点（`textAttachment = 5` 等）** → 文字以插入点为**中心**向四周溢出，宽文本框会横向跨到临近视图上（默认 `7` TopLeft 无此问题；误写 DXF 习惯的 `1` 则是 BottomLeft，文字向上长）。**修法**：`mt.textAttachment = 7` 显式写死 + 插入点放在"左视图右边界 + 1000 mm"以右，双保险。

## 八、提升出图质量的若干技巧

> 这一节回答"为什么默认渲染看起来一般，怎么改"——也是把本示例从"几何对了"升级到"成品图能交付"的关键。

### 8.1 标注样式 `dimstyle`

vjcad 默认 `DimensionStyle.textHeight = 2.5` 是按 AutoCAD 习惯（纸面单位）。**1:1 大几何 + 1:100 出图**时纸面字高变成 0.025 mm，等于隐形：

```js
function setupDimStyle() {
  Engine.currentDoc.DIMSCALE = 50;   // 文档标注比例
}
// 每个代码创建的标注：
dim.setDefaults();
dim.applyCurrentDocStyle();          // 克隆文档当前样式，并把 scaleFactor 覆盖为 DIMSCALE
// 或逐个设快捷属性：dim.scaleFactor = 50 / dim.textHeight / dim.arrowSize
```

> ⚠️ **只改 `dimensionStyles.itemByName('Standard').scaleFactor` 无效**：标注构造时 `_style = DefaultDimensionStyle.clone()`（全局默认样式，不是文档样式），`setDefaults()` 也不吸文档样式；`applyCurrentDocStyle()` 会吸文档样式但随即把 `scaleFactor` 覆盖成 `doc.DIMSCALE`。内置标注命令就是 `setDefaults(); applyCurrentDocStyle();` 两步。
>
> `DimensionStyle` 是驼峰字段名（`scaleFactor / textHeight / arrowSize / extensionLineOffset` 等）；`dimscale / dimtxt` 这类小写名只出现在 `DbDimensionStyle` 序列化层，不能直接对样式对象赋值（详 [entity/dimension-mleader](../../entity/dimension-mleader.md) 第四节"标注样式"）。

### 8.2 视图名标签

每个视图下方放一行 `TextEnt`，高度 ≥ 字高 × 1.5，至少 200 mm：

```js
const t = new TextEnt();
t.insertionPoint = [600, -350];
t.text   = '主视图  1:100';
t.height = 200;
t.styleName = 'simkai';
t.setDefaults();
```

视图名下还可加图框线（`PolylineEnt` 闭合矩形）做"题块"。

### 8.3 风机 / 风口等"占位部件"加细节

不要画一个空白矩形当风机——观图者会怀疑是图层背景。至少加：

| 元素 | 实体 | 数量 |
|---|---|---|
| 电机外圆 | `CircleEnt` | 1 |
| 电机轴轮毂 | `CircleEnt` | 1 |
| 4 条电机支架（45° 辐条） | `LineEnt` | 4 |
| 4 个角加强筋（角内 80×80 短线） | `LineEnt` | 4 |
| "风机" 文字标签 | `TextEnt`（高 150） | 1 |

```js
const motor = new CircleEnt([cx, cy], 200);
const hub   = new CircleEnt([cx, cy], 60);
for (let i = 0; i < 4; i++) {
  const a = Math.PI / 4 + i * Math.PI / 2;
  out.push(new LineEnt(
    [cx + 60 * Math.cos(a),  cy + 60 * Math.sin(a)],
    [cx + 200 * Math.cos(a), cy + 200 * Math.sin(a)]));
}
```

### 8.4 比例题块单独画 + MText 锚点

不要把"比例 1:100"塞在 MText 末行——观图者扫不到。给比例单独画一个题块：

```js
const scaleBox = new PolylineEnt();
scaleBox.setPoints([[6200, 800], [9200, 800], [9200, 1100], [6200, 1100]]);
scaleBox.isClosed = true;
scaleBox.setDefaults();

const scale = new TextEnt();
scale.insertionPoint = [6500, 880];
scale.text   = '比例  1 : 100';
scale.height = 200;
scale.setDefaults();
```

**MText 一定要设 `textAttachment`**：

```js
const mt = new MTextEnt();
mt.insertionPoint  = [6200, 2400];
mt.textAttachment  = 7;             // 小键盘布局：7=TopLeft / 8=TopCenter / 9=TopRight / 4~6=Mid* / 1=BottomLeft ... 3=BottomRight
mt.text   = '...';
mt.maxWidth = 3000;                 // 无 width 字段；maxWidth=文本框最大宽度
mt.height = 150;
mt.setDefaults();
```

| 锚点值 | 含义 | 插入点 = | 文本生长方向 |
|---|---|---|---|
| `7` | TopLeft（默认） | 文本框左上角 | 向右下 ✓ 推荐 |
| `8` | TopCenter | 顶边中心 | 向左右 / 向下 |
| `9` | TopRight | 文本框右上角 | 向左下 |
| `5` | MidCenter | 文本框中心 | 向四周溢出 |
| `1` | BottomLeft | 文本框左下角 | 向右上 |

**vjcad 默认值已是 `7`（TopLeft）**：显式赋值是防御性写法；注意 vjcad 按小键盘布局编号（`MTextAttachmentEnum`，与 `TextEnt.textAlignment` 同一布局），不是 DXF 的 1=TopLeft；真正危险的是误用 `5`（MidCenter）等居中锚点——文字会以插入点为中心向四周溢出，把临近视图盖住。见红旗 #14（详 [entity/text-mtext](../../entity/text-mtext.md) 第三节）。

### 8.5 隐藏 / 中心 / 中实四线型在小屏 / 缩略图里很接近

为防"颜色相近、线型微差"在缩略图下糊成一团，**靠图层颜色拉开主次**：

| 用途 | 推荐 ACI | 理由 |
|---|---|---|
| OUTLINE | 7（主题色） | 最显眼 |
| OPENING | 3（亮绿） | 中度 |
| HIDDEN | 2（黄） | 在深背景中清晰 |
| CENTER | 4（青） | 与黄不撞 |
| DIM | 1（红） | 与几何拉开 |

详 [property/color-linetype](../../property/color-linetype.md)。

### 8.6 用 `bbox` 自检视图相互不压叠

```js
import { mergeBoundingBoxes } from 'vjcad';

function bboxOf(ents) {
  const boxes = ents.map(e => e.boundingBox()).filter(b => b && (b.maxX - b.minX) > 0 && (b.maxY - b.minY) > 0);
  // 注意：boundingBox() 返回 BoundingBox 实例（pt1/pt2/width/height/center/minPoint/maxPoint），也有 minX..maxY getter
  return mergeBoundingBoxes(boxes);
}
const bFront = bboxOf(buildFrontView());
const bLeft  = bboxOf(buildLeftView());
console.assert(bFront.maxX < bLeft.minX, '主视、左视水平不分离');
```

详 [geometry/point-bounds](../../geometry/point-bounds.md)。

### 8.7 关键尺寸 / 直径用 `MLeaderEnt` 引出

零散 `Φ` 标注混在 LinearDimension 阵列里很难一眼定位；改用 `MLeaderEnt` 把"DN15 / DN600"引到图外空白区：

```js
const ml = new MLeaderEnt();
ml.contentType  = MLeaderContentType.MText;
ml.textContent  = 'DN600';
ml.textPosition = new Point2D(2000, 2700);
ml.textHeight   = 120;
ml.addLeaderLine(new Point2D(1000, 2500));
ml.setDefaults();
```

详 [entity/dimension-mleader](../../entity/dimension-mleader.md) 第三节。

## 九、扩展挑战（本例不实现，留给读者）

| 想增 | 提示 |
|---|---|
| 图框 + 标题栏 | 用 `BlockDefinition` 定义 A1 图框块，`InsertEnt` 引入（详 [entity/block-insert](../../entity/block-insert.md)） |
| 比例 1:100 真缩放 | 几何画 1:1，`scale([0,0], 0.01)` 缩到纸上，标注用 `scaleFactor = 100` 反算（详 [edit/rotate-scale](../../edit/rotate-scale.md)） |
| 视图标记 / 剖切线 | 在主视上加 A-A 剖切箭头（`SolidEnt` 三角箭头 + `LineEnt` 剖切线 + `TextEnt` "A-A"） |
| 不同墙厚 hatch | 在隐藏 / OPENING 矩形内加 `HatchEnt + ANSI31`（详 [entity/hatch-solid](../../entity/hatch-solid.md)） |
| DWG 导出 | 见 [file/dwg-import-export](../../file/dwg-import-export.md) |

## 参考（官方文档 MCP）

- 通用骨架：[autodraw/overview](../overview.md)
- 实体：[entity/polyline](../../entity/polyline.md)、[entity/line-arc-circle](../../entity/line-arc-circle.md)、[entity/text-mtext](../../entity/text-mtext.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/move-copy](../../edit/move-copy.md)、[edit/rotate-scale](../../edit/rotate-scale.md)
- 撤销：[undo/group](../../undo/group.md)
- 视图：[view/zoom](../../view/zoom.md)
- 字体 / 初始化：[setup/fonts](../../setup/fonts.md)、[setup/install-and-mcp](../../setup/install-and-mcp.md)
- 文档：`vjcad/doc/entity/polyline-ent.md`、`vjcad/doc/entity/circle-ent.md`、`vjcad/doc/entity/mtext-ent.md`、`vjcad/doc/command/dimension-commands.md`、`vjcad/doc/property/index.md`、`vjcad/doc/layer/layers.md`、`vjcad/doc/guide/quick-start.md`
- 示例：`vjcad/example/entity/all/01entities.js`、`vjcad/example/entity/polyline/02rectangle.js`、`vjcad/example/entity/circle/01create.js`、`vjcad/example/entity/text/02multiline.js`、`vjcad/example/entity/text/04fonts.js`、`vjcad/example/entity/dimension/01linear.js`、`vjcad/example/entity/dimension/06diametric.js`、`vjcad/example/property/03linetype.js`、`vjcad/example/property/06setdefaults.js`、`vjcad/example/layer/01create.js`、`vjcad/example/undo/03group.js`、`vjcad/example/comprehensive/01section.js`
