Resources Numerical accuracy Calculation policy

Constraint values and signs

Measures what a dimensional constraint holds (distance, radius, diameter, angle), records which side or direction a constraint applies to, and decides what an edit moved.

How the precision is kept

Measurements are closed-form 64-bit expressions on the objects' coordinates: differences, Math.hypot, signed distance from a line and Math.atan2 of a cross and a dot product. Signs and sides are taken from the geometry as drawn. The solving itself is in gsolve.js. Where a dimensional constraint's value is written into the parameter table as text, it is first rounded to 6 decimals and then to 12 significant digits.

Open item. Write a dimensional constraint's value into the parameter table with full precision instead of rounding it to 6 decimals and 12 significant digits.

Calculations under this policy

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

gcons.js CadCraft 0.8.2 64-bit 13 calculations Reviewed

Tests bin/test-cadcraft-constraints.mjs

What a dimensional constraint measures

64-bit

The current horizontal, vertical or direct distance, radius, diameter or angle of the constrained geometry.

hdist = s × (x₁ − x₀); dist = |p₁ − p₀| or s × signed distance from the line; diameter = 2r; angle = s × atan2(u × v, u · v) × 180 ÷ π; arc angle = (a₁ − a₀) mod 2π in degrees

Each case is one closed-form 64-bit expression. The angle between two lines uses atan2 of the cross and dot products, which keeps its accuracy for small and near-straight angles alike. An arc whose sweep reduces to 0 or less has 2π added, so a full circle measures 360 degrees.

  • gcons.js
  • lines 262 to 286
  • CadCraft 0.8.2
/** What a dimensional constraint measures in the drawing now (≥ 0; degrees for angles). */
export function measure(doc, c, get = getter(doc)) {
  const P = (i) => refPoint(get(c.refs[i].e), c.refs[i].k);
  const L = (i) => refLine(get(c.refs[i].e), c.refs[i].k);
  const C = (i) => refCircle(get(c.refs[i].e), c.refs[i].k);
  const K = (i) => refKind(get(c.refs[i].e), c.refs[i].k);
  const s = c.sign || 1;
  switch (c.type) {
    case 'hdist': return s * (P(1).x - P(0).x);
    case 'vdist': return s * (P(1).y - P(0).y);
    case 'dist':
      if (K(1) === 'P') return Math.hypot(P(1).x - P(0).x, P(1).y - P(0).y);
      if (K(0) === 'P') return s * sdist(P(0), L(1));
      { const l2 = L(1); return s * sdist({ x: l2.x1, y: l2.y1 }, L(0)); }
    case 'radius': return C(0).r;
    case 'diameter': return 2 * C(0).r;
    case 'angle': {
      if (K(0) === 'R') { const e = get(c.refs[0].e); let sw = (e.a1 - e.a0) % TAU; if (sw <= 0) sw += TAU; return sw * 180 / Math.PI; }
      const l1 = L(0), l2 = L(1), [s1, s2] = c.dirs || [1, 1];
      const u = [(l1.x2 - l1.x1) * s1, (l1.y2 - l1.y1) * s1], v = [(l2.x2 - l2.x1) * s2, (l2.y2 - l2.y1) * s2];
      return s * Math.atan2(u[0] * v[1] - u[1] * v[0], u[0] * v[0] + u[1] * v[1]) * 180 / Math.PI;
    }
    default: return 0;
  }
}

Signed distance from a line

64-bit

How far a point or a circle's center lies from an infinite line, positive on the left of its direction.

d = (u_x × (p_y − l_y) − u_y × (p_x − l_x)) ÷ |u|

A cross product divided by the line's length from Math.hypot. For a point very close to the line the cross product subtracts two nearly equal products and loses digits; the distance concerned is then itself very small. A zero-length line divides by 1e-300 instead of 0.

  • gcons.js
  • lines 244 to 248
  • CadCraft 0.8.2
/** Signed distance of a point (or a circle's centre) from the infinite line l, left of its direction positive. */
const sdist = (p, l) => {
  const px = p.x ?? p.cx, py = p.y ?? p.cy, ux = l.x2 - l.x1, uy = l.y2 - l.y1;
  return (ux * (py - l.y1) - uy * (px - l.x1)) / (Math.hypot(ux, uy) || 1e-300);
};

Angle constraint: legs and sign

64-bit

Which direction along each line the angle is measured between, and whether it turns counterclockwise.

parallel when |u × v| < 1e-9 × |u| × |v|; sign = sign of atan2(u′ × v′, u′ · v′)

The parallel test is scaled to the two lines' lengths. The legs are chosen by the bisector that points most toward the label; only signs are stored, so this step adds no rounding to the angle's value.

  • gcons.js
  • lines 212 to 228
  • CadCraft 0.8.2
