Resources Numerical accuracy Calculation policy

Ellipses and curves as short segments

Turns ellipses, sheared arcs, splines and outlines into runs of short straight segments, and measures ellipses.

How the precision is kept

An ellipse is handled as a polyline of 72 chords per full turn, so its picks, snaps, intersections and bounds are those of the polyline and not of the true curve. The number of chords sets the accuracy; the arithmetic at each point is 64-bit. A full ellipse's perimeter uses a closed-form approximation.

Open item. Intersect and snap to the ellipse itself instead of to its 72-chord polyline.

Calculations under this policy

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

geom.js CadCraft 0.8.2 64-bit 7 calculations Reviewed

Tests bin/test-cadcraft.mjs

Points along an ellipse

64-bit

The points that stand in for an ellipse or elliptical arc.

n = max(8, ⌈steps × sweep ÷ 2π⌉); t = t0 + (t1 − t0)·i ÷ n; p = c + R(rot)·(a·cos t, a·ratio·sin t)

Each point is one 64-bit expression; the limit is the step. With the default 72 steps per turn every chord subtends 5° of the parameter, and the polyline lies inside the true curve by up to about 0.1% of the major radius. The major radius is Math.hypot of the stored axis vector and the rotation is atan2 of it.

  • geom.js
  • lines 229 to 239
  • CadCraft 0.8.2
export function ellipsePoints(e, steps = ELLIPSE_STEPS) {
  const ma = Math.hypot(e.mx, e.my), mi = ma * e.ratio, rot = Math.atan2(e.my, e.mx);
  const [t0, t1] = e.t1 - e.t0 >= TAU - EPS ? [0, TAU] : arcRange(e.t0, e.t1);
  const n = Math.max(8, Math.ceil(steps * (t1 - t0) / TAU));
  const c = Math.cos(rot), s = Math.sin(rot), pts = [];
  for (let i = 0; i <= n; i++) {
    const t = t0 + (t1 - t0) * i / n, x = ma * Math.cos(t), y = mi * Math.sin(t);
    pts.push({ x: e.cx + x * c - y * s, y: e.cy + x * s + y * c });
  }
  return pts;
}

Ellipse area and perimeter

64-bit

The area and perimeter of an ellipse, or the area and length of an elliptical arc.

full: area = π·a·b; perimeter ≈ π(a + b)(1 + 3h ÷ (10 + √(4 − 3h))), h = ((a − b) ÷ (a + b))²; arc: 720 chords per turn

The area of a full ellipse is exact in form. Its perimeter is a closed-form approximation and not the elliptic integral, so it is not exact even with exact arithmetic. For an elliptical arc both numbers come from a polygon of 720 chords per full turn: the length is the sum of the chords, and the area is that of the polygon closed straight from its last point to its first.

  • geom.js
  • lines 1557 to 1565
  • CadCraft 0.8.2
const a = Math.hypot(e.mx, e.my), b = a * e.ratio;
if (e.t1 - e.t0 >= TAU - EPS || Math.abs(normAngle(e.t1 - e.t0)) < EPS) {
  const h = ((a - b) / (a + b)) ** 2;
  return { area: Math.PI * a * b, perimeter: Math.PI * (a + b) * (1 + 3 * h / (10 + Math.sqrt(4 - 3 * h))), kind: 'closed' };
}
const pts = ellipsePoints(e, 720), m = polygonMeasure(pts);
let len = 0;
for (let i = 0; i + 1 < pts.length; i++) len += dist(pts[i], pts[i + 1]);
return { area: Math.abs(m.area), perimeter: len, kind: 'open' };

A piece through a transform

64-bit

A line or arc piece of a block put through the block reference's transform.

uniform: center M·c, radius r·s, start angle = atan2(M·start − M·c); otherwise n = max(8, ⌈48 × sweep ÷ 2π⌉) chords

Under a uniform scale and rotation an arc stays an arc: its start angle is recomputed with atan2 and the sweep is carried over unchanged. Under a transform that stretches or shears, the arc is replaced by straight chords at 48 per full turn (at least 8), so a circle in an unevenly scaled block is a polygon for picks, snaps and intersections.

  • geom.js
  • lines 402 to 418
  • CadCraft 0.8.2
