# 关联标注（最常用反应器场景）

> 示例：`vjcad/example/reactor/02dimension.js`
> 文档：`vjcad/doc/reactor/index.md`、`vjcad/doc/command/dimension-commands.md`

vjcad 标注实体（`LinearDimensionEnt`、`AlignedDimensionEnt`、`RadialDimensionEnt` 等）已经内置了 `IEntityReactor`。**直接调用 `dim.setSourceEntities(...)` / `dim.setSourceEntity(...)` 就能让标注随源实体自动更新**，无需自己实现接口。方法名按标注类型不同：

| 标注类 | 关联方法 |
|---|---|
| `LinearDimensionEnt` / `AlignedDimensionEnt` | `setSourceEntities(startId, startSegIdx, endId, endSegIdx, startPointType?, endPointType?, startT?, endT?)` |
| `TwoLineAngularDimensionEnt` / `AngleDimensionEnt` | `setSourceEntities(line1Id, line1SegIdx, line2Id, line2SegIdx)` |
| `ThreePointAngularDimensionEnt` | `setSourceEntities(e1Id, e1SegIdx, p1Type, e2Id, e2SegIdx, p2Type, centerId?, centerType?, t1?, t2?, tc?)` |
| `RadialDimensionEnt` / `DiametricDimensionEnt` | `setSourceEntity(circleId, pointAngle?)`（单数） |
| `ArcDimensionEnt` | `setSourceEntity(arcId)`（单数） |
| `OrdinateDimensionEnt` | `setSourceEntity(entityId, pointType?)`（单数） |

## 1. setSourceEntities 签名

```js
dim.setSourceEntities(
    startLineId, startSegIdx,    // 起点关联实体 ID + 线段下标（LINE 忽略下标，传 -1 即可；PLINE 必须 ≥0）
    endLineId,   endSegIdx,
    startPointType, endPointType, // 'start' | 'end' | 'midpoint' | undefined（类型里有 'center' 但未实现，回退为起点）
    startParameterT, endParameterT // 可选：0~1 曲线参数（更精细的位置）
);
```

> 注意：必须 **先 `Engine.addEntities(dim)`**，再调用 `setSourceEntities`。否则标注还没拿到 docId，注册不到反应器表。
>
> **只支持 `LINE` 与 `PLINE` 的直线段**作为 Linear/Aligned 的源：`PLINE` 按 `segIdx` 取 `getSubEnts()[segIdx]`，该段须是直线（`segIdx=-1` 或弧段 → 取不到点，标注不更新）；ARC/CIRCLE 等其他类型同样取不到点。

## 2. 标准用法：水平线性标注关联到一段直线

```js
const line1 = new LineEnt([0, 0], [100, 0]);
line1.setDefaults();
Engine.addEntities(line1);

const dim = new LinearDimensionEnt(
    new Point2D(0, 0),    // 测量起点
    new Point2D(100, 0),  // 测量终点
    new Point2D(50, 25),  // 标注线位置点
    0                     // 标注角度（弧度，0=水平）
);
dim.setDefaults();
Engine.addEntities(dim);

dim.setSourceEntities(
    line1.id, -1,          // 起点关联整条 line1
    line1.id, -1,
    'start',               // 起点取 line1 的起点
    'end'                  // 终点取 line1 的终点
);
```

之后 `line1.move(...)` / 修改端点，`dim` 会在下一次 `Engine.regen()` 时重新测量（见下方说明）。

> ⚠️ 联动链路是 `owner.setModified()`（只置 `_modified` 旗标）→ **`Engine.regen()`**（bucket 脏扫描时派发 `EntityModified`）→ 反应器桥 `notifyOwnerChanged` → `dim.setReactorDirty()` → `updateDirtyReactors()`/`buildNestEnts` 时 `updateFromOwners()` 重算测量值。**只改属性不 `regen` 不会联动**；`Engine.updateEntities`/`Engine.pcanvas.updateEntities` 均不存在，勿用。也可手动 `ReactorNotifier.notifyModified(entity)` 直接驱动。

## 3. 对齐标注关联斜线

