用反应器实现几何约束
示例:
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