Works out the circles, arcs, polyline arcs, polygons, ellipses, construction lines and text positions that the draw commands store in the drawing.
How the precision is kept
Each shape is computed from the picked points in 64-bit and stored unrounded. Circles and arcs through three points use the circumcenter formula on the points' absolute coordinates, which loses digits when the points are close together and far from the origin. Input with no size is refused with absolute tolerances of 1e-12.
Open item. In circle3, subtract the first point from the other two before squaring and add it back to the center, and scale the in-line test to the lengths of the sides.
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 formula uses the squares of the absolute coordinates, and far from the origin its three terms nearly cancel: for coordinates of size M and points a distance s apart the center's error grows as M² × 10⁻¹⁶ ÷ s. Evaluating it for an equilateral triangle of radius 0.5 at coordinates near 10⁶ gives a center off by up to about 6 × 10⁻⁴, against about 10⁻¹⁰ when the first point is subtracted first. Points are taken as in line when |d| < 1e-12, an absolute test in square drawing units.
The center, radius and end angles of the arc from a start point through a second point to an end point.
center, r from circle3; counter-clockwise when (m − s) × (e − s) > 0; a₀ = atan2(s − center), a₁ = atan2(e − center)
The center and radius come from circle3 and carry its error. The direction is the sign of one cross product of coordinate differences, with no tolerance. The arc is stored as center, radius and two angles normalized to 0…2π, so its ends are center + r·(cos, sin) of those angles and can differ from the picked points by the center's error and the rounding of the trigonometry.
commands.js
lines 3992 to 4001
CadCraft 0.8.2
/** Arc through start, a point on it and end. */
export function arc3(s, m, e) {
const k = circle3(s, m, e);
if (!k) return null;
const c = { x: k.cx, y: k.cy };
const ccw = (m.x - s.x) * (e.y - s.y) - (m.y - s.y) * (e.x - s.x) > 0; // a left turn s→m→e travels CCW round the centre
let a0 = angleOf(c, s), a1 = angleOf(c, e);
if (!ccw) [a0, a1] = [a1, a0];
return { ...k, a0: normAngle(a0), a1: normAngle(a1) };
}
Arc segment of a polyline
64-bit
The bulge of a polyline's arc segment that leaves the last point along the previous segment's direction and ends at the picked point.
φ = angle(p → q) − tangent at p, wrapped to −π…π; bulge = tan(φ ÷ 2)
The tangent is the previous straight segment's angle, or for a previous arc segment atan2 from that arc's center (geom.js bulgeArc) turned 90°. As the picked point comes to lie straight behind the tangent, φ approaches ±π and tan(φ ÷ 2) grows without limit; the code does not cap it.
commands.js
lines 548 to 560
CadCraft 0.8.2
const tangent = () => { // direction at the end of the last segment
if (pts.length < 2) return null;
const p = pts[pts.length - 2], q = pts[pts.length - 1];
if (!p.b) return angleOf(p, q);
const a = bulgeArc(p, q, p.b);
const ang = Math.atan2(q.y - a.cy, q.x - a.cx);
return ang + (p.b > 0 ? Math.PI / 2 : -Math.PI / 2);
};
const arcBulge = (p, q) => {
const t = tangent() ?? 0;
let phi = normAngle(angleOf(p, q) - t); if (phi > Math.PI) phi -= TAU;
return Math.tan(phi / 2);
};
Circle from the two ends of a diameter
64-bit
The center and radius of a circle given by two opposite points on it.
center = (a + b) ÷ 2; r = |b − a| ÷ 2
The midpoint and half of one Math.hypot distance. The halvings are exact, so only the sum and the hypot round.
The radius is the distance to the start point. The end point only gives the end angle, so the arc ends on the ray toward the picked point and not at it. Both angles are normalized to 0…2π.
commands.js
lines 754 to 760
CadCraft 0.8.2
if (p0.option === 'C') {
const center = await c.point(t('cmd.arc.specify_center_point_arc'));
const s = await c.point(t('cmd.arc.specify_start_point_arc'), { base: center });
const r = dist(center, s), a0 = angleOf(center, s);
c.preview((cur) => [{ type: 'arc', cx: center.x, cy: center.y, r, a0, a1: angleOf(center, cur) }]);
const e = await c.point(t('cmd.arc.specify_end_point_arc'), { base: center });
c.add(ent(c, { type: 'arc', cx: center.x, cy: center.y, r, a0: normAngle(a0), a1: normAngle(angleOf(center, e)) }));
Rectangle from two corners
64-bit
The closed four-corner polyline between two picked corners.
The four corners reuse the two picked points' coordinates with no arithmetic. A width or height under 1e-12 drawing units (absolute) is refused.
The corners of a regular polygon inscribed in or drawn round a circle.
R = r (inscribed) or r ÷ cos(π ÷ n) (circumscribed); pᵢ = center + R·(cos, sin)(a₀ + i·2π ÷ n)
Each corner is computed from the center with its own angle a₀ + i × 2π ÷ n, so no error builds up round the polygon. Corners that should share a coordinate, such as a square's, agree only to the rounding of cos and sin. The number of sides is Math.round of the typed number and must be 3 to 1024.
commands.js
lines 781 to 791
CadCraft 0.8.2
const sides = Math.round(n);
if (!(sides >= 3 && sides <= 1024)) { c.print(t('cmd.polygon.requires_whole_number_3')); return; }
c.engine.lastSides = sides;
const center = await c.point(t('cmd.polygon.specify_center_polygon'));
const k = await c.keyword(t('cmd.mea.option'), { options: [opt('I', t('cmd.polygon.inscribed_circle')), opt('C', t('cmd.polygon.circumscribed_about_circle'))], default: c.engine.lastPolyMode || 'I' });
const mode = (k && k.option) || (typeof k === 'string' ? k : 'I');
c.engine.lastPolyMode = mode;
const make = (cur) => {
const r = dist(center, cur), a = angleOf(center, cur);
const R = mode === 'C' ? r / Math.cos(Math.PI / sides) : r, a0 = mode === 'C' ? a + Math.PI / sides : a;
return { type: 'poly', closed: true, pts: Array.from({ length: sides }, (_, i) => ({ ...polar(center, a0 + i * TAU / sides, R), b: 0 })) };
Ellipse from an axis and a half-width
64-bit
The center, major-axis vector and axis ratio of an ellipse.
ratio = half-width ÷ L, kept in 10⁻⁶…1; if half-width d > L: major = d·(−m_y, mₓ) ÷ L, ratio = L ÷ d
L is Math.hypot of the major-axis vector; an axis under 1e-12 (absolute) is refused. A half-width longer than the first axis swaps the roles, with the new major vector built from two divisions by L. The ratio is clamped to at least 1e-6.
commands.js
lines 810 to 819
CadCraft 0.8.2
const L = Math.hypot(major.x, major.y);
if (L < 1e-12) return;
const make = (half) => ({ type: 'ellipse', cx: center.x, cy: center.y, mx: major.x, my: major.y, ratio: Math.min(Math.max(half / L, 1e-6), 1), t0: 0, t1: TAU });
c.preview((cur) => [make(Math.abs(((cur.x - center.x) * -major.y + (cur.y - center.y) * major.x) / L))]);
const d = await c.distance(t('cmd.ellipse.specify_distance_other_axis'), { base: center });
if (!d) return;
if (d > L) { // the other axis is the longer one: swap so the ratio stays ≤ 1
const u = { x: -major.y / L * d, y: major.x / L * d };
c.add(ent(c, { type: 'ellipse', cx: center.x, cy: center.y, mx: u.x, my: u.y, ratio: L / d, t0: 0, t1: TAU }));
} else c.add(ent(c, make(d)));
Construction line at an angle
64-bit
The direction of a construction line given by a typed angle, alone or measured from a picked straight object.
direction = (cos a, sin a); with a reference: a = atan2(u_y, uₓ) + typed angle
The direction is stored as the cosine and sine of the angle, so a line typed at 90° has dx = cos(π ÷ 2), about 6 × 10⁻¹⁷, rather than 0. The Hor and Ver options store (1, 0) and (0, 1) exactly. With a reference, the object's direction goes through atan2 and back through cos and sin.
commands.js
lines 695 to 697
CadCraft 0.8.2
ang = Math.atan2(u.dy, u.dx) + (typeof r === 'number' ? r : 0);
}
return throughPoints(c, 'xline', fixed({ dx: Math.cos(ang), dy: Math.sin(ang) }));
Construction line that bisects an angle
64-bit
The direction of the line through a vertex that halves the angle between two legs.
b = unit(u₀ + u₁), u₀, u₁ the unit vectors of the legs
Each leg is made a unit vector by geom.js unitDir and the bisector is the unit vector of their sum. For legs that point nearly opposite ways the sum is a small difference of two unit vectors and loses digits; when it is shorter than 1e-9 the perpendicular to the first leg is used.
The point and direction of a construction line parallel to a picked straight object.
foot(q) = on + ((q − on)·u)·u; new point = foot(pick) + d·n, n = unit(side − foot(side))
The side direction n is the unit vector from the side point's foot on the line to that point, so it is perpendicular to the line only to within the rounding of the projection. A side point closer than 1e-9 to the line is refused. With the Through option the line passes exactly through the picked point.
Where each further line of TEXT is placed below the first.
line k at start + k × 1.667 × h, at right angles to the text direction
Each further line is placed 1.667 × the text height below the last; 1.667 is a rounded constant written in the code. The offset is a running sum of h × 1.667 and the position goes through cos and sin of the rotation − 90°.
commands.js
lines 848 to 850
CadCraft 0.8.2
const pos = polar(at, (rot || 0) - Math.PI / 2, y);
c.add(ent(c, { type: 'text', x: pos.x, y: pos.y, h, rot: normAngle(rot || 0), s: line, ha, va, wf: 1 }));
y += h * 1.667; // DTEXT line spacing
Paragraph text box
64-bit
The box between two picked corners, measured along the text's own direction, and the width and insertion point stored from it.
lx = d·u; ly = d·n, u = (cos rot, sin rot), n = (−sin rot, cos rot)
The second corner's offset is projected on the text direction and its normal by two dot products. The stored width is right − left and the insertion point is the box corner or middle for the chosen justification, turned back to world axes; a width of 1e-9 or less is stored as no width.
commands.js
lines 925 to 954
CadCraft 0.8.2
const frame = (b) => { // the box from corner a to b, in the text's own direction
const d = { x: b.x - a.x, y: b.y - a.y }, lx = d.x * u().x + d.y * u().y, ly = d.x * n().x + d.y * n().y;
return { left: Math.min(0, lx), right: Math.max(0, lx), top: Math.max(0, ly), bottom: Math.min(0, ly) };
};
const boxPoly = (f) => ({ type: 'poly', closed: true, pts: [[f.left, f.top], [f.right, f.top], [f.right, f.bottom], [f.left, f.bottom]].map(([x, y]) => ({ x: a.x + u().x * x + n().x * y, y: a.y + u().y * x + n().y * y, b: 0 })) });
let b = null;
for (;;) {
c.preview((cur) => [boxPoly(frame(cur))]);
const p = await c.point(t('cmd.mtext.corner'), { base: a, options: [opt('H', t('opt.height')), opt('J', t('opt.justify')), opt('L', t('opt.linespacing')), opt('R', t('opt.rotation')), opt('W', t('opt.width'))] });
if (p.option === 'H') { const h = await c.distance(t('cmd.mtext.height'), { base: a, default: S.h }); if (h > 0) S.h = h; continue; }
if (p.option === 'J') {
const k = await c.keyword(t('cmd.mtext.just'), { options: Object.keys(MTEXT_JUST).map((key) => opt(key, key)), default: S.just });
const key = k?.option || k; if (MTEXT_JUST[key]) S.just = E.mtextJust = key; continue;
}
if (p.option === 'L') { const f = await c.number(t('cmd.mtext.ls'), { default: S.ls }); if (f >= 0.25 && f <= 4) S.ls = E.mtextLs = f; else c.print(t('cmd.mtext.ls.bad')); continue; }
if (p.option === 'R') { const r = await c.angle(t('cmd.mtext.rot'), { base: a, default: S.rot, ucs: true }); S.rot = normAngle(r || 0); continue; }
if (p.option === 'W') { const w = await c.distance(t('cmd.mtext.width'), { base: a, default: 0 }); S.width = w || 0; break; }
b = p; break;
}
c.preview(null);
const [ha, va] = MTEXT_JUST[S.just];
let at = a, ww = S.width ?? 0;
if (b) {
const f = frame(b);
ww = f.right - f.left;
const lx = ha === 'left' ? f.left : ha === 'right' ? f.right : (f.left + f.right) / 2;
const ly = va === 'top' ? f.top : va === 'bottom' ? f.bottom : (f.top + f.bottom) / 2;
at = { x: a.x + u().x * lx + n().x * ly, y: a.y + u().y * lx + n().y * ly };
}
const e = ent(c, { type: 'text', x: at.x, y: at.y, h: S.h, rot: S.rot, s: '', ha, va, wf: 1, ...(ww > 1e-9 ? { ww } : {}), ...(S.ls !== 1 ? { ls: S.ls } : {}) });
In detail
Method
Circumcenter by the determinant formula; polyline arc bulge = tan(φ ÷ 2), φ being the turn from the previous segment's end direction to the chord; polygon corners at equal angle steps from the center; ellipse stored as center, major-axis vector and ratio.
Number types
64-bit numbers.
Tolerances
1e-12, absolute: three points are in line when |d|, four times the area of their triangle, is under this; a segment, a rectangle side or a major axis shorter than this is refused. The ellipse ratio is kept between 1e-6 and 1. geom.js unitDir refuses a direction shorter than 1e-9.
Limits
circle3 squares the absolute coordinates: for points a distance s apart at coordinates of size M the center's error grows as M² × 10⁻¹⁶ ÷ s. The in-line test is absolute (square drawing units), so it is strict for large figures and loose for very small ones. DTEXT lines are spaced by the rounded factor 1.667.
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.