Resources Numerical accuracy Calculation policy

Part and stock in machine coordinates

Turns the part into the machine's work frame for toolpaths: the stock block, the work origin, exact outlines of flat faces, hole positions and sizes, and the height of the part's top at any point.

How the precision is kept

Frame changes are exact: the tool axis is a world axis, so rotation only swaps and negates coordinates, and the origin is one subtraction. Outlines of flat faces are rebuilt from the exact edge table (64-bit end points; circles through three edge points), with the mesh used only to find which edges bound a face. Heights of curved surfaces, however, are interpolated on the display mesh, whose vertices were read from a Float32Array; the code's own tolerance comment names this.

Open item. Use the edge table's exact circle center and radius instead of the three-point circumcircle, and take heights of curved surfaces from the exact faces or a 64-bit mesh.

Calculations under this policy

12 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.js PartCraft 0.8.1 32-bit in part 12 calculations Reviewed

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

Stock block

64-bit

The size and position of the stock around the part.

offset mode: min = box min − (side, side, bottom), max = box max + (side, side, top); cylinder: r = ½ × box diagonal in XY + side

Additions and subtractions of the model's exact bounding box and the stored offsets. The cylinder radius uses Math.hypot of the box's two XY sides.

  • cam.js
  • lines 216 to 231
  • PartCraft 0.8.1
const st = setup.stock;
let smin, smax;
if (st.mode === 'fixed') {
  const cx = (bmin[0] + bmax[0]) / 2, cy = (bmin[1] + bmax[1]) / 2, top = bmax[2] + st.top;
  smin = [cx - st.size[0] / 2, cy - st.size[1] / 2, top - st.size[2]]; smax = [cx + st.size[0] / 2, cy + st.size[1] / 2, top];
} else {
  smin = [bmin[0] - st.side, bmin[1] - st.side, bmin[2] - st.bottom]; smax = [bmax[0] + st.side, bmax[1] + st.side, bmax[2] + st.top];
}
let cyl = null;
if (st.kind === 'cylinder') {
  const c = [(bmin[0] + bmax[0]) / 2, (bmin[1] + bmax[1]) / 2];
  const r = st.mode === 'fixed' ? st.r : Math.hypot(bmax[0] - bmin[0], bmax[1] - bmin[1]) / 2 + st.side;
  cyl = { c, r };
  smin = [c[0] - r, c[1] - r, smin[2]]; smax = [c[0] + r, c[1] + r, smax[2]];
}
const sc = [(smin[0] + smax[0]) / 2, (smin[1] + smax[1]) / 2];

Work origin and frame change

64-bit

Moves points from model coordinates to the work frame (G54) and back.

work = (p · x, p · y, p · z) − origin

The frame axes hold only 0, 1 and −1, so the dot products just pick and negate coordinates with no rounding. The origin subtraction is one 64-bit operation per coordinate.

  • cam.js
  • lines 233 to 245
  • PartCraft 0.8.1
const O0 = {
  'stock-top-min': [smin[0], smin[1], smax[2]], 'stock-top-centre': [sc[0], sc[1], smax[2]],
  'stock-bottom-min': [smin[0], smin[1], smin[2]], 'stock-bottom-centre': [sc[0], sc[1], smin[2]],
  'model-top-centre': [bc[0], bc[1], bmax[2]], 'model-origin': [0, 0, 0],
}[setup.origin] || [smin[0], smin[1], smax[2]];
const toM = (p) => { const q = rot(p); return [q[0] - O0[0], q[1] - O0[1], q[2] - O0[2]]; };
const toWorld = (m) => {
  const q = [m[0] + O0[0], m[1] + O0[1], m[2] + O0[2]];
  return [0, 1, 2].map((k) => ax.x[k] * q[0] + ax.y[k] * q[1] + ax.z[k] * q[2]);
};
const sh = (p) => [p[0] - O0[0], p[1] - O0[1], p[2] - O0[2]];
const stock = { kind: cyl ? 'cylinder' : 'box', min: sh(smin), max: sh(smax), c: cyl ? [cyl.c[0] - O0[0], cyl.c[1] - O0[1]] : null, r: cyl ? cyl.r : null };
const box = { min: sh(bmin), max: sh(bmax) };

