Resources Numerical accuracy Calculation policy

2D curve offsets for toolpaths

Offsets outlines made of lines and arcs by the tool radius, intersects and trims them, and measures their lengths, areas and distances.

How the precision is kept

Curves stay lines and circular arcs throughout, in 64-bit; nothing is faceted. An offset moves lines sideways and changes arc radii, then every piece is cut at its intersections and kept only if its midpoint is at the offset distance from the original outline. Decisions use fixed tolerances in millimeters and are made in ordinary 64-bit arithmetic, not with exact predicates.

Calculations under this policy

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

cam-offset.js PartCraft 0.8.1 64-bit 18 calculations Reviewed

Tests tools/partcraft-test/test-cam.mjs

Length of a line or arc

64-bit

The length of one piece of an outline or toolpath.

line: |e − s|; arc: |sweep| × r

One Math.hypot or one product. The arc length uses the stored radius and sweep, not the end points.

  • cam-offset.js
  • line 46
  • PartCraft 0.8.1
export const lengthOf = (p) => (p.k === 'L' ? dist(p.s, p.e) : Math.abs(p.sw) * p.r);

Point along a piece

64-bit

The point a given fraction of the way along a line or arc.

line: s + (e − s) t; arc: c + r (cos, sin)(a₀ + sweep × t)

At t ≤ 0 and t ≥ 1 the stored end points are returned as they are, so shared ends stay identical. In between, an arc point is computed from the center and radius.

  • cam-offset.js
  • lines 47 to 50
  • PartCraft 0.8.1
export function pointAt(p, t) {
  if (t <= 0) return p.s; if (t >= 1) return p.e;
  return p.k === 'L' ? [p.s[0] + (p.e[0] - p.s[0]) * t, p.s[1] + (p.e[1] - p.s[1]) * t] : polar(p.c, p.r, p.a0 + p.sw * t);
}

Is an angle on an arc

64-bit

Whether a direction from the center falls within an arc, and how far along.

u = mod(±(a − a₀), 2π); on the arc when u ≤ |sweep| + eps; t = u ÷ |sweep|

The tolerance eps is in radians (1e-9 by default), so the allowed overshoot in millimeters grows with the radius; callers that need a length tolerance pass eps divided by the radius.

  • cam-offset.js
  • lines 67 to 74
  • PartCraft 0.8.1
/** Is angle a on the arc? → parameter t in [0, 1] or −1. */
export function arcParam(p, a, eps = EPS) {
  const span = Math.abs(p.sw);
  if (span >= TAU - eps) return mod((a - p.a0) * Math.sign(p.sw), TAU) / TAU;
  let u = p.sw > 0 ? mod(a - p.a0, TAU) : mod(p.a0 - a, TAU);
  if (u > span + eps) { if (u > TAU - eps) u = 0; else return -1; }
  return Math.min(1, u / span);
}

Distance from a point to a line or arc

64-bit

The shortest distance from a point to one piece of an outline.

line: |q − (s + d × clamp(((q − s) · d) ÷ |d|², 0, 1))|; arc: ||q − c| − r| when the direction is on the arc, else the nearer end

For an arc the distance is a difference |ρ − r| of two lengths, so its absolute error is the rounding of the larger one. A zero-length line is handled without dividing.

  • cam-offset.js
  • lines 76 to 86
  • PartCraft 0.8.1
/** Closest distance from point q to a prim. */
export function distToPrim(q, p) {
  if (p.k === 'L') {
    const d = sub(p.e, p.s), l2 = dot(d, d);
    const t = l2 > 0 ? Math.max(0, Math.min(1, dot(sub(q, p.s), d) / l2)) : 0;
    return dist(q, [p.s[0] + d[0] * t, p.s[1] + d[1] * t]);
  }
  const v = sub(q, p.c), rho = len(v);
  if (rho < 1e-300 || arcParam(p, Math.atan2(v[1], v[0]), 1e-12) >= 0) return Math.abs(rho - p.r);
  return Math.min(dist(q, p.s), dist(q, p.e));
}