/** A primitive through a transform (arcs become polylines when the transform is not similar). */
export function txPrim(p, m) {
  if (p.k === 'L') { const a = mApply(m, { x: p.x1, y: p.y1 }), b = mApply(m, { x: p.x2, y: p.y2 }); return [{ k: 'L', x1: a.x, y1: a.y, x2: b.x, y2: b.y }]; }
  const sim = mSimilar(m);
  if (sim) {
    const c = mApply(m, { x: p.cx, y: p.cy });
    if (p.full) return [{ k: 'A', cx: c.x, cy: c.y, r: p.r * sim.s, a0: 0, a1: TAU, full: true }];
    // A mirror reverses the direction of travel: the image of the old end is the new CCW start.
    const s = mApply(m, primPoint(p, sim.flip ? 1 : 0));
    const a0 = normAngle(Math.atan2(s.y - c.y, s.x - c.x));
    return [{ k: 'A', cx: c.x, cy: c.y, r: p.r * sim.s, a0, a1: a0 + (p.a1 - p.a0) }];
  }
  const out = [], n = Math.max(8, Math.ceil(48 * (p.a1 - p.a0) / TAU));
  let prev = mApply(m, primPoint(p, 0));
  for (let i = 1; i <= n; i++) { const q = mApply(m, primPoint(p, i / n)); out.push({ k: 'L', x1: prev.x, y1: prev.y, x2: q.x, y2: q.y }); prev = q; }
  return out;
}

Outline of an object as points

64-bit

The outline a revision cloud follows, with arcs and curves in short pieces.

A circle gives 96 points, an arc one point per 1 ÷ 96 of a turn (at least 8 steps), a polyline arc segment the same (at least 4), an ellipse 144 steps per turn, and a spline the points of splinePolyline(e, 24). Points are computed from center, radius and angle.

  • geom.js
  • lines 1194 to 1212
  • CadCraft 0.8.2
/** An object's outline as points (arcs and curves in short pieces): { pts, closed } | null. */
export function pathOf(e) {
  if (e.type === 'circle') { const n = 96, pts = []; for (let i = 0; i < n; i++) pts.push(polar({ x: e.cx, y: e.cy }, (i / n) * TAU, e.r)); return { pts, closed: true }; }
  if (e.type === 'ellipse') { const pts = ellipsePoints(e, 144); const closed = e.t1 - e.t0 >= TAU - 1e-9; if (closed) pts.pop(); return { pts, closed }; }
  if (e.type === 'spline' && splineValid(e)) { const pts = splinePolyline(e, 24); return { pts, closed: !!e.closed }; }
  if (e.type === 'line') return { pts: [{ x: e.x1, y: e.y1 }, { x: e.x2, y: e.y2 }], closed: false };
  if (e.type === 'arc') { const p = prims(e)[0], n = Math.max(8, Math.ceil((p.a1 - p.a0) / (TAU / 96))), pts = []; for (let i = 0; i <= n; i++) pts.push(primPoint(p, i / n)); return { pts, closed: false }; }
  if (e.type === 'poly' && e.pts.length >= 2) {
    const pts = [];
    for (const p of polyPrims(e)) {
      if (p.k === 'L') { pts.push({ x: p.x1, y: p.y1 }); continue; }
      const n = Math.max(4, Math.ceil((p.a1 - p.a0) / (TAU / 96)));
      for (let i = 0; i < n; i++) pts.push(primPoint(p, p.rev ? 1 - i / n : i / n));
    }
    if (!e.closed) pts.push({ x: e.pts[e.pts.length - 1].x, y: e.pts[e.pts.length - 1].y });
    return { pts, closed: !!e.closed };
  }
  return null;
}

Even spacing along a path

64-bit

Points spaced evenly along a run of points, about a given step apart.

n = max(1 or 3, round(L ÷ step)); target k = L·k ÷ n; point = linear interpolation on the segment that holds the target

The total length is the segment lengths added in order, and each target distance is L·k ÷ n. The walk along the segments uses a running sum and moves to the next segment only when the target is more than 1e-12 past the current one's end. Corners of the original path are not kept.

  • geom.js
  • lines 1214 to 1232
  • CadCraft 0.8.2
