# 用反应器实现几何约束

> 示例：`vjcad/example/reactor/04constraint.js`

vjcad 没有内置 SolidWorks 那种参数化求解器，但是利用事件型反应器可以实现 **端点连接、距离恒定、平行、共心** 这类轻量约束，足够大部分项目场景。

## 1. 端点连接约束（"图形粘合"）

> 当用户拖动 line1 的某个端点时，line2 的对应端点自动跟过去。

数据结构：

```js
this.constraints = [
    { line1Id, point1: 'start'|'end', line2Id, point2: 'start'|'end' },
];
```

事件回调（**关键：递归保护 `this.updating`**）：

```js
class EndpointConstraintReactor {
    constructor() {
        this.constraints = [];
        this.updating = false;
    }
    install() {
        Engine.eventManager.on(CadEvents.EntityModified, args => {
            if (this.updating || args.entity.type !== 'LINE') return;
            this.updating = true;
            try { this.apply(args.entity); }
            finally { this.updating = false; }
        });
    }
    addConstraint(line1Id, point1, line2Id, point2) {
        this.constraints.push({ line1Id, point1, line2Id, point2 });
    }
    apply(entity) {
        for (const c of this.constraints) {
            if (entity.id === c.line1Id) this.sync(entity, c.point1, c.line2Id, c.point2);
            if (entity.id === c.line2Id) this.sync(entity, c.point2, c.line1Id, c.point1);
        }
        Engine.regen();
    }
    sync(src, srcPoint, dstId, dstPoint) {
        const dst = Engine.getEntities().find(e => e.id === dstId);
        if (!dst) return;
        const target = srcPoint === 'start' ? src.startPoint : src.endPoint;
        if (dstPoint === 'start') dst.startPoint = [target.x, target.y];
        else                       dst.endPoint   = [target.x, target.y];
    }
}
```

> 完整代码：`vjcad/example/reactor/04constraint.js`（含折线 line1↔line2↔line3 的链式连接）

## 2. 距离恒定约束（圆心距）

```js
class DistanceConstraintReactor {
    constructor() { this.constraints = []; this.updating = false; }
    addConstraint(c1Id, c2Id) {
        const c1 = Engine.getEntities().find(e => e.id === c1Id);
        const c2 = Engine.getEntities().find(e => e.id === c2Id);
        const dx = c2.center.x - c1.center.x;
        const dy = c2.center.y - c1.center.y;
        const dist = Math.hypot(dx, dy);
        this.constraints.push({ c1Id, c2Id, dist });
    }
    install() {
        Engine.eventManager.on(CadEvents.EntityModified, ({ entity }) => {
            if (this.updating || entity.type !== 'CIRCLE') return;
            this.updating = true;
            try { this.apply(entity); }
            finally { this.updating = false; }
        });
    }
    apply(moved) {
        for (const c of this.constraints) {
            const otherId = moved.id === c.c1Id ? c.c2Id : c.c1Id;
            if (moved.id !== c.c1Id && moved.id !== c.c2Id) continue;
            const other = Engine.getEntities().find(e => e.id === otherId);
            const dx = other.center.x - moved.center.x;
            const dy = other.center.y - moved.center.y;
            const cur = Math.hypot(dx, dy);
            if (cur < 1e-3) continue;
            const k = c.dist / cur;
            other.center = new Point2D(
                moved.center.x + dx * k,
                moved.center.y + dy * k
            );
        }
        Engine.regen();
    }
}
```

> 完整代码：`vjcad/example/reactor/04constraint.js`

## 3. 推荐封装

把上面两类 Reactor 包装成"管道"，启动时一次安装：

```js
const endpoint = new EndpointConstraintReactor(); endpoint.install();
const distance = new DistanceConstraintReactor(); distance.install();

endpoint.addConstraint(line1.id, 'end', line2.id, 'start');
distance.addConstraint(circle1.id, circle2.id);
```

## 红旗

- **递归保护必须有**，否则 `apply` 内部修改 dst 又触发 `EntityModified`，无限循环
- 使用 `Engine.getEntities().find(e => e.id === id)` 在大图纸上很慢；约束数量多时改用 `Map<id, entity>` 缓存或在 `EntityAdded`/`EntitiesErased` 时维护索引
- 约束数量大于几十条时，建议合并成一个反应器统一调度，而不是为每条约束各开一个监听
- 删除是**软删除**：`Engine.eraseEntities` 只置 `isAlive=false`，实体仍留在 `currentSpace.items`，`Engine.getEntities().find(e => e.id === id)` 照样返回这个**已删除的旧实体**（不是 undefined），约束会继续去改它。查找时加 `e.isAlive` 过滤；上面的示例类没有 `removeConstraint`，需自己实现并在 `EntitiesErased` 回调里按 `args.entityIds` 移除相关约束

## 参考（官方文档 MCP）

- 示例：`vjcad/example/reactor/04constraint.js`