Signed area of a loop

64-bit

The area enclosed by a loop of lines and arcs, positive when counterclockwise.

A = ½ Σ [line: s × e; arc: r c_x (sin a₁ − sin a₀) − r c_y (cos a₁ − cos a₀) + r² × sweep]

Green's theorem with the exact arc term, so arcs are not approximated. The terms use absolute coordinates and are added in order; for a small loop far from the origin they are large and nearly cancel.

  • cam-offset.js
  • lines 123 to 134
  • PartCraft 0.8.1
/** Signed area (counter-clockwise positive), exact for arcs. */
export function loopArea(l) {
  let s = 0;
  for (const p of l) {
    if (p.k === 'L') s += crs(p.s, p.e);
    else {
      const a1 = p.a0 + p.sw;
      s += p.r * p.c[0] * (Math.sin(a1) - Math.sin(p.a0)) - p.r * p.c[1] * (Math.cos(a1) - Math.cos(p.a0)) + p.r * p.r * p.sw;
    }
  }
  return s / 2;
}

Winding number

64-bit

How many times the loops wind round a point, which says whether the point is inside.

count signed crossings of a ray to +x; arc crossing at x = c_x ± √(r² − (q_y − c_y)²)

Side tests are signs of 64-bit cross products, not exact predicates, so a point within rounding of the boundary can be counted either way. The square root's argument is clamped at 0.

  • cam-offset.js
  • lines 136 to 158
  • PartCraft 0.8.1
/** Winding number of the loops about q (horizontal ray to +x; arcs split at their top and bottom). */
export function winding(loops, q) {
  let w = 0;
  const seg = (a, b) => {
    if (a[1] <= q[1] && b[1] > q[1]) { if (crs(sub(b, a), sub(q, a)) > 0) w++; }
    else if (b[1] <= q[1] && a[1] > q[1]) { if (crs(sub(b, a), sub(q, a)) < 0) w--; }
  };
  for (const l of loops) for (const p of l) {
    if (p.k === 'L') { seg(p.s, p.e); continue; }
    // monotone pieces of the arc between the angles π/2 + kπ
    const pieces = monotonePieces(p);
    for (const m of pieces) {
      const ys = m.s[1], ye = m.e[1];
      const up = ye > ys;
      if (!(up ? (ys <= q[1] && ye > q[1]) : (ye <= q[1] && ys > q[1]))) continue;
      const dy = q[1] - p.c[1];
      const h = Math.sqrt(Math.max(0, p.r * p.r - dy * dy));
      const x = p.c[0] + (Math.cos(m.a0 + m.sw / 2) >= 0 ? h : -h);
      if (x > q[0]) w += up ? 1 : -1;
    }
  }
  return w;
}

Line and line

64-bit

The point where two line pieces cross, or the ends of their overlap when they lie on one line.

t_a = ((B.s − A.s) × s) ÷ (r × s); parallel when |r × s| ≤ 1e-12 |r| |s|

The parallel test is relative to the two lengths. For nearly parallel lines the divisor is small and the crossing point loses digits in proportion. Parameters are accepted up to 1e-9 plus 1e-8 mm beyond the ends and then clamped.

  • cam-offset.js
  • lines 198 to 216
  • PartCraft 0.8.1