```js
const line2 = new LineEnt([0, -60], [80, -100]);
line2.setDefaults(); Engine.addEntities(line2);

const aligned = new AlignedDimensionEnt(
    new Point2D(0, -60),
    new Point2D(80, -100),
    new Point2D(50, -65)
);
aligned.setDefaults(); Engine.addEntities(aligned);
aligned.setSourceEntities(line2.id, -1, line2.id, -1, 'start', 'end');
```

`AlignedDimensionEnt` 自动按两端点连线方向出标注。

## 4. 标注跨越两条线段（端点 → 端点）

```js
const line3 = new LineEnt([150, 0], [200, 40]);
const line4 = new LineEnt([200, 40], [260, 0]);
[line3, line4].forEach(l => { l.setDefaults(); Engine.addEntities(l); });

const span = new LinearDimensionEnt(
    new Point2D(150, 0),
    new Point2D(260, 0),
    new Point2D(205, -25),
    0
);
span.setDefaults(); Engine.addEntities(span);

span.setSourceEntities(
    line3.id, -1,    // 起点 → line3 的 start
    line4.id, -1,    // 终点 → line4 的 end
    'start',
    'end'
);
```

## 5. 用 parameterT 精确定位（线段中段）

`pointType` 拿不到的位置（例如线段 1/4 处），用 `parameterT`（0~1）：

```js
const line5 = new LineEnt([0, -150], [120, -150]);
line5.setDefaults(); Engine.addEntities(line5);

const partial = new LinearDimensionEnt(
    new Point2D(30,  -150),  // 1/4 处
    new Point2D(90,  -150),  // 3/4 处
    new Point2D(60,  -130),
    0
);
partial.setDefaults(); Engine.addEntities(partial);

partial.setSourceEntities(
    line5.id, -1,
    line5.id, -1,
    undefined, undefined,    // 不使用 pointType
    0.25,                    // startParameterT
    0.75                     // endParameterT
);
```

> 完整 4 个示例都在 `vjcad/example/reactor/02dimension.js`。

## 6. 检查关联状态

```js
console.log(dim.isAssociative); // 仅表示 _ownerRefs 非空（调过 setSourceEntities 且 id≥0），不校验源实体存在/有效
dim.getSourceEntityRefs();      // 查看关联引用

// 读取联动后的测量值：惰性更新，先通知再 regen，然后读公开方法 getMeasurement()
line1.endPoint = new Point2D(150, 0);
ReactorNotifier.notifyModified(line1);   // 让 dim 立即标脏
Engine.regen();                          // regen 开头 updateDirtyReactors() → updateFromOwners()
console.log(dim.getMeasurement());       // 150
```

## 红旗

- **顺序很重要**：先 `Engine.addEntities(dim)`，再 `dim.setSourceEntities(...)`
- ⚠️ **纠正**：`Engine.eraseEntities(owner)` **首次删除并不通知反应器**——`EntityErased` 单数事件从不派发（源码 0 个 emit 点），`ReactorNotifier.notifyErased` 只挂在 ERASE 撤销记录的 `pushRedo` 里，即只有 **undo 后再 redo** 才触发 `unlinkAllOwners`。正常 erase 后 `dim.isAssociative` 仍为 `true`、`_ownerRefs` 残留死引用——需要清理时自己 `dim.unlinkAllOwners()` 或在 `EntitiesErased`（复数，正常派发）事件里手动通知
- 修改源实体后标注是**惰性同步**更新（不是异步）：`Engine.regen()` 开头的 `updateDirtyReactors()` 才调 `updateFromOwners()`；而 `EntityModified` 在 regen 的 bucket 扫描里才派发，晚于这一步——所以要么先 `ReactorNotifier.notifyModified(owner)` 再 `Engine.regen()`，要么 regen 两次，之后读公开的 `dim.getMeasurement()`
- `setSourceEntities` 接收的是 **number 类型的 entity.id**（d.ts：`line1Id: number`），不要传 `entity` 对象本身
- `pointType: 'midpoint'` 在多段线上是 `segIdx` 那一段的中点，单线段上等价于 `parameterT: 0.5`；`parameterT` 优先于 `pointType`；`'center'` 未实现（落到 default 分支取起点）

## 参考（官方文档 MCP）

- 示例：`vjcad/example/reactor/02dimension.js`
- 文档：`vjcad/doc/reactor/index.md`、`vjcad/doc/command/dimension-commands.md`
