# 表格识别（extractTables）

> 文档：`vjcad/doc/recognition/table-extract.md`
> 示例：`vjcad/example/recognition/01extracttable.js`

## 1. 端到端最小例子

下例先用代码画一张 3×3 的小表，再调 `extractTables`：

```js
class TableDrawingExtraction {
    async main() {
        await this.draw3x3Table(0, 0);
        await vjcad.delay(500);   // ⚠️ delay 是顶层导出函数，不是 Engine.delay
        const result = extractTables({ debug: true });
        if (result.error) return message.error(result.error);
        if (!result.tables.length) return message.warn("未识别到表格");

        result.tables.forEach((t, i) => {
            const lines = [
                `表格 ${i + 1}：${t.rowCount} 行 × ${t.colCount} 列`,
                ...t.datas.map(row =>
                    "| " + row.map(c => (c || "").padEnd(8, " ")).join(" | ") + " |"
                )
            ];
            console.log(lines.join("\n"));
        });
    }

    async draw3x3Table(ox, oy) {
        const cw = 50, rh = 25;
        // 画 4 条横线 + 4 条竖线
        for (let i = 0; i <= 3; i++) {
            const ln = new LineEnt([ox, oy + i * rh], [ox + 3 * cw, oy + i * rh]);
            ln.setDefaults(); Engine.addEntities(ln);
        }
        for (let j = 0; j <= 3; j++) {
            const ln = new LineEnt([ox + j * cw, oy], [ox + j * cw, oy + 3 * rh]);
            ln.setDefaults(); Engine.addEntities(ln);
        }
        // 写文字（坐标取单元中心）
        const data = [["编号", "名称", "数量"], ["1", "螺栓", "10"], ["2", "螺母", "20"]];
        for (let r = 0; r < 3; r++) for (let c = 0; c < 3; c++) {
            // TextEnt ctor: (insertionPoint, textString, height, ...)
            const t = new TextEnt([ox + c * cw + cw / 2, oy + (2 - r) * rh + rh / 2], data[r][c], 8);
            t.setDefaults(); Engine.addEntities(t);
        }
    }
}
```

> 完整代码：`vjcad/example/recognition/01extracttable.js`

## 2. TableData 结构详解

```ts
interface TableData {
    rect: string;              // 两对角点逗号串 "x1,y1,x2,y2"（WCS，无方括号，不保证 min/max 顺序）
    rowCount: number;
    colCount: number;
    rows: string[];            // 行分隔线 y 坐标，数字字符串数组（自上而下）
    cols: string[];            // 列分隔线 x 坐标，数字字符串数组
    spans: Record<string, string>; // 合并单元格（已是对象）"row_col"（1 起算）-> "rowSpan,colSpan"
    datas: string[][];         // 二维数组 [row][col]，元素是单元格**文本字符串**
    attr: TableAttr;           // 识别统计信息
}

interface TableAttr {
    cellEmptyRatio: number;     // 空单元百分比（0–100）
    tableTextCount: number;     // 表内文字总数
    tableCellMaxCount: number;  // 最大单元数
    unLinkLineRatio: number;    // 未关联线比例
}
```

> `datas[r][c]` 按 **行优先 / 顶部为 row=0** 排列，元素是字符串（空单元为 `""`），不是带 `.text`/`.rect` 的对象。

## 3. 范围与图层限定

```js
// 拿到当前可识别图层名
const layers = getTableExtractLayers();
console.log(layers);

// 只识别"表格"图层 + 指定矩形
extractTables({
    bounds: { minX: 0, minY: 0, maxX: 1000, maxY: 800 },
    layers: ['表格'],
});
```

`bounds` 是 `{ minX, minY, maxX, maxY }` 对象（`Bounds`），不是数组。让用户拉框用 `Engine.getPoint` + `Engine.getCorner`（顶层 `getCorner` 也可）：

```js
const p1 = await Engine.getPoint(new PointInputOptions('指定第一角点:'));
if (p1.status !== InputStatusEnum.OK) return;
const p2 = await Engine.getCorner(new CornerInputOptions('指定对角点:', p1.value));
if (p2.status !== InputStatusEnum.OK) return;
extractTables({
    bounds: {
        minX: Math.min(p1.value.x, p2.value.x),
        minY: Math.min(p1.value.y, p2.value.y),
        maxX: Math.max(p1.value.x, p2.value.x),
        maxY: Math.max(p1.value.y, p2.value.y),
    },
});
```

## 4. 调优建议

| 症状 | 解决 |
|---|---|
| 找不到表格 | `tol` 调大；或 `includeBlocks: true` 展开图块 |
| 单元被错切 | `tableEdgeMinPoint` 调大，过滤短线段 |
| 表格被分裂成多个 | 用 `bounds` 限定区域；或检查闭合误差 |
| 文字找不到 | 看是否 `MText` —— 默认 `includeMText: true`，但被关掉时要打开 |
| 中文乱码 | 缺字体；先确认 `MainView({ fonts })` 已加载对应字体 |
| 算得太慢 | 加 `bounds` + `layers`，避免全图扫描 |

`debug: true` 会输出每一步候选数量，是调优最有效的开关。

## 5. 完整选项类型

```ts
interface TableExtractOptions {
    bounds?:   Bounds;                // { minX, minY, maxX, maxY }，不传 = 全图
    layers?:   string[];              // 限定参与图层（大小写不敏感）
    includeLine?:     boolean;        // 计入 LINE（默认计）
    includePolyline?: boolean;        // 计入 PLINE/POLYLINE（默认计）
    includeText?:     boolean;        // 计入 TEXT（默认计）
    includeMText?:    boolean;        // 计入 MTEXT（默认计）
    includeBlocks?:   boolean;        // 展开 INSERT 内实体（默认展开）
    includeGroups?:   boolean;        // 展开 GROUP 内实体（默认展开）
    digit?: number;                   // 几何比较精度（默认 4）
    tol?:   number;                   // 端点对齐容差，默认 0 = 引擎自适应
    tableEdgeMinPoint?: number;       // 表格边框最少顶点数（默认 12）
    tableTextMinCount?: number;       // 表内最少文字数（默认 4）
    cellEmptyRatio?:    number;       // 允许的最大空单元百分比 0–100（默认 90），超过则整表丢弃
    debug?: boolean;                  // 控制台输出中间过程
}
```

## 红旗

- 返回前请检查 `result.error`；如果有错，`result.tables` 为空
- 大图（>10万实体）必须传 `bounds + layers`，否则容易卡顿
- "表格里有图片 / 印章" 时，**禁用** `includeBlocks` 避免把图章内文字也识别进去
- `cellEmptyRatio` 是百分比（默认 90）：空单元占比超过它时**整张表被丢弃**（不是丢空单元）；稀疏表识别不出时调大，切勿填 `0.5` 这类 0~1 小数（几乎所有表都会被丢弃）
- 后续步骤想反向写回（修改单元文字）：`datas[r][c]` 只有文本，要定位原文字实体需结合 `rect`（`split(',').map(Number)`）/ `rows` / `cols`（`.map(Number)`）的坐标用 `getEntities` 按范围查找——这些字段不是 JSON 串，不能 `JSON.parse`；按范围查找时加 `e.isAlive` 过滤
- `extractTables` 与 `getEntities` 都包含已 erase 的实体（`isAlive=false`），识别结果可能含已删内容

## 参考（官方文档 MCP）

- 文档：`vjcad/doc/recognition/table-extract.md`
- 示例：`vjcad/example/recognition/01extracttable.js`