function llx(A, B) {
  const r = sub(A.e, A.s), s = sub(B.e, B.s);
  const den = crs(r, s), qp = sub(B.s, A.s);
  const lr = len(r), ls = len(s);
  if (lr < 1e-15 || ls < 1e-15) return [];
  if (Math.abs(den) <= 1e-12 * lr * ls) {
    if (Math.abs(crs(qp, r)) / lr > TOL * 0.1) return [];
    // collinear: overlap ends
    const out = [];
    const tA = (q) => dot(sub(q, A.s), r) / (lr * lr), tB = (q) => dot(sub(q, B.s), s) / (ls * ls);
    for (const q of [B.s, B.e]) { const t = tA(q); if (t > -EPS && t < 1 + EPS) out.push({ ta: clamp01(t), tb: tB(q), p: q }); }
    for (const q of [A.s, A.e]) { const t = tB(q); if (t > -EPS && t < 1 + EPS) out.push({ ta: tA(q), tb: clamp01(t), p: q }); }
    return out.map((x) => ({ ta: clamp01(x.ta), tb: clamp01(x.tb), p: x.p }));
  }
  const ta = crs(qp, s) / den, tb = crs(qp, r) / den;
  const ea = EPS + TOL * 0.01 / lr, eb = EPS + TOL * 0.01 / ls;
  if (ta < -ea || ta > 1 + ea || tb < -eb || tb > 1 + eb) return [];
  return [{ ta: clamp01(ta), tb: clamp01(tb), p: [A.s[0] + r[0] * ta, A.s[1] + r[1] * ta] }];
}

Line and arc

64-bit

The points where a line piece meets an arc.

a t² + b t + c = 0; t = (−b ± √(b² − 4ac)) ÷ 2a

The textbook quadratic formula: when b² is much larger than 4ac one of the two roots subtracts nearly equal numbers and loses digits. A discriminant slightly below zero (a miss of less than about 1e-8 mm) is treated as a tangent point.

  • cam-offset.js
  • lines 218 to 238
  • PartCraft 0.8.1
function lax(L, C) {
  const d = sub(L.e, L.s), f = sub(L.s, C.c);
  const a = dot(d, d), b = 2 * dot(f, d), c = dot(f, f) - C.r * C.r;
  if (a < 1e-30) return [];
  let disc = b * b - 4 * a * c;
  const ld = Math.sqrt(a);
  // tangent within tolerance: one touching point
  if (disc < 0) { if (disc > -4 * a * (2 * C.r * TOL * 0.01)) disc = 0; else return []; }
  const sq = Math.sqrt(disc);
  const ts = disc === 0 ? [-b / (2 * a)] : [(-b - sq) / (2 * a), (-b + sq) / (2 * a)];
  const out = [];
  const e = EPS + TOL * 0.01 / ld;
  for (const t of ts) {
    if (t < -e || t > 1 + e) continue;
    const p = pointAt(L, clamp01(t));
    const tb = arcParam(C, Math.atan2(p[1] - C.c[1], p[0] - C.c[0]), EPS + TOL * 0.01 / C.r);
    if (tb < 0) continue;
    out.push({ ta: clamp01(t), tb, p });
  }
  return out;
}

Arc and arc

64-bit

The points where two arcs meet.

a = (r_A² − r_B² + d²) ÷ 2d; h = √(r_A² − a²); p = c_A + a u ± h n

The terms r_A² − a² subtract nearly equal numbers when the circles are close to tangent, so h loses digits there; a negative value is taken as 0 and h below 1e-12 gives one point. Circles whose centers are within 1e-12 × max(1, r) are treated as concentric.

  • cam-offset.js
  • lines 239 to 261
  • PartCraft 0.8.1
function aax(A, B) {
  const d = dist(A.c, B.c);
  const out = [];
  if (d < 1e-12 * Math.max(1, A.r)) {
    if (Math.abs(A.r - B.r) > TOL * 0.1) return [];
    // concentric, same radius: overlap ends
    for (const q of [B.s, B.e]) { const t = arcParam(A, Math.atan2(q[1] - A.c[1], q[0] - A.c[0])); if (t >= 0) out.push({ ta: t, tb: q === B.s ? 0 : 1, p: q }); }
    for (const q of [A.s, A.e]) { const t = arcParam(B, Math.atan2(q[1] - B.c[1], q[0] - B.c[0])); if (t >= 0) out.push({ ta: q === A.s ? 0 : 1, tb: t, p: q }); }
    return out;
  }
  if (d > A.r + B.r + TOL * 0.01 || d < Math.abs(A.r - B.r) - TOL * 0.01) return [];
  const a = (A.r * A.r - B.r * B.r + d * d) / (2 * d);
  const h2 = A.r * A.r - a * a;
  const h = h2 > 0 ? Math.sqrt(h2) : 0;
  const u = mul(sub(B.c, A.c), 1 / d), m = add(A.c, mul(u, a)), n = left(u);
  const pts = h > 1e-12 ? [add(m, mul(n, h)), sub(m, mul(n, h))] : [m];
  for (const p of pts) {
    const ta = arcParam(A, Math.atan2(p[1] - A.c[1], p[0] - A.c[0]), EPS + TOL * 0.01 / A.r);
    const tb = arcParam(B, Math.atan2(p[1] - B.c[1], p[0] - B.c[0]), EPS + TOL * 0.01 / B.r);
    if (ta >= 0 && tb >= 0) out.push({ ta, tb, p });
  }
  return out;
}