const u = [l1.x2 - l1.x1, l1.y2 - l1.y1], v = [l2.x2 - l2.x1, l2.y2 - l2.y1];
if (Math.abs(u[0] * v[1] - u[1] * v[0]) < 1e-9 * Math.hypot(...u) * Math.hypot(...v)) return { error: t('gcons.those_lines_parallel_there') };
let s1 = 1, s2 = 1;
if (opts.loc) {
  // the sector holding the label: each leg points from the corner toward the side the label is on
  const x = intersectLines(l1, l2);
  const w = [opts.loc.x - x.x, opts.loc.y - x.y];
  const bis = (a, b) => [a[0] / Math.hypot(...a) + b[0] / Math.hypot(...b), a[1] / Math.hypot(...a) + b[1] / Math.hypot(...b)];
  let best = -Infinity;
  for (const a of [1, -1]) for (const b of [1, -1]) {
    const m = bis(u.map((q) => q * a), v.map((q) => q * b)), sc = (m[0] * w[0] + m[1] * w[1]) / (Math.hypot(...m) || 1);
    if (sc > best) { best = sc; s1 = a; s2 = b; }
  }
}
c.dirs = [s1, s2];
const uu = u.map((q) => q * s1), vv = v.map((q) => q * s2);
c.sign = Math.atan2(uu[0] * vv[1] - uu[1] * vv[0], uu[0] * vv[0] + uu[1] * vv[1]) >= 0 ? 1 : -1;

Tangent circles: inside or outside

64-bit

Decides whether two circles made tangent touch from inside or from outside.

internal when |c_b − c_a| < max(r_a, r_b) − 1e-12

The distance between centers is compared with the larger radius, with an absolute margin of 1e-12 drawing units. The result is stored as a kind and a sign, not as a number.

  • gcons.js
  • lines 188 to 189
  • CadCraft 0.8.2
const a = C(0), b = C(1), d = Math.hypot(b.cx - a.cx, b.cy - a.cy);
if (d < Math.max(a.r, b.r) - 1e-12) { c.kind = 'int'; c.sign = a.r >= b.r ? 1 : -1; }

Crossing point of two lines

64-bit

Where two infinite lines meet, used as the corner of an angle constraint.

den = d₁ × d₂; t = ((l₂ − l₁) × d₂) ÷ den; x = l₁ + d₁ × t

The only guard is |den| < 1e-300, which catches exact parallels; makeConstraint tests for near-parallel lines with a scaled tolerance before it uses the point. For lines at a small angle den is a difference of nearly equal products and the point loses digits in proportion.

  • gcons.js
  • lines 254 to 260
  • CadCraft 0.8.2
function intersectLines(l1, l2) {
  const d1x = l1.x2 - l1.x1, d1y = l1.y2 - l1.y1, d2x = l2.x2 - l2.x1, d2y = l2.y2 - l2.y1;
  const den = d1x * d2y - d1y * d2x;
  if (Math.abs(den) < 1e-300) return { x: l1.x1, y: l1.y1 };
  const t = ((l2.x1 - l1.x1) * d2y - (l2.y1 - l1.y1) * d2x) / den;
  return { x: l1.x1 + d1x * t, y: l1.y1 + d1y * t };
}

Foot of a perpendicular

64-bit

The point of a line nearest to a given point.

t = ((p − l₁) · u) ÷ (u · u); foot = l₁ + t × u

One dot product divided by the squared length (1e-300 when that is 0). Used to place the two ends of a point-to-line or line-to-line distance.

  • gcons.js
  • lines 334 to 338
  • CadCraft 0.8.2
function foot(p, l) {
  const ux = l.x2 - l.x1, uy = l.y2 - l.y1, L2 = ux * ux + uy * uy || 1e-300;
  const t = ((p.x - l.x1) * ux + (p.y - l.y1) * uy) / L2;
  return { x: l.x1 + t * ux, y: l.y1 + t * uy };
}

Mirror gap

64-bit

How far a point is from being the mirror image of another across a line.

f = foot of a on the line; gap = 2f − a − b

Used when a symmetric constraint is made, to choose which ends of two lines pair up: the pairing with the smaller total gap wins. The gap itself is not stored.

  • gcons.js
  • lines 249 to 253
  • CadCraft 0.8.2
function mirrorGap(a, b, l) {
  const ux = l.x2 - l.x1, uy = l.y2 - l.y1, L2 = ux * ux + uy * uy || 1e-300;
  const t = ((a.x - l.x1) * ux + (a.y - l.y1) * uy) / L2, fx = l.x1 + t * ux, fy = l.y1 + t * uy;
  return [2 * fx - a.x - b.x, 2 * fy - a.y - b.y];
}

Value rounded to 6 decimals

64-bit

