Resources Numerical accuracy Calculation policy

Fillet and chamfer

Rounds or cuts the corner between two objects, between two segments of one polyline, or at every corner of a polyline.

How the precision is kept

Corners are solved in closed form in 64-bit numbers. The corner angle comes from acos of the dot product of two unit directions, clamped to [−1, 1]. Fillets that involve an arc or circle intersect the two pieces offset by the radius. Size limits are absolute at 1e-9.

Calculations under this policy

9 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 9 calculations Reviewed

Tests bin/test-cadcraft.mjs

Fillet of two lines

64-bit

The arc of a given radius tangent to two lines and the lines cut back to it.

half = acos(ua·ub) ÷ 2; t = r ÷ tan(half); center = corner + bisector × r ÷ sin(half)

The corner is the intersection of the two lines taken as unbounded. The dot product is clamped to [−1, 1] before acos, and a half angle under 1e-6 rad or within 1e-6 of 90° is refused. The arc stores the radius that was asked for; its angles come from atan2 of the tangent points minus the center.

  • geom.js
  • lines 1158 to 1189
  • CadCraft 0.8.2
/** Fillet two lines with radius r (0 = sharp corner). picks say which side of each line to keep. */
export function filletLines(a, b, r, pickA, pickB) {
  const la = { k: 'L', x1: a.x1, y1: a.y1, x2: a.x2, y2: a.y2 }, lb = { k: 'L', x1: b.x1, y1: b.y1, x2: b.x2, y2: b.y2 };
  const x = intersect(la, lb, true, true)[0];
  if (!x) return null;
  // direction from the corner toward the kept part of each line
  const keepDir = (l, pick) => {
    const ends = [{ x: l.x1, y: l.y1 }, { x: l.x2, y: l.y2 }];
    const far = ends.sort((u, v) => dist(v, x) - dist(u, x))[0];
    const tp = primParam({ k: 'L', x1: x.x, y1: x.y, x2: far.x, y2: far.y }, pick, false);
    const target = tp >= 0 ? far : ends[1];
    const d = dist(x, target) || 1;
    return { ux: (target.x - x.x) / d, uy: (target.y - x.y) / d, far: target };
  };
  const da = keepDir(la, pickA), db = keepDir(lb, pickB);
  if (r <= EPS) {
    return { a: { ...a, x1: x.x, y1: x.y, x2: da.far.x, y2: da.far.y }, b: { ...b, x1: x.x, y1: x.y, x2: db.far.x, y2: db.far.y }, arc: null };
  }
  const half = Math.acos(Math.max(-1, Math.min(1, da.ux * db.ux + da.uy * db.uy))) / 2;
  if (half < 1e-6 || half > Math.PI / 2 - 1e-6) return null;
  const t = r / Math.tan(half);
  const pa = { x: x.x + da.ux * t, y: x.y + da.uy * t }, pb = { x: x.x + db.ux * t, y: x.y + db.uy * t };
  const bx = da.ux + db.ux, by = da.uy + db.uy, bl = Math.hypot(bx, by);
  const c = { x: x.x + bx / bl * (r / Math.sin(half)), y: x.y + by / bl * (r / Math.sin(half)) };
  let a0 = Math.atan2(pa.y - c.y, pa.x - c.x), a1 = Math.atan2(pb.y - c.y, pb.x - c.x);
  if (normAngle(a1 - a0) > Math.PI) [a0, a1] = [a1, a0];
  return {
    a: { ...a, x1: pa.x, y1: pa.y, x2: da.far.x, y2: da.far.y },
    b: { ...b, x1: pb.x, y1: pb.y, x2: db.far.x, y2: db.far.y },
    arc: { type: 'arc', layer: a.layer, color: a.color ?? null, cx: c.x, cy: c.y, r, a0: normAngle(a0), a1: normAngle(a1) },
  };
}

Tangent points and center at a corner

64-bit

The two tangent points and the center of a fillet at the corner of two straight pieces.

half = acos(ua·ub) ÷ 2; t = r ÷ tan(half); center = corner + bisector × r ÷ sin(half)