Raw offset of a loop

64-bit

Moves every piece of a loop sideways by the offset and joins the corners with arcs.

line: s, e + d × left normal; arc: r ∓ d; corner arc radius d, sweep = atan2(t_a × t_b, t_a · t_b)

An offset arc keeps its center and angles and changes only its radius, so no fitting is involved. The corner turn uses atan2 of the cross and dot products of the tangents. Arcs whose new radius is within 1e-9 of zero are left out.

  • cam-offset.js
  • lines 265 to 290
  • PartCraft 0.8.1
/** Raw offset pieces of one loop, d to its left (continuous closed curve). */
function rawOffset(loop, d) {
  const out = [];
  const n = loop.length;
  for (let i = 0; i < n; i++) {
    const p = loop[i];
    const t0 = tangentAt(p, 0), t1 = tangentAt(p, 1);
    const s = add(p.s, mul(left(t0), d)), e = add(p.e, mul(left(t1), d));
    if (p.k === 'L') out.push(line(s, e));
    else {
      const r2 = p.sw > 0 ? p.r - d : p.r + d;
      if (r2 > 1e-9) out.push(arc(p.c, r2, p.a0, p.sw, s, e));
      else if (r2 < -1e-9) out.push(arc(p.c, -r2, p.a0 + Math.PI, p.sw, s, e));
    }
    // corner to the next prim
    const q = loop[(i + 1) % n];
    const ta = t1, tb = tangentAt(q, 0);
    const turn = Math.atan2(crs(ta, tb), dot(ta, tb));
    if (Math.abs(turn) > 1e-12) {
      const v = p.e;
      const na = left(ta);
      out.push(arc(v, d, Math.atan2(na[1], na[0]), turn, e, add(q.s, mul(left(tb), d))));
    }
  }
  return out.filter((p) => lengthOf(p) > 1e-12);
}

Splitting pieces at intersections

64-bit

Cuts every offset piece where it meets another.

cut at parameter t unless within 1e-5 mm of an end or of the previous cut

Both pieces are cut at the same point object, so the two sides share identical coordinates. Cuts closer than JOIN (1e-5 mm) to an end are ignored.

  • cam-offset.js
  • lines 292 to 318
  • PartCraft 0.8.1
/** Split every piece at its intersections with the others → sub-pieces. */
export function splitAll(pieces) {
  const boxes = pieces.map(boxOf);
  const cuts = pieces.map(() => []);
  const pad = JOIN;
  for (let i = 0; i < pieces.length; i++) {
    const bi = boxes[i];
    for (let j = i + 1; j < pieces.length; j++) {
      const bj = boxes[j];
      if (bi[0] > bj[2] + pad || bj[0] > bi[2] + pad || bi[1] > bj[3] + pad || bj[1] > bi[3] + pad) continue;
      for (const x of intersect(pieces[i], pieces[j])) { cuts[i].push([x.ta, x.p]); cuts[j].push([x.tb, x.p]); }
    }
  }
  const out = [];
  pieces.forEach((p, i) => {
    const L = lengthOf(p);
    const ts = cuts[i].filter(([t]) => t * L > JOIN && (1 - t) * L > JOIN).sort((a, b) => a[0] - b[0]);
    let t0 = 0, s = p.s;
    for (const [t, q] of ts) {
      if ((t - t0) * L <= JOIN) continue;
      out.push(subPrim(p, t0, t, s, q));
      t0 = t; s = q;
    }
    out.push(subPrim(p, t0, 1, s, p.e));
  });
  return out;
}