Rounds a constraint's value to a millionth of a unit for its label and its parameter text.

Math.round(v × 1e6) ÷ 1e6

An absolute rounding. It is used for the label and also where the value becomes the parameter's expression text, so there it changes the stored value: a value of 0.0000004 becomes 0.

  • gcons.js
  • line 350
  • CadCraft 0.8.2
const round6 = (v) => Math.round(v * 1e6) / 1e6;

Constraint value as a parameter

64-bit

Creates the parameter for a dimensional constraint read from a file that lacks one.

expr = formatNumber(round6(value))

The parameter's expression is the value rounded to 6 decimals and then to 12 significant digits, written as text. The parameter drives the constraint from then on, so the driving value can differ from the measured one by up to half a millionth of a unit, or by the 12-digit rounding when the value is above a million. The same rounding is used when constraints are pasted.

  • gcons.js
  • line 661
  • CadCraft 0.8.2
params.vars.push({ name: c.name, expr: formatNumber(round6(c.value)) });

Label value

64-bit

The number shown in a dimensional constraint's label.

text = formatNumber(round6(v)), with ° for angles

Rounded to 6 decimals and at most 12 significant digits for display. The constraint's own value field is not changed by this.

  • gcons.js
  • line 351
  • CadCraft 0.8.2
export const fmtValue = (c, v) => (c.type === 'angle' ? formatNumber(round6(v)) + '°' : formatNumber(round6(v)));

Did solving collapse an object

64-bit

Refuses a solution that shrinks a line, a polyline segment or a radius to almost nothing.

collapsed when new < 1e-6 × old and old > 1e-9

The test is relative to the object's size before solving, with an absolute floor of 1e-9 on that size. An object already shorter than 1e-9 units is never reported.

  • gcons.js
  • lines 444 to 456
  • CadCraft 0.8.2
/** Did solving shrink a line, a polyline segment or a radius to (almost) nothing? */
function collapsed(was, now) {
  const tiny = (a, b) => b < 1e-6 * Math.max(a, 1e-300) && a > 1e-9;
  if (now.type === 'line') return tiny(Math.hypot(was.x2 - was.x1, was.y2 - was.y1), Math.hypot(now.x2 - now.x1, now.y2 - now.y1));
  if (now.type === 'circle' || now.type === 'arc') return tiny(was.r, now.r);
  if (now.type === 'poly') {
    for (let i = 0; i < segCount(now); i++) {
      const j = (i + 1) % now.pts.length;
      if (tiny(Math.hypot(was.pts[j].x - was.pts[i].x, was.pts[j].y - was.pts[i].y), Math.hypot(now.pts[j].x - now.pts[i].x, now.pts[j].y - now.pts[i].y))) return true;
    }
  }
  return false;
}

Which fields an edit moved

64-bit

Finds the coordinates an edit changed, so the solver holds them where the edit put them.

moved when |before − after| > 1e-12 × max(1, |before|)

Relative above 1 and absolute (1e-12) below. A change smaller than that is not pinned and the solver may move the field back.

  • gcons.js
  • lines 522 to 525
  • CadCraft 0.8.2
for (const f of fieldsOf(now)) {
  const a = getField(was, f), b = getField(now, f);
  if (Math.abs(a - b) > 1e-12 * Math.max(1, Math.abs(a))) pinned.add(id + '|' + f);
}

Fixed values moved on paste

64-bit

Shifts the stored position of a fix constraint by the paste offset.

x′ = x + dx; y′ = y + dy

One addition per stored coordinate, the same offset the pasted objects received. Radii and angles in a fixed object are left unchanged.

  • gcons.js
  • lines 761 to 762
  • CadCraft 0.8.2
if (k.value) k.value = [k.value[0] + dx, k.value[1] + dy];
if (k.fixed) { const f = { ...k.fixed }; for (const key of Object.keys(f)) { if (/^(x1|x2|cx|x|\d+x)$/.test(key)) f[key] += dx; else if (/^(y1|y2|cy|y|\d+y)$/.test(key)) f[key] += dy; } k.fixed = f; }

In detail

Method
Signed distance = cross product ÷ line length; angle = atan2(u × v, u · v) in degrees; an arc's angle is its sweep modulo 2π.
Number types
64-bit numbers; lengths in drawing units, angles in degrees.
Tolerances
Parallel lines: |u × v| < 1e-9 × |u| × |v|. Internal tangency: d < max(r) − 1e-12. A field counts as moved when it changed by more than 1e-12 × max(1, |value|). A solution is refused when a length or radius shrinks below 1e-6 of what it was. A zero line length is replaced by 1e-300 in divisions.
Limits
A value written as parameter text is rounded to 6 decimals (absolute), which in a drawing in meters is one micrometer and in a drawing whose values are below 1e-6 is zero.

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