The half angle must lie between 1e-9 rad and 90° − 1e-9. The tangent points are placed with polar(), which turns each unit direction into an angle with atan2 and back with cos and sin instead of using the direction's components directly.

  • geom.js
  • lines 1652 to 1659
  • CadCraft 0.8.2
/** Corner of two straight pieces at x, along the unit rays ua and ub: fillet points for radius r. */
function llFillet(x, ua, ub, r) {
  const half = Math.acos(Math.max(-1, Math.min(1, dot(ua, ub)))) / 2;
  if (half < 1e-9 || half > Math.PI / 2 - 1e-9) return null;
  const t = r / Math.tan(half);
  const bis = unit(ua.x + ub.x, ua.y + ub.y);
  return { t, a: polar(x, Math.atan2(ua.y, ua.x), t), b: polar(x, Math.atan2(ub.y, ub.x), t), c: { x: x.x + bis.x * r / Math.sin(half), y: x.y + bis.y * r / Math.sin(half) } };
}

Fillet arc between two tangent points

64-bit

The arc entity of the fillet, the short way round from one tangent point to the other.

Both angles are atan2 of a tangent point minus the center, swapped when the counter-clockwise sweep exceeds π. The radius stored is the one given, not the computed distance from center to tangent point.

  • geom.js
  • lines 1661 to 1666
  • CadCraft 0.8.2
/** The arc of circle c (radius r) between a and b, the short way round. */
function cornerArc(c, r, a, b, like) {
  let a0 = Math.atan2(a.y - c.y, a.x - c.x), a1 = Math.atan2(b.y - c.y, b.x - c.x);
  if (normAngle(a1 - a0) > Math.PI) [a0, a1] = [a1, a0];
  return { type: 'arc', ...like, cx: c.x, cy: c.y, r, a0: normAngle(a0), a1: normAngle(a1) };
}

Cutting an object back to the corner

64-bit

Moves one end of a line, arc or polyline segment to the tangent or chamfer point.

arc end angle = atan2(T − center); polyline arc bulge = ± tan(sweep ÷ 4)

A line's end takes the cut point's coordinates as given. A cut is refused as too large when what would be kept is not longer than 1e-9 × max(1, length) on a line or under 1e-9 rad on an arc.

  • geom.js
  • lines 1612 to 1650
  • CadCraft 0.8.2
/**
 * Move one end of a target's piece to T, keeping the part that runs from T in the direction `keep`
 * (a unit vector for straight pieces; 'ccw' | 'cw' for arcs). A circle is never cut. → { e } | { error }.
 */
function cutTarget(t, T, keep, e = t.e) {
  if (e.type === 'circle') return { e };
  const p = t.prim;
  let end;                                  // which end of the primitive T replaces: 'start' (x1 / a0) or 'end'
  if (p.k === 'L') {
    const ds = (p.x1 - T.x) * keep.x + (p.y1 - T.y) * keep.y, de = (p.x2 - T.x) * keep.x + (p.y2 - T.y) * keep.y;
    if (Math.max(ds, de) <= 1e-9 * Math.max(1, primLength(p))) return { error: 'large' };
    end = de >= ds ? 'start' : 'end';
  } else {
    const a = Math.atan2(T.y - p.cy, T.x - p.cx);
    end = keep === 'ccw' ? 'start' : 'end';
    if (onSweep(a, p.a0, p.a1, 1e-9)) {
      if ((end === 'start' ? normAngle(p.a1 - a) : normAngle(a - p.a0)) < 1e-9) return { error: 'large' };
    } else {
      const toStart = normAngle(p.a0 - a), toEnd = normAngle(a - p.a1);   // an extension grows the nearer end
      if (end === 'start' ? toStart > toEnd : toEnd > toStart) return { error: 'large' };
    }
  }
  if (e.type === 'line') return { e: end === 'start' ? { ...e, x1: T.x, y1: T.y } : { ...e, x2: T.x, y2: T.y } };
  if (e.type === 'arc') { const a = normAngle(Math.atan2(T.y - e.cy, T.x - e.cx)); return { e: end === 'start' ? { ...e, a0: a } : { ...e, a1: a } }; }
  if (e.type !== 'poly') return { error: 'type' };
  // a polyline segment i runs from vertex i (P) to vertex i+1 (Q); a reversed arc primitive starts at Q
  const n = e.pts.length, i = t.seg, segs = e.closed ? n : n - 1;
  const atStart = p.k === 'L' || !p.rev ? end === 'start' : end === 'end';   // T replaces P (else Q)
  if (e.closed || (atStart ? i !== 0 : i !== segs - 1)) return { error: 'middle' };
  const pts = e.pts.map((v) => ({ ...v }));
  const k = atStart ? i : i + 1;
  pts[k] = { ...pts[k], x: T.x, y: T.y };
  if (p.k === 'A') {
    const a = Math.atan2(T.y - p.cy, T.x - p.cx);
    const a0 = end === 'start' ? a : p.a0, a1 = end === 'end' ? a : p.a1;
    pts[i].b = Math.tan(normAngle(a1 - a0) / 4) * (p.rev ? -1 : 1);
  }
  return { e: { ...e, pts } };
}

