Resources Numerical accuracy Calculation policy

Automatic constraints and dimension conversion

Finds ends that meet and lines that are level or upright and constrains them, and turns a plain dimension into a constraint on the objects it measures.

How the precision is kept

These are yes-or-no decisions on 64-bit geometry with stated tolerances. Automatic constraints use the person's own tolerances, 0.05 drawing units and 1° by default. Once a constraint is added the solver moves the geometry to meet it, so ends up to the distance tolerance apart are pulled together.

Calculations under this policy

2 calculations in 1 source file. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.

pcommands.js CadCraft 0.8.2 64-bit 2 calculations Reviewed

Tests bin/test-cadcraft-constraints.mjs

Automatic constraints

64-bit

Which 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.

  • pcommands.js
  • lines 345 to 402
  • CadCraft 0.8.2
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?.();
}

Dimension to constraint

64-bit

The constraint a plain dimension stands for, when its points sit on constrainable geometry.

tol = 1e-7 × max(1, largest |x| or |y|)

A dimension point is matched to an object's point within tol, a circle by center and radius within tol, and a point on a line within 10 × tol. tol grows with the largest coordinate in the model, so it is looser far from the origin. A linear dimension is horizontal or vertical when its rotation is within 1e-9 rad of 0, π or π ÷ 2.

  • pcommands.js
  • lines 491 to 523
  • CadCraft 0.8.2
/** A plain dimension → the constraint it stands for, when its points sit on constrainable geometry. */
export function convertDim(doc, d) {
  const ents = [...doc.model.values()].filter((e) => SOLVABLE.has(e.type));
  let size = 1;
  for (const e of ents) for (const p of constraintPoints(e)) size = Math.max(size, Math.abs(p.x), Math.abs(p.y));
  const tol = 1e-7 * size;
  const pointAt = (p) => {
    for (const e of ents) for (const q of constraintPoints(e)) if (Math.hypot(q.x - p.x, q.y - p.y) <= tol) return { e: e.id, k: q.k };
    return null;
  };
  if (d.kind === 'linear' || d.kind === 'aligned') {
    const a = pointAt(d.p1), b = pointAt(d.p2);
    if (!a || !b) return null;
    if (d.kind === 'aligned') return { type: 'dist', refs: [a, b] };
    const rot = ((d.rot || 0) % Math.PI + Math.PI) % Math.PI;
    if (Math.abs(rot) < 1e-9 || Math.abs(rot - Math.PI) < 1e-9) return { type: 'hdist', refs: [a, b] };
    if (Math.abs(rot - Math.PI / 2) < 1e-9) return { type: 'vdist', refs: [a, b] };
    return { type: 'dist', refs: [a, b] };
  }
  if (d.kind === 'radius' || d.kind === 'diameter') {
    const r = Math.hypot(d.p1.x - d.c.x, d.p1.y - d.c.y);
    const e = ents.find((x) => (x.type === 'circle' || x.type === 'arc') && Math.hypot(x.cx - d.c.x, x.cy - d.c.y) <= tol && Math.abs(x.r - r) <= tol);
    return e ? { type: d.kind, refs: [{ e: e.id, k: 'R' }] } : null;
  }
  if (d.kind === 'angular') {
    const through = (a, b) => ents.find((e) => e.type === 'line' && onLine(e, a, tol) && onLine(e, b, tol));
    const l1 = through(d.c, d.p1), l2 = through(d.c, d.p2);
    if (l1 && l2 && l1 !== l2) return { type: 'angle', refs: [{ e: l1.id, k: 'L' }, { e: l2.id, k: 'L' }] };
    const arc = ents.find((e) => e.type === 'arc' && Math.hypot(e.cx - d.c.x, e.cy - d.c.y) <= tol);
    return arc ? { type: 'angle', refs: [{ e: arc.id, k: 'R' }] } : null;
  }
  return null;
}

In detail

Method
Pairwise distance between ends; sine and cosine of each line's direction against the sine of the angle tolerance.
Number types
64-bit numbers.
Tolerances
Automatic constraints: distance 0.05 drawing units and angle 1° by default, both settable. Dimension conversion: 1e-7 × the largest coordinate in size (at least 1e-7) for points, 10 times that for a point on a line, 1e-9 rad for a level or upright dimension.
Limits
The default distance tolerance is an absolute 0.05 drawing units whatever the drawing's units or size.

Approach: 64-bit, direct

Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.

Every policy that takes this approach