Offset of a region

64-bit

The outline at a fixed distance inside or outside a region, which is the path of the tool center.

keep a piece when dist(midpoint, original outline) ≥ d − 1e-6 and the midpoint is on the offset side

The kept pieces are exact lines and arcs at distance d. The trim decision is made at each piece's midpoint with an absolute slack of 1e-6 mm. An offset below 1e-12 returns the input unchanged.

  • cam-offset.js
  • lines 320 to 344
  • PartCraft 0.8.1
/**
 * Offset a region (loops oriented region-on-left) by d: d > 0 shrinks (moves into the region), d < 0 grows.
 * → loops of the offset region, oriented the same way. Empty when nothing is left.
 */
export function offsetRegion(loops, d, { tol = TOL } = {}) {
  if (!loops.length) return [];
  if (Math.abs(d) < 1e-12) return loops.map((l) => l.slice());
  const grow = d < 0, a = Math.abs(d);
  const src = grow ? loops.map(reverseLoop) : loops;
  const raw = [];
  for (const l of src) raw.push(...rawOffset(l, a));
  const parts = splitAll(raw);
  const kept = [];
  for (const p of parts) {
    const m = pointAt(p, 0.5);
    if (distToLoops(m, loops, a - tol) < a - tol) continue;
    if (inside(loops, m) === grow) continue;
    kept.push(p);
  }
  const clean = dropOpposed(kept);
  let out = chain(clean);
  out = out.map((l) => mergeLoop(l)).filter((l) => l.length && Math.abs(loopArea(l)) > 1e-10);
  if (grow) out = out.map(reverseLoop);   // built on the reversed loops: the grown region was on their right
  return out;
}

Chaining pieces into loops

64-bit

Joins the kept pieces end to start into closed loops.

next piece starts within 4e-5 mm of the current end; sharpest left turn wins; then next.start = current.end

Joints are made identical by replacing each start with the previous end. An arc keeps its center, radius and angles, so its stored start point can differ from the point those give by up to the 4e-5 mm that was bridged.

  • cam-offset.js
  • lines 367 to 404
  • PartCraft 0.8.1
/** Chain directed pieces end → start into closed loops (sharpest left turn at a junction). */
export function chain(pieces) {
  const used = new Array(pieces.length).fill(false);
  const loops = [];
  const G = JOIN * 10;
  const cell = (p) => [Math.round(p[0] / G), Math.round(p[1] / G)];
  const starts = new Map();
  pieces.forEach((p, i) => { const k = cell(p.s).join(','); if (!starts.has(k)) starts.set(k, []); starts.get(k).push(i); });
  const near = (q) => {
    const [a, b] = cell(q), out = [];
    for (let i = -1; i <= 1; i++) for (let j = -1; j <= 1; j++) for (const k of starts.get((a + i) + ',' + (b + j)) || []) if (!used[k] && dist(pieces[k].s, q) < JOIN * 4) out.push(k);
    return out;
  };
  const turn = (p, q) => { const a = tangentAt(p, 1), b = tangentAt(q, 0); return Math.atan2(crs(a, b), dot(a, b)); };
  for (let i0 = 0; i0 < pieces.length; i0++) {
    if (used[i0]) continue;
    const loop = [pieces[i0]];
    used[i0] = true;
    let closed = false;
    for (let guard = 0; guard < 100000; guard++) {
      const cur = loop[loop.length - 1];
      const home = dist(cur.e, loop[0].s) < JOIN * 4;
      let best = -1, bt = home ? turn(cur, loop[0]) : -Infinity;
      for (const j of near(cur.e)) { const t = turn(cur, pieces[j]); if (t > bt) { bt = t; best = j; } }
      if (best < 0) { closed = home; break; }
      used[best] = true;
      loop.push(pieces[best]);
    }
    if (!closed) continue;
    // make the joints exact: each prim starts where the previous ends
    for (let k = 0; k < loop.length; k++) {
      const p = loop[k], j = (k + 1) % loop.length, q = loop[j];
      if (p.e !== q.s) loop[j] = q.k === 'L' ? line(p.e, q.e) : arc(q.c, q.r, q.a0, q.sw, p.e, q === p ? p.e : q.e);
    }
    loops.push(loop);
  }
  return loops;
}

