# 综合实战 — 工程剖面图（钻孔数据自动出图）

> 文档：`vjcad/doc/comprehensive/index.md`
> 示例：`vjcad/example/comprehensive/01section.js`

这是 vjcad 在 **真实业务（工程地质 / 勘察）** 中最完整的端到端示例：根据钻孔数据，自动绘制带图框 / 标尺 / 图例 / 标题栏的剖面图。它把 **图层 / 实体 / Hatch / Block / Insert / 文字 / 标尺 / 表格** 等组合在一起，是评估 vjcad 能力上限的好样板。

## 1. 业务流程

```mermaid
flowchart TB
    A[钻孔数据 mockBoreholeData] --> B[注册岩性 PAT 图案<br/>patternManager.registerPattern]
    B --> C[计算图框尺寸 / 标尺范围]
    C --> D[创建孔口标签 Block<br/>doc.blocks.add + addEntity]
    D --> E[绘制柱状图<br/>PolylineEnt + HatchEnt + InsertEnt]
    E --> F[绘制连线 / 距离标注]
    F --> G[绘制图框 + 标题 + 图例]
    G --> H[绘制标题栏表格]
    H --> I[Engine.addEntities + zoomExtents]
```

## 2. 关键技术点 1：注册 PAT 图案

```js
function parsePatternString(patString) {
    const lines  = patString.trim().split('\n');
    const header = lines[0].match(/^\*([^,]+),(.*)$/);
    const name = header[1].trim();
    const desc = header[2].trim();
    const patternLines = lines.slice(1).map(l => {
        const p = l.split(',').map(parseFloat);
        return {
            angle: p[0], xOrigin: p[1], yOrigin: p[2],
            deltaX: p[3], deltaY: p[4], dashes: p.slice(5)
        };
    });
    return { name, description: desc, lines: patternLines };
}

for (const [name, patString] of Object.entries(ROCK_PATTERNS)) {
    Engine.patternManager.registerPattern(parsePatternString(patString), 'rock');
}
```

要点：

- **PAT 文本格式**：`*Name,Desc` 行 + 若干"角度,xOrigin,yOrigin,dx,dy,dashes…"行
- 第二参数 `'rock'` 是 **分类**（用于 `PatternPickerDialog` 分组）
- 注册后 `HatchEnt.patternName = 'XX'` 即可使用

## 3. 关键技术点 2：用 Block 做"重复模板"

```js
function createBoreholeBlock() {
    const doc = Engine.currentDoc;
    let blockDef = doc.blocks.itemByName('BoreholeLabel');
    if (blockDef) return blockDef;

    blockDef = doc.blocks.add('BoreholeLabel');
    blockDef.basePoint = [0, 0];

    const attrName = new TextEnt([0, 5], '孔', 30, 0, TextAlignmentEnum.Center);
    attrName.tag = 'NAME';   attrName.prompt = '钻孔编号';   attrName.setDefaults();
    const sep = new LineEnt([-30, 0], [30, 0]); sep.setDefaults();
    const attrElev = new TextEnt([0, -5], '0', 25, 0, TextAlignmentEnum.TopCenter);
    attrElev.tag = 'ELEV';   attrElev.prompt = '孔口高程';  attrElev.setDefaults();

    blockDef.addEntity(attrName);
    blockDef.addEntity(sep);
    blockDef.addEntity(attrElev);
    return blockDef;
}
```

要点：

- `doc.blocks.itemByName / .add` 维护 BlockTable
- 块里包含 **属性文字**（带 `tag`），后续 `InsertEnt` 时用 `addAttribute` 填值
- `BlockEntities` 的 `setDefaults()` **不可省**

```js
const label = new InsertEnt();
label.blockId        = boreholeBlock.blockId;
label.insertionPoint = [x + 15, y + 50];
label.scaleFactor    = 1;
label.rotation       = 0;
label.setDefaults();
label.addAttribute({ tag: 'NAME', textString: bh.name });
label.addAttribute({ tag: 'ELEV', textString: bh.y.toString() });
entities.push(label);
```

## 4. 关键技术点 3：HatchEnt 的边界与图案

```js
function createRectBoundary(x, y, w, h) {
    const b = new PolylineEnt();
    b.setPoints([[x, y], [x + w, y], [x + w, y + h], [x, y + h]]);
    b.isClosed = true; b.setDefaults();
    return b;
}

function createEdgesFromPolyline(polyline) {
    const edges = new Edges();
    const e = new Edge();
    e.edgeType = EdgeType.Polyline;
    e.bulgePoints = polyline.bulgePoints.clone();
    edges.add(e);
    return edges;
}

const hatch = new HatchEnt();
hatch.patternName  = stratum.name;
hatch.patternScale = Math.max(1, stratum.thickness / 5);
hatch.setLoops(createEdgesFromPolyline(boundary));
hatch.setDefaults();
hatch.color = ROCK_COLORS[stratum.name] || 7;
entities.push(hatch);
```

要点：