Mesh in the work frame and snap tolerance

32-bit in part

Copies the part's mesh into work coordinates and sets how close a mesh point must be to an exact edge to be taken as that edge.

tol = 1e-4 + 2e-6 × largest model dimension

The vertices come from the kernel's Float32Array and are stored in a Float64Array after the frame change, so each still carries 32-bit precision. The code comment on the tolerance reads 'float32 mesh vertices vs exact edges'.

  • cam.js
  • lines 248 to 261
  • PartCraft 0.8.1
const P = regen.mesh.positions, I = regen.mesh.indices;
const nv = P.length / 3;
const pos = new Float64Array(P.length);
for (let i = 0; i < nv; i++) { const q = toM([P[i * 3], P[i * 3 + 1], P[i * 3 + 2]]); pos[i * 3] = q[0]; pos[i * 3 + 1] = q[1]; pos[i * 3 + 2] = q[2]; }
const mesh = { positions: pos, indices: I, triFace: regen.mesh.triFace };
const faces = (regen.faces || []).map((f) => {
  const g = { ...f, at: toM(f.at) };
  if (f.n) { g.n = rotD(f.n); g.o = toM(f.o); }
  if (f.axis) g.axis = { o: toM(f.axis.o), d: rotD(f.axis.d) };
  return g;
});
const edges = (regen.edges || []).map((e) => ({ ...e, a: toM(e.a), b: toM(e.b), at: toM(e.at) }));
const size = Math.max(1, ...box.max.map((v, k) => v - box.min[k]));
const tol = 1e-4 + 2e-6 * size;   // float32 mesh vertices vs exact edges

Where a vertical line meets the part

32-bit in part

The heights at which a vertical line through (x, y) crosses the part's surface.

weights l₁, l₂, l₃ from the triangle's XY corners; z = l₁ z₁ + l₂ z₂ + l₃ z₃

Barycentric interpolation in 64-bit on vertices of 32-bit precision. Triangles seen nearly edge-on (|den| < 1e-14, an absolute test in mm²) are skipped, and a weight down to −1e-12 still counts as inside. The top of the part returned by heightAt is the largest of these values.

  • cam.js
  • lines 276 to 293
  • PartCraft 0.8.1
/** z values where the vertical line at (x, y) meets the mesh (triangles seen edge-on are skipped). */
function hits(x, y) {
  const i = Math.floor((x - bx0) / bw), j = Math.floor((y - by0) / bh);
  if (i < 0 || j < 0 || i >= binN || j >= binN) return [];
  const out = [];
  for (const t of bins[j * binN + i]) {
    const a = I[t * 3] * 3, b = I[t * 3 + 1] * 3, c = I[t * 3 + 2] * 3;
    const x1 = pos[a], y1 = pos[a + 1], x2 = pos[b], y2 = pos[b + 1], x3 = pos[c], y3 = pos[c + 1];
    const den = (y2 - y3) * (x1 - x3) + (x3 - x2) * (y1 - y3);
    if (Math.abs(den) < 1e-14) continue;
    const l1 = ((y2 - y3) * (x - x3) + (x3 - x2) * (y - y3)) / den;
    const l2 = ((y3 - y1) * (x - x3) + (x1 - x3) * (y - y3)) / den;
    const l3 = 1 - l1 - l2;
    if (l1 < -1e-12 || l2 < -1e-12 || l3 < -1e-12) continue;
    out.push({ z: l1 * pos[a + 2] + l2 * pos[b + 2] + l3 * pos[c + 2], up: den > 0 });
  }
  return out;
}

Top facet and its slope

32-bit in part

