Automatic constraints
64-bitWhich ends are joined and which lines are made horizontal or vertical.
coincident when |a − b| ≤ distance; horizontal when |sin α| ≤ sin(tolerance); vertical when |cos α| ≤ sin(tolerance)
Ends no further apart than the distance tolerance get a coincident constraint, and lines within the angle tolerance of level or upright get a horizontal or vertical one. The angle test compares sines, so there are no wrap-around cases. The tolerances are absolute and are not scaled to the drawing.
async function autoconstrain(c, { only = null, ids = null } = {}) {
const st = c.engine.autoc || (c.engine.autoc = { dist: 0.05, angle: 1 });
let set = ids;
if (!set) {
for (;;) {
set = await c.select(t('cmd.sim.select'), { options: only ? [] : [opt('S', 'Settings')] });
if (set && set.option === 'S') {
const d = await c.distance(t('cmd.delconstraint.specify_distance_tolerance'), { default: st.dist });
if (typeof d === 'number' && d >= 0) st.dist = d;
const a = await c.number(t('cmd.delconstraint.specify_angle_tolerance_degrees'), { default: st.angle });
if (typeof a === 'number' && a >= 0 && a < 45) st.angle = a;
c.print(t('cmd.autoconstrain.tolerances', { dist: st.dist, angle: st.angle }));
continue;
}
break;
}
}
const ents = [...set].map((id) => c.doc.get(id)).filter((e) => e && SOLVABLE.has(e.type));
if (!ents.length) { c.print(t('cmd.delconstraint.nothing_there_takes_constraints')); return; }
const has = (type, refs) => c.doc.constraints.some((k) => k.type === type && k.refs.length === refs.length && refs.every((r) => k.refs.some((q) => q.e === r.e && q.k === r.k)));
let n = 0;
// coincident: ends within the distance tolerance (one constraint per pair of neighbours, chained)
if (!only || only === 'coincident') {
const ends = [];
for (const e of ents) {
const ks = e.type === 'line' ? ['p1', 'p2'] : e.type === 'arc' ? ['s', 'e'] : e.type === 'point' ? ['p'] : e.type === 'poly' && !e.closed ? ['v0', 'v' + (e.pts.length - 1)] : [];
for (const k of ks) { const p = refPoint(e, k); ends.push({ e: e.id, k, x: p.x, y: p.y }); }
}
const joined = new Set();
for (let i = 0; i < ends.length; i++) for (let j = i + 1; j < ends.length; j++) {
const a = ends[i], b = ends[j];
if (a.e === b.e || joined.has(j) || Math.hypot(a.x - b.x, a.y - b.y) > st.dist) continue;
const refs = [{ e: b.e, k: b.k }, { e: a.e, k: a.k }];
if (has('coincident', refs)) { joined.add(j); continue; }
const r = addConstraint(c.doc, 'coincident', refs, { pinFirst: false });
if (r.ok) { n++; joined.add(j); }
}
}
// horizontal / vertical: lines and straight polyline segments within the angle tolerance
if (!only) {
const tol = st.angle * Math.PI / 180;
for (const e of ents) {
const segs = e.type === 'line' ? ['L'] : e.type === 'poly' ? Array.from({ length: segCount(e) }, (_, i) => 's' + i).filter((k) => refKind(e, k) === 'L') : [];
for (const k of segs) {
const l = refLine(e, k);
const a = Math.atan2(l.y2 - l.y1, l.x2 - l.x1);
const h = Math.abs(Math.sin(a)) <= Math.sin(tol), v = Math.abs(Math.cos(a)) <= Math.sin(tol);
const type = h ? 'horizontal' : v ? 'vertical' : null;
if (!type || has(type, [{ e: e.id, k }]) || has(type === 'horizontal' ? 'vertical' : 'horizontal', [{ e: e.id, k }])) continue;
const r = addConstraint(c.doc, type, [{ e: e.id, k }], { pinFirst: false });
if (r.ok) n++;
}
}
}
c.host.setSelection(new Set());
c.print(n ? t('cmd.autoconstrain.applied', { n }) + ' ' + statusText(c.doc) : t('cmd.autoconstrain.none', { dist: st.dist, angle: st.angle }));
c.host.redraw?.();
}