Fillet of two picked objects

64-bit

The fillet arc and trimmed ends for two lines, a line and an arc or circle, two arcs, two segments of one polyline, or two parallel lines.

with an arc: center = intersection of the two pieces offset by r; parallel lines: half circle of radius gap ÷ 2

With an arc or circle the center is found by intersecting each piece offset by ±r (lines moved along the normal, circles with radius R ± r), and the candidate whose tangent points are nearest the two picks is kept. Tangent points are the center projected onto each piece. The tests are absolute at 1e-9: tangent length against the available length, tangent points closer than 1e-9, and a gap between parallel lines under 1e-9 × max(1, length).

  • geom.js
  • lines 1718 to 1800
  • CadCraft 0.8.2
/**
 * FILLET between two picked targets with radius r. `trim` false leaves the objects as they are.
 * Lines that are parallel get a half circle across the gap, at the end of the first line nearest its pick.
 */
export function filletPair(A, B, r, { trim = true, current = null } = {}) {
  if (!A || !B) return { ok: false, error: 'type' };
  const like = layerOf(A.e, B.e, current);
  if (A.e === B.e || (A.e.id != null && A.e.id === B.e.id)) {
    if (A.e.type !== 'poly' || A.seg === B.seg) return { ok: false, error: 'same' };
    const sv = sharedVertex(A, B);
    if (!sv) return cutBoth(A, B, null, null, { same: true });
    if (!straight(polyPrims(A.e)[sv.inSeg]) || !straight(polyPrims(A.e)[sv.outSeg])) return { ok: false, error: 'type' };
    const vc = vertexCorner(A.e, sv.k, sv.inSeg, sv.outSeg);
    if (!vc) return { ok: false, error: 'none' };
    if (r <= EPS) return { ok: true, edits: [], add: [] };      // already a sharp corner
    const f = llFillet(vc.x, vc.ua, vc.ub, r);
    if (!f) return { ok: false, error: 'collinear' };
    if (f.t > vc.la + 1e-9 || f.t > vc.lb + 1e-9) return { ok: false, error: 'large' };
    const sweep = Math.PI - 2 * Math.acos(Math.max(-1, Math.min(1, dot(vc.ua, vc.ub)))) / 2;
    return { ok: true, edits: [spliceCorner(A.e, sv.k, f.a, Math.tan(sweep / 4) * vc.turn, f.b)], add: [] };
  }
  const pa = A.prim, pb = B.prim;
  if (straight(pa) && straight(pb)) {
    const x = intersect(pa, pb, true, true)[0];
    if (!x) {
      // parallel lines: a half circle across the gap
      const ua = lineDir(pa), ub = lineDir(pb);
      const E = [{ x: pa.x1, y: pa.y1 }, { x: pa.x2, y: pa.y2 }].sort((u, v) => dist(u, A.pick) - dist(v, A.pick))[0];
      const s = (E.x - pb.x1) * ub.x + (E.y - pb.y1) * ub.y, F = { x: pb.x1 + ub.x * s, y: pb.y1 + ub.y * s };
      const gap = dist(E, F);
      if (gap <= 1e-9 * Math.max(1, primLength(pa))) return { ok: false, error: 'collinear' };
      const other = dist(E, { x: pa.x1, y: pa.y1 }) < dist(E, { x: pa.x2, y: pa.y2 }) ? { x: pa.x2, y: pa.y2 } : { x: pa.x1, y: pa.y1 };
      const k = unit(other.x - E.x, other.y - E.y) || ua;
      const M = { x: (E.x + F.x) / 2, y: (E.y + F.y) / 2 }, rr = gap / 2;
      const aE = Math.atan2(E.y - M.y, E.x - M.x), aF = Math.atan2(F.y - M.y, F.x - M.x);
      const ccwFromE = (E.x - M.x) * -k.y - (E.y - M.y) * -k.x > 0;   // turning CCW from E reaches the outward side first
      const arc = { type: 'arc', ...like, cx: M.x, cy: M.y, r: rr, a0: normAngle(ccwFromE ? aE : aF), a1: normAngle(ccwFromE ? aF : aE) };
      return { ...cutBoth(A, B, [E, k], [F, k], { trim, add: [arc] }), semicircle: true };
    }
    const ra = keepRay(pa, x, A.pick), rb = keepRay(pb, x, B.pick);
    if (!ra || !rb) return { ok: false, error: 'none' };
    if (r <= EPS) return cutBoth(A, B, [x, ra.u], [x, rb.u], { trim, add: [] });
    const f = llFillet(x, ra.u, rb.u, r);
    if (!f) return { ok: false, error: 'collinear' };
    if (f.t >= ra.reach - 1e-9 || f.t >= rb.reach - 1e-9) return { ok: false, error: 'large' };
    return cutBoth(A, B, [f.a, ra.u], [f.b, rb.u], { trim, add: [cornerArc(f.c, r, f.a, f.b, like)] });
  }
  // a line, arc or circle with an arc or circle
  const full = (p) => ({ k: 'A', cx: p.cx, cy: p.cy, r: p.r, a0: 0, a1: TAU, full: true });
  const inf = (p) => (straight(p) ? p : full(p));
  if (r <= EPS) {
    let best = null;
    for (const x of intersect(inf(pa), inf(pb), true, true)) { const s = dist(x, A.pick) + dist(x, B.pick); if (!best || s < best.s) best = { s, x }; }
    if (!best) return { ok: false, error: 'none' };
    return cutBoth(A, B, [best.x, sharpKeep(pa, best.x, A.pick)], [best.x, sharpKeep(pb, best.x, B.pick)], { trim, add: [] });
  }
  const offsets = (p) => {
    if (straight(p)) {
      const u = lineDir(p), n = { x: -u.y * r, y: u.x * r };
      return [1, -1].map((s) => ({ k: 'L', x1: p.x1 + n.x * s, y1: p.y1 + n.y * s, x2: p.x2 + n.x * s, y2: p.y2 + n.y * s }));
    }
    return [p.r + r, p.r - r].filter((R) => R > EPS).map((R) => ({ ...full(p), r: R }));
  };
  const touch = (p, c) => {
    if (straight(p)) { const t = primParam(p, c, false); return primPoint(p, t); }
    const u = unit(c.x - p.cx, c.y - p.cy);
    return u ? { x: p.cx + u.x * p.r, y: p.cy + u.y * p.r } : null;
  };
  let best = null;
  for (const oa of offsets(pa)) for (const ob of offsets(pb)) for (const c of intersect(oa, ob, true, true)) {
    const ta = touch(pa, c), tb = touch(pb, c);
    if (!ta || !tb || dist(ta, tb) <= 1e-9) continue;
    const s = dist(ta, A.pick) + dist(tb, B.pick);
    if (!best || s < best.s) best = { s, c, ta, tb };
  }
  if (!best) return { ok: false, error: 'none' };
  const away = (p, at, to) => {   // the original runs away from the fillet arc at its tangent point
    if (straight(p)) { const u = lineDir(p), s = dot(u, { x: to.x - at.x, y: to.y - at.y }); return s > 0 ? { x: -u.x, y: -u.y } : u; }
    const a = Math.atan2(at.y - p.cy, at.x - p.cx);
    return (to.x - at.x) * -Math.sin(a) + (to.y - at.y) * Math.cos(a) > 0 ? 'cw' : 'ccw';
  };
  return cutBoth(A, B, [best.ta, away(pa, best.ta, best.tb)], [best.tb, away(pb, best.tb, best.ta)], { trim, add: [cornerArc(best.c, r, best.ta, best.tb, like)] });
}