The height and slope of the highest mesh triangle under a point, used by 3D finishing.

g_x = ((z₂ − z₁)(y₃ − y₁) − (z₃ − z₁)(y₂ − y₁)) ÷ d₂; g_y likewise

The slope is that of a flat facet of the mesh, not of the true surface, and its inputs are 32-bit vertices. The divisor is twice the facet's projected area, the same magnitude as the value den that the 1e-14 test guards.

  • cam.js
  • lines 304 to 325
  • PartCraft 0.8.1
/** The highest triangle at (x, y) as z(X, Y) = z + gx·(X − x) + gy·(Y − y): {z, gx, gy}, null where there is none (3D finishing). */
function topPlane(x, y) {
  const i = Math.floor((x - bx0) / bw), j = Math.floor((y - by0) / bh);
  if (i < 0 || j < 0 || i >= binN || j >= binN) return null;
  let best = null;
  for (const t of bins[j * binN + i]) {
    const a = I[t * 3] * 3, b = I[t * 3 + 1] * 3, c = I[t * 3 + 2] * 3;
    const x1 = pos[a], y1 = pos[a + 1], x2 = pos[b], y2 = pos[b + 1], x3 = pos[c], y3 = pos[c + 1];
    const den = (y2 - y3) * (x1 - x3) + (x3 - x2) * (y1 - y3);
    if (Math.abs(den) < 1e-14) continue;
    const l1 = ((y2 - y3) * (x - x3) + (x3 - x2) * (y - y3)) / den;
    const l2 = ((y3 - y1) * (x - x3) + (x1 - x3) * (y - y3)) / den;
    const l3 = 1 - l1 - l2;
    if (l1 < -1e-12 || l2 < -1e-12 || l3 < -1e-12) continue;
    const z = l1 * pos[a + 2] + l2 * pos[b + 2] + l3 * pos[c + 2];
    if (best && z <= best.z) continue;
    const d2 = (x2 - x1) * (y3 - y1) - (x3 - x1) * (y2 - y1);
    const z1 = pos[a + 2], z2 = pos[b + 2], z3 = pos[c + 2];
    best = { z, gx: ((z2 - z1) * (y3 - y1) - (z3 - z1) * (y2 - y1)) / d2, gy: ((z3 - z1) * (x2 - x1) - (z2 - z1) * (x3 - x1)) / d2 };
  }
  return best;
}

Material below a point

32-bit in part

Whether there is part material just under a height at (x, y), used to tell a through hole from a blind one.

first surface below z faces up

The query point is nudged by fixed amounts of 1.37e-7 and 0.91e-7 mm so the line does not pass exactly through a mesh vertex or edge. The answer is a yes or no from the mesh.

  • cam.js
  • lines 326 to 332
  • PartCraft 0.8.1
/** Is there part material just below z at (x, y)? (ray parity, nudged off mesh vertices) */
function materialBelow(x, y, z) {
  const hs = hits(x + 1.37e-7, y + 0.91e-7).filter((h) => h.z < z).sort((a, b) => b.z - a.z);
  if (!hs.length) return false;
  // the first surface below: facing up means we are above material
  return hs[0].up;
}

Circles of round edges

64-bit

The center and radius of each circular edge lying in a plane square to the tool.

full circle: c = (a + mid) ÷ 2, r = |a − mid| ÷ 2; arc: circle through a, mid, b

The three points are exact 64-bit points of the edge. The center and radius are recomputed from them rather than read from the edge table, which also carries the kernel's own circle (the field named circ) that this file does not use.

  • cam.js
  • lines 337 to 344
  • PartCraft 0.8.1