Merging collinear lines and co-circular arcs

64-bit

Joins neighboring pieces that lie on the same line or circle into one.

lines: |u × v| < 1e-10 and end within 1e-9 mm of the line; arcs: centers and radii within 1e-9 mm, same direction

A merged line runs from the first start to the last end, so intermediate points are dropped; a merged arc adds the sweeps. Pieces shorter than 1e-6 mm are removed first.

  • cam-offset.js
  • lines 406 to 427
  • PartCraft 0.8.1
/** Merge collinear lines and arcs on one circle; drop pieces shorter than the join tolerance. */
export function mergeLoop(loop) {
  let l = loop.filter((p) => lengthOf(p) > JOIN * 0.1);
  if (l.length < 2) return l;
  const can = (p, q) => {
    if (p.k !== q.k) return false;
    if (p.k === 'L') { const u = unit(sub(p.e, p.s)), v = unit(sub(q.e, q.s)); return dot(u, v) > 0 && Math.abs(crs(u, v)) < 1e-10 && distToLineInf(q.e, p) < 1e-9; }
    return dist(p.c, q.c) < 1e-9 && Math.abs(p.r - q.r) < 1e-9 && Math.sign(p.sw) === Math.sign(q.sw) && Math.abs(p.sw + q.sw) <= TAU + 1e-12;
  };
  const join = (p, q) => (p.k === 'L' ? line(p.s, q.e) : arc(p.c, p.r, p.a0, p.sw + q.sw, p.s, q.e));
  let changed = true;
  while (changed && l.length > 1) {
    changed = false;
    const out = [];
    for (const p of l) {
      if (out.length && can(out[out.length - 1], p)) { out[out.length - 1] = join(out[out.length - 1], p); changed = true; } else out.push(p);
    }
    if (out.length > 1 && can(out[out.length - 1], out[0])) { out[0] = join(out[out.length - 1], out[0]); out.pop(); changed = true; }
    l = out;
  }
  return l;
}

Splitting arcs to half turns

64-bit

Cuts arcs so none sweeps more than 180°, as G-code controls expect.

n = ceil(|sweep| ÷ π); piece i from a₀ + sweep × i ÷ n

Split angles are computed from the start each time. The first and last pieces keep the arc's stored end points; inner points come from the center and radius.

  • cam-offset.js
  • lines 449 to 463
  • PartCraft 0.8.1
/** Split arcs so none sweeps more than maxSweep (every control reads ≤ 180° arcs the same way). */
export function limitArcs(loop, maxSweep = Math.PI) {
  const out = [];
  for (const p of loop) {
    if (p.k !== 'A' || Math.abs(p.sw) <= maxSweep + 1e-12) { out.push(p); continue; }
    const n = Math.ceil(Math.abs(p.sw) / maxSweep - 1e-12);
    let s = p.s;
    for (let i = 0; i < n; i++) {
      const e = i === n - 1 ? p.e : polar(p.c, p.r, p.a0 + p.sw * (i + 1) / n);
      out.push(arc(p.c, p.r, p.a0 + p.sw * i / n, p.sw / n, s, e));
      s = e;
    }
  }
  return out;
}

Points along a piece at a chord tolerance

64-bit

Sample points no farther than a tolerance from an arc, for checks and drawing.

step angle = 2 acos(1 − tol ÷ r)

The acos argument is clamped at −1. For a tolerance much smaller than the radius, 1 − tol ÷ r is close to 1, where acos loses digits; this affects only how many points are taken.

  • cam-offset.js
  • lines 465 to 474
  • PartCraft 0.8.1