Chamfer distances

64-bit

How far to cut back each side of a corner, from two distances or from a length and an angle.

angle method: θ = acos(ua·ub); da = len; db = len·sin α ÷ sin(θ + α)

The dot product is clamped before acos. The angle method is refused when α ≤ 1e-9 or θ + α ≥ π − 1e-9, which keeps sin(θ + α) away from 0. Negative distances are raised to 0.

  • geom.js
  • lines 1823 to 1832
  • CadCraft 0.8.2
/** The chamfer cut on each side of a corner: { da, db } from distances, or from a length and an angle. */
function chamferCuts(spec, ua, ub) {
  if (spec.method === 'A') {
    const theta = Math.acos(Math.max(-1, Math.min(1, dot(ua, ub)))), al = spec.ang || 0;
    if (!(spec.len > EPS)) return { da: 0, db: 0 };
    if (!(al > 1e-9) || theta + al >= Math.PI - 1e-9) return null;
    return { da: spec.len, db: spec.len * Math.sin(al) / Math.sin(theta + al) };
  }
  return { da: Math.max(0, spec.d1 || 0), db: Math.max(0, spec.d2 ?? spec.d1 ?? 0) };
}

Chamfer of two picked pieces

64-bit

The chamfer line and trimmed ends for two straight pieces.

