唯杰地图 logo唯杰地图
VJMAP
EN

用反应器实现几何约束

vjcad/references/reactor/constraint.md 4.8 KB122 行
原文

示例:vjcad/example/reactor/04constraint.js

vjcad 没有内置 SolidWorks 那种参数化求解器,但是利用事件型反应器可以实现 端点连接、距离恒定、平行、共心 这类轻量约束,足够大部分项目场景。

1. 端点连接约束("图形粘合")

当用户拖动 line1 的某个端点时,line2 的对应端点自动跟过去。

数据结构:

this.constraints = [
    { line1Id, point1: 'start'|'end', line2Id, point2: 'start'|'end' },
];

事件回调(关键:递归保护 this.updating):

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. 距离恒定约束(圆心距)

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 包装成"管道",启动时一次安装:

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