/** Points along a prim no farther than chord tolerance tol from it (for drawing and tests). */
export function samplePrim(p, tol = 0.01, maxStep = Infinity) {
  if (p.k === 'L') {
    const L = lengthOf(p), n = Math.max(1, Math.ceil(L / maxStep));
    return Array.from({ length: n + 1 }, (_, i) => pointAt(p, i / n));
  }
  const da = p.r > tol ? 2 * Math.acos(Math.max(-1, 1 - tol / p.r)) : Math.PI / 2;
  const n = Math.max(2, Math.ceil(Math.abs(p.sw) / da), Math.ceil(lengthOf(p) / maxStep));
  return Array.from({ length: n + 1 }, (_, i) => pointAt(p, i / n));
}

Bounding box of a piece

64-bit

The extent of a line or arc, including where an arc bulges past its ends.

ends, plus c ± r along each axis direction the arc passes

The four axis directions are tested with a 1e-12 radian tolerance; the extreme points are computed from the center and radius.

  • cam-offset.js
  • lines 107 to 116
  • PartCraft 0.8.1
/** Bounding box [minx, miny, maxx, maxy] of a prim (arcs: their end points plus the axis extremes they pass). */
export function boxOf(p) {
  let x0 = Math.min(p.s[0], p.e[0]), y0 = Math.min(p.s[1], p.e[1]), x1 = Math.max(p.s[0], p.e[0]), y1 = Math.max(p.s[1], p.e[1]);
  if (p.k === 'A') for (let k = 0; k < 4; k++) {
    if (arcParam(p, k * Math.PI / 2, 1e-12) < 0) continue;
    const q = polar(p.c, p.r, k * Math.PI / 2);
    x0 = Math.min(x0, q[0]); y0 = Math.min(y0, q[1]); x1 = Math.max(x1, q[0]); y1 = Math.max(y1, q[1]);
  }
  return [x0, y0, x1, y1];
}

Splitting a loop at distances along it

64-bit

Cuts a loop at given distances from its start, used for tabs and ramps.

t = (cut − length so far) ÷ piece length

The running length is a sum of piece lengths added in order. A cut within 1e-5 mm of a joint is moved to the joint.

  • cam-offset.js
  • lines 476 to 496
  • PartCraft 0.8.1
/** Split a loop at arc lengths (sorted, 0..total) → list of prim lists between them. */
export function splitAtLengths(loop, cuts) {
  const out = [[]];
  let acc = 0, ci = 0;
  for (const p0 of loop) {
    let p = p0, L = lengthOf(p), base = acc;
    while (ci < cuts.length && cuts[ci] < base + L - JOIN) {
      if (cuts[ci] <= base + JOIN) { if (out[out.length - 1].length) out.push([]); ci++; continue; }
      const t = (cuts[ci] - base) / L;
      const x = pointAt(p, t);
      out[out.length - 1].push(subPrim(p, 0, t, p.s, x));
      out.push([]);
      p = subPrim(p, t, 1, x, p.e);
      base = cuts[ci]; L = lengthOf(p);
      ci++;
    }
    out[out.length - 1].push(p);
    acc += lengthOf(p0);
  }
  return out;
}

In detail

Method
Raw offset plus trim by distance, as described in the file's header. Intersections in closed form: line–line by cross products, line–arc by a quadratic, arc–arc by the radical line. Winding number by ray crossing with arcs split into monotone pieces. Area by Green's theorem, exact for arcs.
Number types
64-bit numbers.
Tolerances
EPS = 1e-9 (curve parameter). TOL = 1e-6 mm (trim distance). JOIN = 1e-5 mm (points this close are one point); chaining accepts 4 × JOIN. Lines parallel when |sin| ≤ 1e-12, collinear within 1e-7 mm. A line missing an arc by less than about 1e-8 mm counts as touching. Loops of area below 1e-10 mm² are dropped. Pieces shorter than 1e-6 mm are dropped when merging.
Limits
Tolerances are absolute millimeters and do not scale with the size of the outline. The header gives the cost as quadratic in the number of pieces.

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