Measures what a dimension spans from its defining points and works out its extension lines, dimension line, arrowheads and text position.
How the precision is kept
A dimension keeps its defining points as 64-bit numbers and measures from them each time, so the value is never copied from text. Lengths use Math.hypot, angles Math.atan2, and points are placed with cosine and sine of 64-bit angles. Degenerate cases are caught with absolute tolerances of 1e-12 and 1e-9; the parallel test for two lines is scaled to their lengths.
Calculations under this policy
8 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.
dims.jsCadCraft 0.8.264-bit8 calculationsReviewed
Tests bin/test-cadcraft.mjs
Measured value
64-bit
The length, radius, diameter or angle a dimension reports.
Distances use Math.hypot of coordinate differences. The linear case multiplies the offset by cos r and sin r: for r = π ÷ 2 Math.cos returns 6.1e-17 and not 0, so a vertical dimension picks up that fraction of the horizontal offset. The diameter doubles the radius, which is exact.
dims.js
lines 33 to 43
CadCraft 0.8.2
/** The measured value (drawing units, or radians for angular). */
export function dimMeasure(e) {
switch (e.kind) {
case 'linear': { const u = { x: Math.cos(e.rot || 0), y: Math.sin(e.rot || 0) }; return Math.abs((e.p2.x - e.p1.x) * u.x + (e.p2.y - e.p1.y) * u.y); }
case 'aligned': return dist(e.p1, e.p2);
case 'radius': return dist(e.c, e.p1);
case 'diameter': return 2 * dist(e.c, e.p1);
case 'angular': return angularSweep(e).sweep;
default: return 0;
}
}
Sector of an angular dimension
64-bit
The start angle, end angle and counterclockwise sweep an angular dimension measures.
The side that holds the text point is chosen with onSweep, which allows 1e-9 radians. A sweep below 1e-12 radians (absolute) is returned as a full turn, so two legs that coincide measure 360 degrees and not 0.
dims.js
lines 62 to 69
CadCraft 0.8.2
/** The CCW sector of an angular dimension: { s, e (start/end angles), sweep }. */
export function angularSweep(e) {
const a1 = angleOf(e.c, e.p1), a2 = angleOf(e.c, e.p2), al = angleOf(e.c, e.loc);
const [s, end] = onSweep(normAngle(al), normAngle(a1), normAngle(a1) + normAngle(a2 - a1)) ? [a1, a2] : [a2, a1];
let sweep = normAngle(end - s);
if (sweep < 1e-12) sweep = TAU;
return { s: normAngle(s), e: normAngle(s) + sweep, sweep };
}
Ends of the dimension line
64-bit
Where the two extension lines meet the dimension line, square or at an obliquing angle.
square: q = loc + u × ((p − loc) · u); oblique: q = p + d × ((loc − p) · n) ÷ (d · n)
The square case is one projection per point. The oblique case divides by d · n, which equals sin(obl − r); it is used only when that sine exceeds 1e-6 in magnitude, so the division can magnify rounding by at most a factor of a million.
dims.js
lines 191 to 206
CadCraft 0.8.2
/**
* Where a linear or aligned dimension's extension lines meet its dimension line: q1 on the first, q2 on the second. Square to
* the dimension line, or slanted when the dimension has an obliquing angle `obl` (DIMEDIT Oblique: the angle of the extension
* lines from the X axis, radians).
*/
export function dimLinePoints(e) {
const r = e.kind === 'aligned' ? angleOf(e.p1, e.p2) : (e.rot || 0);
const u = { x: Math.cos(r), y: Math.sin(r) };
if (Number.isFinite(e.obl) && Math.abs(Math.sin(e.obl - r)) > 1e-6) {
const dv = { x: Math.cos(e.obl), y: Math.sin(e.obl) }, n = { x: -u.y, y: u.x }, den = dv.x * n.x + dv.y * n.y;
const hit = (p) => { const k = ((e.loc.x - p.x) * n.x + (e.loc.y - p.y) * n.y) / den; return { x: p.x + dv.x * k, y: p.y + dv.y * k }; };
return { q1: hit(e.p1), q2: hit(e.p2), u, r };
}
const d1 = (e.p1.x - e.loc.x) * u.x + (e.p1.y - e.loc.y) * u.y, d2 = (e.p2.x - e.loc.x) * u.x + (e.p2.y - e.loc.y) * u.y;
return { q1: { x: e.loc.x + u.x * d1, y: e.loc.y + u.y * d1 }, q2: { x: e.loc.x + u.x * d2, y: e.loc.y + u.y * d2 }, u, r };
}
Angular dimension between two lines
64-bit
The vertex where two lines cross and a point on each leg bounding the sector that holds the text point.
den = d₁ × d₂; t = ((l₂ − l₁) × d₂) ÷ den; c = l₁ + d₁ × t
The parallel test is |den| < 1e-12 × |d₁| × |d₂|, scaled to the lines' lengths. For lines that are nearly parallel but pass the test, den is a difference of nearly equal products and the vertex loses digits in proportion. The leg tests use absolute 1e-9.
dims.js
lines 220 to 251
CadCraft 0.8.2
/**
* Angular dimension between two lines: the vertex, and a point on each leg chosen so that the
* sector holding `loc` lies between them (the far end of each line on that side of the vertex).
*/
export function angularFromLines(l1, l2, loc) {
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-12 * Math.hypot(d1x, d1y) * Math.hypot(d2x, d2y)) return null;
const t = ((l2.x1 - l1.x1) * d2y - (l2.y1 - l1.y1) * d2x) / den;
const c = { x: l1.x1 + d1x * t, y: l1.y1 + d1y * t };
const al = angleOf(c, loc);
const legs = [l1, l2].map((l) => {
const a = Math.atan2(l.y2 - l.y1, l.x2 - l.x1);
return [a, a + Math.PI].map((ang) => {
// the far end of the line in this direction, else a unit step along it
let best = null;
for (const p of [{ x: l.x1, y: l.y1 }, { x: l.x2, y: l.y2 }]) {
const along = (p.x - c.x) * Math.cos(ang) + (p.y - c.y) * Math.sin(ang);
if (along > 1e-9 && (!best || along > best.along)) best = { along, p };
}
return { ang: normAngle(ang), p: best ? best.p : polar(c, ang, Math.hypot(d1x, d1y) * 1e-3 || 1) };
});
});
// the two rays (one per line) whose CCW sector, under 180°, holds loc
for (const r1 of legs[0]) for (const r2 of legs[1]) {
for (const [a, b] of [[r1, r2], [r2, r1]]) {
const sw = normAngle(b.ang - a.ang);
if (sw > 1e-9 && sw < Math.PI - 1e-9 && onSweep(normAngle(al), a.ang, a.ang + sw)) return { c, p1: a.p, p2: b.p };
}
}
return null;
}
Linear and aligned dimension parts
64-bit
The extension lines, dimension line, arrowheads and text point of a linear or aligned dimension.
extension line: from p + exo to q + exe; arrows inside when length ≥ 2 × asz; text at the middle, offset by gap
Each part is built with polar(point, angle, distance), one cosine and sine per point. Zero-length parts are dropped at 1e-12 (absolute). The text width used to break the line comes from font advance widths and only affects layout, not the measured value.
dims.js
lines 107 to 137
CadCraft 0.8.2
case 'linear': case 'aligned': {
const r = e.kind === 'aligned' ? angleOf(e.p1, e.p2) : (e.rot || 0);
const { q1, q2 } = dimLinePoints(e);
for (const [p, q, role] of [[e.p1, q1, 'ext1'], [e.p2, q2, 'ext2']]) {
const L = dist(p, q);
if (L < 1e-12) continue;
const a = angleOf(p, q);
if (exo < L + exe) line(polar(p, a, Math.min(exo, L)), polar(q, a, exe), role);
}
const len = dist(q1, q2);
const inside = len >= 2 * asz + 1e-12;
const a12 = len > 1e-12 ? angleOf(q1, q2) : r;
const mid = { x: (q1.x + q2.x) / 2, y: (q1.y + q2.y) / 2 };
const ta = readable(a12), up = ta + Math.PI / 2;
// DIMJUST 1 / 2 (st.just): the text beside the first / second extension line instead of centred
const off = Math.min(len / 2, 2 * asz + tw / 2 + gap);
const tm = st.just === 1 && len > 1e-12 ? polar(q1, a12, off) : st.just === 2 && len > 1e-12 ? polar(q2, a12 + Math.PI, off) : mid;
const fitsInLine = st.tad === 0 && len > tw + 2 * gap + (inside ? 2 * asz : 0);
if (inside) {
if (fitsInLine) { line(q1, polar(tm, a12 + Math.PI, tw / 2 + gap), 'dim1'); line(polar(tm, a12, tw / 2 + gap), q2, 'dim2'); }
else line(q1, q2, 'dim');
arrow(q1, a12 + Math.PI, 'arrow1'); arrow(q2, a12, 'arrow2');
} else {
line(polar(q1, a12 + Math.PI, 2 * asz), polar(q2, a12, 2 * asz), 'dim');
arrow(q1, a12, 'arrow1'); arrow(q2, a12 + Math.PI, 'arrow2');
}
if (fitsInLine) text(tm, ta, 'center', 'middle', s);
else if (st.tad === 4) text(polar(tm, up, -gap), ta, 'center', 'top', s); // DIMTAD 4: below the line
else text(polar(tm, up, gap), ta, 'center', 'bottom', s);
break;
}
Angular dimension parts
64-bit
The arc, extension lines, arrowheads and text point of an angular dimension.
R = |loc − c|; arc from a₀ to a₁ at R; arrows outside when R × sweep < 2 × asz; text at c + (R + gap) at a₀ + sweep ÷ 2
The arc radius is the distance from the vertex to the text point. Extension lines are drawn when R differs from the leg point's distance by more than 1e-9 (absolute). A radius below 1e-12 draws nothing.
The center mark, leader or dimension line, arrowheads and text point of a radius or diameter dimension.
r = |p₁ − c|; P = c + r × (cos u, sin u); Q = c − r × (cos u, sin u); center mark half-length = asz ÷ 2
The direction u is the angle to the text point, or to the picked point when the text point is within 1e-12 of the center. The text goes outside when its distance exceeds r by more than 1e-9 (absolute).
dims.js
lines 156 to 185
CadCraft 0.8.2
case 'radius': case 'diameter': {
const r = dist(e.c, e.p1);
if (r < 1e-12) break;
const dl = dist(e.c, e.loc);
const u = dl > 1e-12 ? angleOf(e.c, e.loc) : angleOf(e.c, e.p1);
const P = polar(e.c, u, r);
const cm = asz / 2; // centre mark
line(polar(e.c, 0, -cm), polar(e.c, 0, cm)); line(polar(e.c, Math.PI / 2, -cm), polar(e.c, Math.PI / 2, cm));
if (dl > r + 1e-9) {
// text outside: a leader from the curve out to the text, with a short landing
line(P, e.loc, 'dim');
arrow(P, u + Math.PI, 'arrow1');
const sx = Math.cos(u) >= 0 ? 1 : -1;
const land = { x: e.loc.x + sx * asz, y: e.loc.y };
line(e.loc, land, 'dim');
text({ x: land.x + sx * gap, y: land.y }, 0, sx > 0 ? 'left' : 'right', 'middle', s);
} else if (e.kind === 'radius') {
line(e.c, P, 'dim');
arrow(P, u, 'arrow1');
const ta = readable(u);
text(polar(polar(e.c, u, r / 2), ta + Math.PI / 2, gap), ta, 'center', 'bottom', s);
} else {
const Q = polar(e.c, u + Math.PI, r);
line(Q, P, 'dim');
arrow(P, u, 'arrow1'); arrow(Q, u + Math.PI, 'arrow2');
const ta = readable(u);
text(polar(e.c, ta + Math.PI / 2, gap), ta, 'center', 'bottom', s);
}
break;
}
Closed arrowhead
64-bit
The three corners of the default filled arrowhead.
base = tip − asz along the arrow; corners = base ± asz ÷ 6 across
Three polar steps from the tip. The arrow is asz long and asz ÷ 3 wide at its base.
dims.js
lines 97 to 101
CadCraft 0.8.2
const arrow = (tip, ang, role) => {
const base = polar(tip, ang + Math.PI, asz), w = asz / 6;
const l = polar(base, ang + Math.PI / 2, w), r = polar(base, ang - Math.PI / 2, w);
out.push({ type: 'hatch', layer: '0', color: 0, solid: true, loops: [[tip.x, tip.y, l.x, l.y, r.x, r.y, tip.x, tip.y]], role, tip: { x: tip.x, y: tip.y }, ang: normAngle(ang) });
};
In detail
Method
Linear: projection of p2 − p1 on the dimension direction. Aligned and radial: Euclidean distance. Angular: counterclockwise sweep between two rays from the vertex, on the side of the text point.
Number types
64-bit numbers throughout; angles in radians.
Tolerances
1e-12 (absolute, drawing units) for zero-length spans and parts; 1e-9 (absolute) for radial comparisons and angle sectors; an obliquing angle is used only when |sin(obl − r)| > 1e-6; two lines are parallel when their cross product is below 1e-12 × the product of their lengths.
Limits
Absolute tolerances do not scale with the drawing: in a drawing whose features are smaller than 1e-9 units they treat real geometry as 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.