The cut points are the corner plus each unit direction times its distance, placed through polar() (atan2, then cos and sin). Pieces count as collinear when |ua·ub| > 1 − 1e-12, and a distance may exceed the available length by 1e-9.

  • geom.js
  • lines 1834 to 1869
  • CadCraft 0.8.2
/**
 * CHAMFER between two picked straight pieces. spec { method: 'D' | 'A', d1, d2, len, ang (radians) }:
 * d1 is cut from the first piece, d2 from the second; or len from the first at ang to it.
 */
export function chamferPair(A, B, spec, { trim = true, current = null } = {}) {
  if (!A || !B) return { ok: false, error: 'type' };
  if (!straight(A.prim) || !straight(B.prim)) return { ok: false, error: 'type' };
  const like = layerOf(A.e, B.e, current);
  if (A.e === B.e || (A.e.id != null && A.e.id === B.e.id)) {
    if (A.e.type !== 'poly' || A.seg === B.seg) return { ok: false, error: 'same' };
    const sv = sharedVertex(A, B);
    if (!sv) return { ok: false, error: 'middle' };
    // the first pick's segment takes the first distance
    const vc = vertexCorner(A.e, sv.k, sv.inSeg, sv.outSeg);
    if (!vc) return { ok: false, error: 'none' };
    const firstIn = A.seg === sv.inSeg;
    const cuts = chamferCuts(spec, firstIn ? vc.ua : vc.ub, firstIn ? vc.ub : vc.ua);
    if (!cuts) return { ok: false, error: 'angle' };
    const dIn = firstIn ? cuts.da : cuts.db, dOut = firstIn ? cuts.db : cuts.da;
    if (Math.abs(dot(vc.ua, vc.ub)) > 1 - 1e-12) return { ok: false, error: 'collinear' };
    if (dIn > vc.la + 1e-9 || dOut > vc.lb + 1e-9) return { ok: false, error: 'large' };
    if (dIn <= EPS && dOut <= EPS) return { ok: true, edits: [], add: [] };
    return { ok: true, edits: [spliceCorner(A.e, sv.k, polar(vc.x, Math.atan2(vc.ua.y, vc.ua.x), dIn), 0, polar(vc.x, Math.atan2(vc.ub.y, vc.ub.x), dOut))], add: [] };
  }
  const x = intersect(A.prim, B.prim, true, true)[0];
  if (!x) return { ok: false, error: 'parallel' };
  const ra = keepRay(A.prim, x, A.pick), rb = keepRay(B.prim, x, B.pick);
  if (!ra || !rb) return { ok: false, error: 'none' };
  if (Math.abs(dot(ra.u, rb.u)) > 1 - 1e-12) return { ok: false, error: 'collinear' };
  const cuts = chamferCuts(spec, ra.u, rb.u);
  if (!cuts) return { ok: false, error: 'angle' };
  if (cuts.da > ra.reach + 1e-9 || cuts.db > rb.reach + 1e-9) return { ok: false, error: 'large' };
  const pa = polar(x, Math.atan2(ra.u.y, ra.u.x), cuts.da), pb = polar(x, Math.atan2(rb.u.y, rb.u.x), cuts.db);
  const add = dist(pa, pb) > EPS ? [{ type: 'line', ...like, x1: pa.x, y1: pa.y, x2: pb.x, y2: pb.y }] : [];
  return cutBoth(A, B, [pa, ra.u], [pb, rb.u], { trim, add });
}