const circles = [];
for (const e of edges) {
  if (e.type !== 'circle' || Math.abs(e.a[2] - e.b[2]) > tol || Math.abs(e.a[2] - e.at[2]) > tol) continue;
  let c, r;
  if (Math.hypot(e.a[0] - e.b[0], e.a[1] - e.b[1]) < tol) { c = [(e.a[0] + e.at[0]) / 2, (e.a[1] + e.at[1]) / 2]; r = Math.hypot(e.a[0] - e.at[0], e.a[1] - e.at[1]) / 2; }
  else { const cc = circum(e.a, e.at, e.b); if (!cc) continue; c = cc.c; r = cc.r; }
  circles.push({ c, r, z: e.a[2], a: e.a, b: e.b, at: e.at });
}

Circle through three points

64-bit

The center and radius of the circle through three 2D points.

d = 2 (a_x (b_y − c_y) + b_x (c_y − a_y) + c_x (a_y − b_y)); u_x = (|a|² (b_y − c_y) + |b|² (c_y − a_y) + |c|² (a_y − b_y)) ÷ d

The textbook formula on absolute coordinates. The terms |a|², |b|², |c|² grow with the square of the distance from the origin and nearly cancel, so a small circle far from the work origin loses digits. Three points closer to a line than |d| < 1e-14 (absolute) give no circle.

  • cam.js
  • lines 415 to 422
  • PartCraft 0.8.1
function circum(a, b, c) {
  const ax = a[0], ay = a[1], bx = b[0], by = b[1], cx = c[0], cy = c[1];
  const d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
  if (Math.abs(d) < 1e-14) return null;
  const a2 = ax * ax + ay * ay, b2 = bx * bx + by * by, c2 = cx * cx + cy * cy;
  const ux = (a2 * (by - cy) + b2 * (cy - ay) + c2 * (ay - by)) / d, uy = (a2 * (cx - bx) + b2 * (ax - cx) + c2 * (bx - ax)) / d;
  return { c: [ux, uy], r: Math.hypot(ax - ux, ay - uy) };
}

Arc from start, middle and end

64-bit

The signed sweep of an arc about a known center that passes from a start through a middle point to an end.

counterclockwise when mod(a_m − a₀, 2π) < mod(a₁ − a₀, 2π); sweep = ± mod(a₁ − a₀, 2π)

Angles come from atan2 about the center. A sweep below 1e-12 is taken as a full turn. The arc keeps the given end points as its stored ends.

  • cam.js
  • lines 425 to 432
  • PartCraft 0.8.1
/** Arc about c from s to e passing through m (all 2D). */
function arcThrough(c, r, s, m, e) {
  const a0 = ang(c, s), am = ang(c, m), a1 = ang(c, e);
  const ccw = mod(am - a0, O.TAU) < mod(a1 - a0, O.TAU) || O.dist(s, e) < 1e-12;
  let sw = ccw ? mod(a1 - a0, O.TAU) : -mod(a0 - a1, O.TAU);
  if (Math.abs(sw) < 1e-12) sw = ccw ? O.TAU : -O.TAU;
  return O.arc(c, r, a0, sw, [s[0], s[1]], [e[0], e[1]]);
}

Exact outline of a flat face

64-bit

Turns a face's mesh boundary into lines and arcs whose ends are the exact edge ends.

mesh corner within tol of an edge end → that end; mesh run within tol of an edge circle → arc on that circle

Corners are replaced by the exact 64-bit edge ends and curved runs by arcs on the edge circles, so the 32-bit mesh points do not reach the outline in those cases. A run that fits neither a line nor a known circle is kept as the mesh polyline, which does carry the mesh's precision and chord tolerance.

  • cam.js
  • lines 434 to 471
  • PartCraft 0.8.1
