Resources Numerical accuracy Calculation policy

Shapes made by the draw commands

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.

commands.js CadCraft 0.8.2 64-bit 13 calculations Reviewed

Tests bin/test-cadcraft.mjs

Circle through three points

64-bit

The center and radius of the circle through three picked points.

d = 2·(aₓ(b_y − c_y) + bₓ(c_y − a_y) + cₓ(a_y − b_y)); centerₓ = (|a|²(b_y − c_y) + |b|²(c_y − a_y) + |c|²(a_y − b_y)) ÷ d; r = |a − center|

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.

  • commands.js
  • lines 3983 to 3990
  • CadCraft 0.8.2
export function circle3(a, b, c) {
  const d = 2 * (a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y));
  if (Math.abs(d) < 1e-12) return null;
  const a2 = a.x * a.x + a.y * a.y, b2 = b.x * b.x + b.y * b.y, c2 = c.x * c.x + c.y * c.y;
  const cx = (a2 * (b.y - c.y) + b2 * (c.y - a.y) + c2 * (a.y - b.y)) / d;
  const cy = (a2 * (c.x - b.x) + b2 * (a.x - c.x) + c2 * (b.x - a.x)) / d;
  return { cx, cy, r: Math.hypot(a.x - cx, a.y - cy) };
}

Arc through three points

64-bit

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.

  • commands.js
  • lines 619 to 623
  • CadCraft 0.8.2
if (r0.option === '2P') {
  const a = await c.point(t('cmd.circle.specify_first_end_point'));
  c.preview((cur) => [{ type: 'circle', cx: (a.x + cur.x) / 2, cy: (a.y + cur.y) / 2, r: dist(a, cur) / 2 }]);
  const b = await c.point(t('cmd.circle.specify_second_end_point'), { base: a });
  c.add(ent(c, { type: 'circle', cx: (a.x + b.x) / 2, cy: (a.y + b.y) / 2, r: dist(a, b) / 2 }));

Arc from center, start and end

64-bit

The radius and end angles of an arc given its center, its start point and a point toward its end.

r = |start − center|; a₀ = atan2(start − center); a₁ = atan2(end − center)

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.

  • commands.js
  • lines 771 to 776
  • CadCraft 0.8.2
def('RECTANG', ['REC', 'RECTANGLE'], async (c) => {
  const a = await c.point(t('cmd.area.first'));
  c.preview((cur) => [rect(a, cur)]);
  const b = await c.point(t('cmd.rectang.specify_other_corner_point'), { base: a });
  if (Math.abs(b.x - a.x) < 1e-12 || Math.abs(b.y - a.y) < 1e-12) { c.print(t('cmd.rectang.rectangle_needs_width_height')); return; }
  c.add(ent(c, rect(a, b)));

Regular polygon

64-bit

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.

  • commands.js
  • lines 705 to 709
  • CadCraft 0.8.2
const half = (p) => {
  const u1 = unitDir(p.x - v.x, p.y - v.y);
  if (!u1) return null;
  const b = unitDir(u0.dx + u1.dx, u0.dy + u1.dy) || { dx: -u0.dy, dy: u0.dx };   // opposite rays: the perpendicular
  return { x: v.x, y: v.y, ...b };

Construction line parallel at a distance

64-bit

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.

  • commands.js
  • lines 730 to 741
  • CadCraft 0.8.2
const foot = (q) => { const k = (q.x - on.x) * u.dx + (q.y - on.y) * u.dy; return { x: on.x + u.dx * k, y: on.y + u.dy * k }; };
if (through) {
  c.preview((cur) => [{ type: 'xline', x: cur.x, y: cur.y, ...u }]);
  const p = await c.point(t('cmd.xline.through'));
  c.add(ent(c, { type: 'xline', x: p.x, y: p.y, ...u }));
} else {
  const f = foot(pick.point);
  c.preview((cur) => { const g = foot(cur), n = unitDir(cur.x - g.x, cur.y - g.y); return n ? [{ type: 'xline', x: f.x + n.dx * d, y: f.y + n.dy * d, ...u }] : []; });
  const side = await c.point(t('cmd.xline.side'));
  const g = foot(side), n = unitDir(side.x - g.x, side.y - g.y);
  if (!n) { c.print(t('cmd.xline.side_on_line')); continue; }
  c.add(ent(c, { type: 'xline', x: f.x + n.dx * d, y: f.y + n.dy * d, ...u }));

Lines of single-line text

64-bit

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.

Every policy that takes this approach