Removing old fillet arcs

64-bit

Finds the fillet arcs a polyline already has and restores the sharp corners before a new radius is applied.

θ = 4·atan(b); fillet when direction in = chord − θ ÷ 2 and direction out = chord + θ ÷ 2

Tangency is tested to 1e-6 rad, absolute. The restored corner is the intersection of the two neighboring segments taken as unbounded.

  • geom.js
  • lines 1871 to 1894
  • CadCraft 0.8.2
/** Undo the fillet arcs a polyline already has (an arc segment tangent to the straight segments on both sides). */
function unfilletPoly(e) {
  const v = e.pts, n = v.length, closed = e.closed;
  if (n < 4) return { e, n: 0 };
  const dir = (p, q) => Math.atan2(q.y - p.y, q.x - p.x);
  const same = (a, b) => { const d = normAngle(a - b); return d < 1e-6 || d > TAU - 1e-6; };
  const drop = new Set(), at = new Map();
  for (let k = 0; k < n; k++) {
    const b = v[k].b || 0;
    if (Math.abs(b) < 1e-12) continue;
    const pk = (k - 1 + n) % n, k1 = (k + 1) % n, k2 = (k + 2) % n;
    if (!closed && (k < 1 || k + 2 > n - 1)) continue;
    if (Math.abs(v[pk].b || 0) > 1e-12 || Math.abs(v[k1].b || 0) > 1e-12 || drop.has(k) || drop.has(k1) || at.has(k1)) continue;
    const chord = dir(v[k], v[k1]), th = 4 * Math.atan(b);
    if (!same(dir(v[pk], v[k]), chord - th / 2) || !same(dir(v[k1], v[k2]), chord + th / 2)) continue;
    const x = intersect({ k: 'L', x1: v[pk].x, y1: v[pk].y, x2: v[k].x, y2: v[k].y }, { k: 'L', x1: v[k1].x, y1: v[k1].y, x2: v[k2].x, y2: v[k2].y }, true, true)[0];
    if (!x) continue;
    at.set(k, x); drop.add(k1);
  }
  if (!at.size) return { e, n: 0 };
  const pts = [];
  v.forEach((p, k) => { if (drop.has(k)) return; pts.push(at.has(k) ? { x: at.get(k).x, y: at.get(k).y, b: 0 } : { ...p }); });
  return { e: { ...e, pts }, n: at.size };
}

Every corner of a polyline

64-bit

Fillets or chamfers each corner of a polyline where two straight segments meet.

sweep = π − acos(ua·ub); bulge = tan(sweep ÷ 4) × turn

