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