- **边界要先建**（PolylineEnt + setDefaults）才能 clone 它的 bulgePoints
- `HatchEnt.setLoops(edges)` 接受 **`Edges`**（多边界用多个 `Edge`）
- `patternScale` 与 `patternName` 决定纹理疏密；过小会变成黑块
- `Edges` / `Edge` / `EdgeType` 必须从 vjcad 导入

## 5. 关键技术点 4：标尺 / 图框 / 标题栏

```js
// 左标尺（垂直）
const leftScale = new LineEnt([sx, sy], [sx, sy + drawHeight]);
leftScale.setDefaults(); leftScale.color = 7;
entities.push(leftScale);

for (let elev = scaleMinY; elev <= scaleMaxY; elev += interval) {
    const drawY = sy + scaleMaxY - elev;
    entities.push(makeLine([sx, drawY], [sx - tickLen, drawY]));
    entities.push(makeText([sx - tickLen - 10, drawY], elev.toString(),
                           textHeight, TextAlignmentEnum.MidRight));
}

// 图框
const frame = new PolylineEnt();
frame.setPoints([[fxMin, fyMin], [fxMax, fyMin], [fxMax, fyMax], [fxMin, fyMax]]);
frame.isClosed = true; frame.setDefaults();
entities.push(frame);

// 标题栏单元格（双层 for 循环 + createRectBoundary + 文字）
for (let row = 0; row < titleRows.length; row++) {
    for (const cell of titleRows[row]) {
        const cellX = titleX + cell.col * tbCellW;
        const w = tbCellW * (cell.colSpan ?? 1);
        entities.push(createRectBoundary(cellX, rowY - tbCellH, w, tbCellH));
        if (cell.text) {
            const t = cell.text.includes('\n') ? new MTextEnt() : new TextEnt();
            t.insertionPoint = [cellX + w / 2, rowY - tbCellH / 2];
            t.text   = cell.text;
            t.height = cell.fontSize ?? 16;
            if (t instanceof MTextEnt) t.textAttachment = 5;
            else                         t.textAlignment = TextAlignmentEnum.MidCenter;
            t.setDefaults();
            entities.push(t);
        }
    }
}
```

要点：

- **TextEnt vs MTextEnt** 选择：含换行用 MText（`textAttachment = 5` 中心对齐）
- 标题栏用 `colSpan` 实现合并单元格
- 整张图最后一次性 `Engine.addEntities(entities)`：收益是合并为**一条撤销记录**（一次 Ctrl+Z 撤掉整张图）；`EntityAdding/EntityAdded` 仍按实体逐个触发

## 6. 心智模型

```mermaid
flowchart TB
    Data[钻孔 / 岩性数据] --> Setup[注册图案 / 创建块]
    Setup --> Layout[计算图框 / 标尺]
    Layout --> Draw[批量构造 entities[]]
    Draw --> Final[Engine.addEntities + zoomExtents]
```

把"准备 → 布局 → 构造数组 → 一次性入库"分四步走，是 vjcad **批量出图**的标准模板。

## 7. 适合改造的方向

| 改造方向 | 提示 |
|---|---|
| 数据来源换成后端 / Excel | 替换 `mockBoreholeData()`，保留布局逻辑 |
| 多剖面分页 | 把 `drawSectionDiagram()` 包进 for 循环，每页 `entities` 独立、再统一加 |
| 增加自定义岩性 | 新增 PAT 字符串 + ROCK_COLORS 映射 |
| 变成可交互编辑 | 把 `entities.push(...)` 后跟 `setReactor` / 注册命令对柱状图段拖动 |
| 出图打印 | 出图后调用命令 `EXPORTPNG` / `EXPORTDWG`（`Engine.editor.executerWithOp`；无 `Engine.exportPng` 函数，也没有 PDF 导出命令） |

## 红旗

- 不调用 `setDefaults()` 的实体**仍会正常加入**，只是停留在构造默认值（0 层 / `color=256` ByLayer / `lineType='ByLayer'`），不吸当前图层与 `CECOLOR/CELTYPE`
- `HatchEnt` 回路由 `setLoops()` 自动闭合（`Edge` 没有 `isClosed`，按首尾相接处理），只需把边界点 `clone()` 进 `Edge`；边界多段线的 `isClosed` 只影响轮廓线本身
- `TextAlignmentEnum.MidCenter` 与 `MTextEnt.textAttachment = 5` 是两套独立枚举（`TextAlignmentEnum` / `MTextAttachmentEnum`），但编号按同一 3×3 小键盘布局（7=左上、5=正中，不是 DXF 的 1=左上）
- 一次 `addEntities([])` 巨大数组（上万实体）时会卡 UI；可以分批 `Engine.addEntities(batch)` + `await new Promise(r => setTimeout(r, 0))`
- 图框 + 图例 + 标题栏的位置全部依赖**最终图纸尺寸**，必须先算 `frameMinX/Max`、`drawHeight` 再绘
- `patternScale` 不当时 hatch 会变成"全黑"或"空白"；调试时把 `patternScale` 调到 100 看下边界是否对就能定位

## 参考（官方文档 MCP）

- 文档：`vjcad/doc/comprehensive/index.md`
- 示例：`vjcad/example/comprehensive/01section.js`