/** Turn a mesh boundary loop (2D points) into exact prims: corners snapped to edge ends, arcs onto edge circles. */
function snapLoop(pts, zEnds, zCirc, tol) {
  const n = pts.length;
  const corner = pts.map((p) => { let best = null, bd = tol; for (const q of zEnds) { const d = Math.hypot(p[0] - q[0], p[1] - q[1]); if (d < bd) { bd = d; best = [q[0], q[1]]; } } return best; });
  const ci = [];
  for (let i = 0; i < n; i++) if (corner[i]) ci.push(i);
  const onC = (p, c) => Math.abs(Math.hypot(p[0] - c.c[0], p[1] - c.c[1]) - c.r) < tol;
  if (!ci.length) {
    // no exact vertex (should not happen with OCCT edges): keep the polyline
    return pts.map((p, i) => O.line(p, pts[(i + 1) % n]));
  }
  const out = [];
  for (let k = 0; k < ci.length; k++) {
    const i = ci[k], j = ci[(k + 1) % ci.length];
    const span = j > i ? j - i : j + n - i;
    const mids = [];
    for (let s = 1; s < span; s++) mids.push(pts[(i + s) % n]);
    const s = corner[i], e = corner[j];
    let cand = null;
    if (mids.length) cand = zCirc.find((c) => onC(s, c) && onC(e, c) && mids.every((p) => onC(p, c)));
    else cand = zCirc.find((c) => O.dist(s, e) > tol && ((O.dist(c.a, s) < tol && O.dist(c.b, e) < tol) || (O.dist(c.a, e) < tol && O.dist(c.b, s) < tol)) && Math.abs(crossDist(c.at, s, e)) > tol);
    if (cand) {
      const m = mids.length ? mids[Math.floor(mids.length / 2)] : [cand.at[0], cand.at[1]];
      out.push(arcThrough(cand.c, cand.r, s, m, e));
    } else if (mids.length && !mids.every((p) => Math.abs(crossDist(p, s, e)) < tol)) {
      // a free-form edge: the mesh polyline (chord tolerance = the display mesh's)
      let prev = s;
      for (const p of mids) { out.push(O.line(prev, p)); prev = p; }
      out.push(O.line(prev, e));
    } else if (O.dist(s, e) > 1e-12) out.push(O.line(s, e));
  }
  // join exactly
  for (let k = 0; k < out.length; k++) {
    const p = out[k], q = out[(k + 1) % out.length];
    if (p.e !== q.s) out[(k + 1) % out.length] = q.k === 'L' ? O.line(p.e, q.e) : O.arc(q.c, q.r, q.a0, q.sw, p.e, q === p ? p.e : q.e);
  }
  return out;
}

Hole position, size and depth

64-bit

The center, radius, top and bottom of a round face along the tool axis, and whether it is a hole and goes through.

c, r from the exact cylinder; top, bottom = mesh z range snapped to an edge end within tol

The center and radius are the kernel's exact values. Top and bottom start as the mesh's extreme heights and are replaced by an exact edge end when one lies within tol; if none does, the 32-bit mesh value is kept.

  • cam.js
  • lines 361 to 380
  • PartCraft 0.8.1
/** A cylinder face along the tool axis → {c, r, top, bottom, hole (material outside), full, through} or null. */
function cylinderInfo(i) {
  const f = faces[i];
  if (!f || f.type !== 'cylinder' || Math.abs(Math.abs(f.axis.d[2]) - 1) > 1e-9) return null;
  let zmin = Infinity, zmax = -Infinity, hole = 0, ang = new Set();
  const c = [f.axis.o[0], f.axis.o[1]];
  for (let t = 0; t < nt; t++) {
    if (mesh.triFace[t] !== i) continue;
    const vs = [I[t * 3], I[t * 3 + 1], I[t * 3 + 2]].map((v) => [pos[v * 3], pos[v * 3 + 1], pos[v * 3 + 2]]);
    for (const p of vs) { zmin = Math.min(zmin, p[2]); zmax = Math.max(zmax, p[2]); ang.add(Math.floor(((Math.atan2(p[1] - c[1], p[0] - c[0]) + Math.PI) / O.TAU) * 36) % 36); }
    const u = [vs[1][0] - vs[0][0], vs[1][1] - vs[0][1], vs[1][2] - vs[0][2]], w = [vs[2][0] - vs[0][0], vs[2][1] - vs[0][1], vs[2][2] - vs[0][2]];
    const n = [u[1] * w[2] - u[2] * w[1], u[2] * w[0] - u[0] * w[2], u[0] * w[1] - u[1] * w[0]];
    const m = [(vs[0][0] + vs[1][0] + vs[2][0]) / 3 - c[0], (vs[0][1] + vs[1][1] + vs[2][1]) / 3 - c[1]];
    hole += n[0] * m[0] + n[1] * m[1] < 0 ? 1 : -1;
  }
  const snapZ = (z) => { let best = z, bd = tol; for (const p of ends) { const d = Math.abs(p[2] - z); if (d < bd) { bd = d; best = p[2]; } } return best; };
  const top = snapZ(zmax), bottom = snapZ(zmin);
  const isHole = hole > 0;
  return { c, r: f.r, top, bottom, hole: isHole, full: ang.size >= 34, through: isHole ? !materialBelow(c[0], c[1], bottom - 1e-3) : false };
}