Each corner uses the same tangent-length formula as a two-line fillet. The length a previous corner already used on a segment is tracked, and a corner is skipped when it needs more than what is left plus 1e-9. Vertices closer than 1e-9 after the edit are merged.

  • geom.js
  • lines 1896 to 1943
  • CadCraft 0.8.2
/**
 * Every corner of a polyline where two straight segments meet: kind 'fillet' (spec = radius; old fillet arcs
 * are taken out first, so a new radius replaces them) or 'chamfer' (spec as chamferPair; the incoming segment
 * takes the first distance). → { e, done, short, arcs } — short = corners whose segments were too short.
 */
export function polyCorners(e, kind, spec) {
  let src = e, removed = 0;
  if (kind === 'fillet') ({ e: src, n: removed } = unfilletPoly(e));
  const v = src.pts, n = v.length, closed = src.closed;
  const segs = closed ? n : n - 1;
  const isLine = (i) => Math.abs(v[i].b || 0) < 1e-12;
  const used = new Array(Math.max(segs, 0)).fill(0);
  const segLen = (i) => dist(v[i], v[(i + 1) % n]);
  const repl = new Map();
  let short = 0, arcs = 0;
  for (let k = closed ? 0 : 1; k < (closed ? n : n - 1); k++) {
    const i = (k - 1 + n) % n;
    if (!isLine(i) || !isLine(k)) { arcs++; continue; }
    const vc = vertexCorner(src, k, i, k);
    if (!vc || Math.abs(dot(vc.ua, vc.ub)) > 1 - 1e-12) continue;   // no corner there
    let a, b, bulge = 0;
    if (kind === 'fillet') {
      if (!(spec > EPS)) continue;
      const f = llFillet(vc.x, vc.ua, vc.ub, spec);
      if (!f) continue;
      a = f.a; b = f.b;
      const sweep = Math.PI - Math.acos(Math.max(-1, Math.min(1, dot(vc.ua, vc.ub))));
      bulge = Math.tan(sweep / 4) * vc.turn;
    } else {
      const cuts = chamferCuts(spec, vc.ua, vc.ub);
      if (!cuts) { short++; continue; }
      if (cuts.da <= EPS && cuts.db <= EPS) continue;
      a = polar(vc.x, Math.atan2(vc.ua.y, vc.ua.x), cuts.da); b = polar(vc.x, Math.atan2(vc.ub.y, vc.ub.x), cuts.db);
    }
    const ta = dist(vc.x, a), tb = dist(vc.x, b);
    if (ta > segLen(i) - used[i] + 1e-9 || tb > segLen(k) - used[k] + 1e-9) { short++; continue; }
    used[i] += ta; used[k] += tb;
    repl.set(k, { a, b, bulge });
  }
  if (!repl.size) return { e: removed ? src : e, done: 0, short, arcs, removed };
  const pts = [];
  v.forEach((p, k) => {
    const r = repl.get(k);
    if (r) pts.push({ x: r.a.x, y: r.a.y, b: r.bulge }, { x: r.b.x, y: r.b.y, b: p.b || 0 });
    else pts.push({ ...p });
  });
  return { e: { ...src, pts: dedupe(pts, closed) }, done: repl.size, short, arcs, removed };
}

In detail

Method
Two lines: tangent length t = r ÷ tan(half angle), center on the bisector at r ÷ sin(half angle). With an arc: center = intersection of the two offset pieces. Chamfer by two distances, or by a length and an angle with the law of sines.
Number types
64-bit numbers.
Tolerances
Half angle between 1e-9 rad and 90° − 1e-9 (1e-6 in filletLines). Tangent or chamfer length may exceed the available length by 1e-9. Collinear when |ua·ub| > 1 − 1e-12. An existing fillet arc is recognized when it is tangent to both neighbors within 1e-6 rad. Vertices within 1e-9 are merged.
Limits
acos of a dot product close to ±1 keeps fewer digits, so corners between nearly parallel pieces are less exact than square ones. Chamfer works on straight pieces only.

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