/** Points spaced evenly along a path (about `step` apart; corners need not be kept). */
export function resamplePath(pts, closed, step) {
  const ring = closed ? [...pts, pts[0]] : pts;
  const lens = [];
  let L = 0;
  for (let i = 0; i + 1 < ring.length; i++) { const d = dist(ring[i], ring[i + 1]); lens.push(d); L += d; }
  if (!(L > EPS) || !(step > EPS)) return pts.slice();
  const n = Math.max(closed ? 3 : 1, Math.round(L / step));
  const out = [];
  let seg = 0, acc = 0;
  for (let k = 0; k <= n; k++) {
    if (closed && k === n) break;
    const target = (L * k) / n;
    while (seg < lens.length - 1 && acc + lens[seg] < target - 1e-12) { acc += lens[seg]; seg++; }
    const t = lens[seg] > 0 ? (target - acc) / lens[seg] : 0;
    out.push({ x: ring[seg].x + (ring[seg + 1].x - ring[seg].x) * t, y: ring[seg].y + (ring[seg + 1].y - ring[seg].y) * t });
  }
  return out;
}

Revision cloud

64-bit

A polyline of equal arcs along a path, bulging outward.

bulge = ± tan(2π ÷ 3 ÷ 4) = ± tan 30°; outward side from the sign of Σ (xᵢ·yᵢ₊₁ − xᵢ₊₁·yᵢ)

Every scallop is an arc of 120° included angle between two resampled points, stored as the constant bulge tan(TAU ÷ 3 ÷ 4). The direction of a closed path is the sign of the raw-coordinate shoelace sum; only the sign is used.

  • geom.js
  • lines 1237 to 1248
  • CadCraft 0.8.2
/**
 * A revision cloud along a path: a polyline of arcs about `arcLen` long that bulge outward (inward with `reverse`).
 * → poly entity fields { type: 'poly', closed, pts }.
 */
export function revcloud(pts, closed, arcLen, reverse = false) {
  const q = resamplePath(pts, closed, arcLen);
  let a = 0;
  for (let i = 0; i < q.length; i++) { const u = q[i], v = q[(i + 1) % q.length]; a += u.x * v.y - v.x * u.y; }
  // travelling counter-clockwise the outside is on the right, where a positive bulge goes
  const sign = (closed ? (a >= 0 ? 1 : -1) : 1) * (reverse ? -1 : 1);
  return { type: 'poly', closed, pts: q.map((p, i) => ({ x: p.x, y: p.y, b: closed || i < q.length - 1 ? sign * CLOUD_BULGE : 0 })) };
}

Exploding a spline

64-bit

Replaces a spline with a polyline along the curve.

The spline is replaced by straight segments through the points of splinePolyline(e, 10) with all bulges 0. The curve definition is not kept, so the result follows the spline only as closely as that sampling.

  • geom.js
  • lines 1952 to 1956
  • CadCraft 0.8.2
if (e.type === 'spline') {   // a polyline along the curve (precision 10 per knot span)
  if (!splineValid(e)) return null;
  const pts = splinePolyline(e, 10);
  return pts.length >= 2 ? [{ type: 'poly', layer: e.layer, color: e.color ?? null, closed: !!e.closed, pts: pts.map((q) => ({ x: q.x, y: q.y, b: 0 })) }] : null;
}

In detail

Method
Points at equal steps of the ellipse parameter; arcs under a non-uniform transform at 48 chords per turn; outlines for revision clouds at 96 (circles, arcs), 144 (ellipses) chords per turn. Full-ellipse perimeter by π(a + b)(1 + 3h ÷ (10 + √(4 − 3h))), known as Ramanujan's second approximation.
Number types
64-bit numbers.
Tolerances
An ellipse counts as closed when t1 − t0 ≥ 2π − 1e-9. Resampling uses a 1e-12 absolute allowance when it walks along the path.
Limits
A 5° chord of a circle of radius r lies r × (1 − cos 2.5°) ≈ 0.00095 r inside it at its middle, so a 72-chord ellipse is off by up to about 0.1% of its major radius. An exploded spline becomes a straight-segment polyline.

Approach: 64-bit, stated limit

The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.

Every policy that takes this approach