Chaining picked edges into a loop

64-bit

Orders picked edges into one closed outline and makes each start exactly on the previous end.

ends join when closer than tol; then next.start = previous.end

After chaining, each piece's start point is replaced by the previous piece's end point object. For an arc this leaves its center and radius as they were, so the stored start can sit off the circle by the small gap that was closed.

  • cam.js
  • lines 477 to 498
  • PartCraft 0.8.1
/** Order and orient prims into one closed loop (prims may come in any direction). */
function chainOpen(prims, tol) {
  if (prims.length === 1) return O.dist(prims[0].s, prims[0].e) < tol ? [prims[0]] : null;
  const left = prims.slice(1), loop = [prims[0]];
  while (left.length) {
    const end = loop[loop.length - 1].e;
    let k = left.findIndex((p) => O.dist(p.s, end) < tol), rev = false;
    if (k < 0) { k = left.findIndex((p) => O.dist(p.e, end) < tol); rev = true; }
    if (k < 0) {
      if (loop.length === 1) { loop[0] = O.reversePrim(loop[0]); continue; }
      return null;
    }
    const p = left.splice(k, 1)[0];
    loop.push(rev ? O.reversePrim(p) : p);
  }
  if (O.dist(loop[loop.length - 1].e, loop[0].s) > tol) return null;
  for (let k = 0; k < loop.length; k++) {
    const p = loop[k], q = loop[(k + 1) % loop.length];
    if (p.e !== q.s) loop[(k + 1) % loop.length] = q.k === 'L' ? O.line(p.e, q.e) : O.arc(q.c, q.r, q.a0, q.sw, p.e, q.e);
  }
  return loop;
}

In detail

Method
Axis-aligned change of frame; stock from the model box plus offsets, or a fixed size; height by barycentric interpolation on the mesh triangle under a point; mesh boundary loops snapped to exact edge ends and circles; circle through three points by the circumcenter formula.
Number types
64-bit numbers. Mesh vertices are copied from a Float32Array into a Float64Array after the frame change, so they hold 32-bit values in 64-bit storage.
Tolerances
Snap tolerance tol = 1e-4 + 2e-6 × largest model dimension (mm). Triangles with twice-projected-area below 1e-14 are skipped. A point counts as inside a triangle down to −1e-12 in barycentric weights. Faces count as square to the tool when |n_z| is within 1e-9 of 1. A cylinder counts as full when its mesh covers at least 34 of 36 ten-degree sectors.
Limits
Heights carry the display mesh's chord tolerance and 32-bit vertices. Free-form edges of a flat face stay as the mesh polyline. The circumcenter formula uses squared absolute coordinates, which lose digits for a small circle far from the work origin.

Approach: 32-bit step, flagged

One step stores or exchanges 32-bit values that reach a number a person reads. It is listed here with its open item, and the step is either moved to 64-bit or the app will warn where the number is read.

Every policy that takes this approach