Plans roughing passes that keep the cutter's sideways engagement near a set width, and re-checks the engagement they give.
How the precision is kept
The passes are found on a square grid over the region (cell about a twelfth of the tool radius, coarser for large regions), so their shape is resolved to about a cell. Distances to the walls are computed exactly from the region's curves, and every pass point is pushed back to at least the tool radius plus stock to leave from the walls. Arrays are Float64Array and byte masks.
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.
The values written are (q − p)² + f(p), sums of whole numbers that are exact in 64-bit; the division only decides which parabola is lowest. Cells with no source carry 1e20.
cam-adaptive.js
lines 29 to 41
PartCraft 0.8.1
/** 1-D squared distance transform (Felzenszwalb & Huttenlocher 2012) of f into d (length n), with buffers v, z. */
function dt1(f, n, d, v, z) {
let k = 0;
v[0] = 0; z[0] = -INF; z[1] = INF;
for (let q = 1; q < n; q++) {
let s;
for (;;) { const p = v[k]; s = ((f[q] + q * q) - (f[p] + p * p)) / (2 * q - 2 * p); if (s <= z[k] && k > 0) k--; else break; }
if (s <= z[k]) { v[0] = q; z[0] = -INF; z[1] = INF; k = 0; continue; }
k++; v[k] = q; z[k] = s; z[k + 1] = INF;
}
k = 0;
for (let q = 0; q < n; q++) { while (z[k + 1] < q) k++; const p = v[k]; d[q] = (q - p) * (q - p) + f[p]; }
}
Distance to the nearest marked cell
64-bit
The straight-line distance, in cells, from every cell to the nearest cell of a mask.
columns then rows of the 1D transform; distance = √(squared distance)
Squared distances stay whole numbers through both passes and the square root is taken once at the end. The result is a distance between cell centers, so it differs from the true distance to the material's edge by up to about a cell.
cam-adaptive.js
lines 42 to 57
PartCraft 0.8.1
/** Euclidean distance (in cells) from every cell to the nearest cell where src is set. */
function edt(src, cols, rows) {
const n = Math.max(cols, rows), f = new Float64Array(n), d = new Float64Array(n), v = new Int32Array(n), z = new Float64Array(n + 1);
const g = new Float64Array(cols * rows);
for (let x = 0; x < cols; x++) {
for (let y = 0; y < rows; y++) f[y] = src[y * cols + x] ? 0 : INF;
dt1(f, rows, d, v, z);
for (let y = 0; y < rows; y++) g[y * cols + x] = d[y];
}
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) f[x] = g[y * cols + x];
dt1(f, cols, d, v, z);
for (let x = 0; x < cols; x++) g[y * cols + x] = Math.sqrt(d[x]);
}
return g;
}
Grid over the region
64-bit
Lays a grid over the region and stores, per cell, whether it is inside and its exact distance to the walls.
cell h = R ÷ 12 (× 1.25 until the grid fits); wall distance from the region's lines and arcs
Cell centers are x₀ + x × h, computed by multiplication. The wall distance at each center is exact (from the curves), not a grid distance.
cam-adaptive.js
lines 59 to 74
PartCraft 0.8.1
/** The raster of a region: cell centres, inside mask, exact distance to the walls. */
function rasterOf(loops, R, h0, maxCells) {
const [bx0, by0, bx1, by1] = O.boxOfLoops(loops);
const W = bx1 - bx0, Hh = by1 - by0;
let h = h0;
while ((W / h + 3) * (Hh / h + 3) > maxCells) h *= 1.25;
const cols = Math.ceil(W / h) + 3, rows = Math.ceil(Hh / h) + 3;
const x0 = bx0 - h, y0 = by0 - h;
const inside = new Uint8Array(cols * rows), wall = new Float64Array(cols * rows);
for (let y = 0; y < rows; y++) for (let x = 0; x < cols; x++) {
const q = [x0 + x * h, y0 + y * h], i = y * cols + x;
inside[i] = O.inside(loops, q) ? 1 : 0;
wall[i] = inside[i] ? O.distToLoops(q, loops) : 0;
}
return { h, cols, rows, x0, y0, inside, wall, at: (x, y) => [x0 + x * h, y0 + y * h] };
}
Contour of the distance field
64-bit
Traces the line where the field crosses zero, which becomes a pass.
crossing on a cell edge at t = a ÷ (a − b)
Linear interpolation between two cell values of opposite sign, so the divisor cannot be zero. The contour is resolved to the grid: the field between cell centers is assumed linear.
cam-adaptive.js
lines 76 to 118
PartCraft 0.8.1
/** Closed iso-contours {F = 0} of a cell-centre field (F > 0 outside the region of interest; the border is > 0). */
function contours(F, cols, rows) {
const segs = new Map(); // edge key → [other edge key, point] pairs via adjacency
const pts = new Map(), adj = new Map();
const ek = (x, y, dir) => (y * cols + x) * 2 + dir; // dir 0: edge to (x+1, y); 1: edge to (x, y+1)
const cross = (x, y, dir) => {
const k = ek(x, y, dir);
if (!pts.has(k)) {
const a = F[y * cols + x], b = dir ? F[(y + 1) * cols + x] : F[y * cols + x + 1], t = a / (a - b);
pts.set(k, dir ? [x, y + t] : [x + t, y]);
}
return k;
};
const link = (a, b) => { (adj.get(a) || adj.set(a, []).get(a)).push(b); (adj.get(b) || adj.set(b, []).get(b)).push(a); };
for (let y = 0; y + 1 < rows; y++) for (let x = 0; x + 1 < cols; x++) {
const v = [F[y * cols + x], F[y * cols + x + 1], F[(y + 1) * cols + x + 1], F[(y + 1) * cols + x]];
const c = (v[0] <= 0 ? 1 : 0) | (v[1] <= 0 ? 2 : 0) | (v[2] <= 0 ? 4 : 0) | (v[3] <= 0 ? 8 : 0);
if (c === 0 || c === 15) continue;
const E = [() => cross(x, y, 0), () => cross(x + 1, y, 1), () => cross(x, y + 1, 0), () => cross(x, y, 1)]; // bottom, right, top, left
const T = {
1: [[3, 0]], 2: [[0, 1]], 3: [[3, 1]], 4: [[1, 2]], 6: [[0, 2]], 7: [[3, 2]], 8: [[2, 3]], 9: [[2, 0]], 11: [[2, 1]], 12: [[1, 3]], 13: [[1, 0]], 14: [[0, 3]],
}[c];
if (T) { for (const [a, b] of T) link(E[a](), E[b]()); continue; }
const mid = (v[0] + v[1] + v[2] + v[3]) / 4;
if (c === 5) { if (mid <= 0) { link(E[3](), E[2]()); link(E[1](), E[0]()); } else { link(E[3](), E[0]()); link(E[1](), E[2]()); } }
else if (c === 10) { if (mid <= 0) { link(E[0](), E[3]()); link(E[2](), E[1]()); } else { link(E[0](), E[1]()); link(E[2](), E[3]()); } }
}
void segs;
const out = [], seen = new Set();
for (const start of adj.keys()) {
if (seen.has(start)) continue;
const loop = [];
let prev = null, cur = start;
for (let guard = 0; guard < 1e7; guard++) {
seen.add(cur); loop.push(pts.get(cur));
const nx = (adj.get(cur) || []).find((k) => k !== prev && !seen.has(k)) ?? null;
if (nx === null) break;
prev = cur; cur = nx;
}
if (loop.length >= 3) out.push(loop);
}
return out;
}
Field whose contour is the next pass
64-bit
Sets up the field that is zero where the tool center cuts the set width into the remaining material.
F = (R − w) − distance to material (allowed cells); positive elsewhere
The distance to material is the grid distance times the cell size. Cells where the tool may not go are given a positive value of at least 1 plus a cell, so the contour never enters them.
cam-adaptive.js
lines 215 to 217
PartCraft 0.8.1
// F ≤ 0: the cleared side at ≥ R − w from the material, and allowed
for (let i = 0; i < cols * rows; i++) Fv[i] = allowed[i] ? (R - w) - dM[i] * cell : Math.max(1, (reach + margin) - wall[i]) + cell;
const found = contours(Fv, cols, rows).map((l) => l.map(([x, y]) => [ras.x0 + x * cell, ras.y0 + y * cell]));
Keeping passes off the walls
64-bit
Moves any pass point that came nearer the walls than the tool radius plus stock to leave.
p ← nearest wall point + unit(p − nearest) × (R + leave + 1e-6)
The test and the correction use the exact distance to the region's curves, so the grid's interpolation error cannot put the tool into a wall. The 1e-6 mm added is a fixed margin.
cam-adaptive.js
lines 229 to 236
PartCraft 0.8.1
// never nearer the walls than R + leave (the raster's interpolation): nudge such points inward
lp = lp.map((p) => {
const d = O.distToLoops(p, loops);
if (d >= reach) return p;
const near = loops.flat().reduce((bq, pr) => { const cq = O.closestOnPrim(p, pr); return !bq || cq.d < bq.d ? cq : bq; }, null);
const ux = p[0] - near.p[0], uy = p[1] - near.p[1], ul = Math.hypot(ux, uy) || 1;
return [near.p[0] + ux / ul * (reach + 1e-6), near.p[1] + uy / ul * (reach + 1e-6)];
});
Simplifying a pass
64-bit
Drops points of a pass that lie within a tolerance of the line between their neighbors.
keep the point farthest from the chord when its distance > cell ÷ 4, then repeat on both halves
Point-to-line distance by a 2D cross product divided by the chord length (1 when the chord has no length). The pass can move by up to a quarter cell.
cam-adaptive.js
lines 120 to 145
PartCraft 0.8.1
/** Douglas–Peucker on a closed loop. */
function simplify(loop, tol) {
if (loop.length < 4) return loop;
const keep = new Uint8Array(loop.length);
const rec = (a, b) => {
let worst = -1, wi = -1;
const A = loop[a], B = loop[b], dx = B[0] - A[0], dy = B[1] - A[1], L = Math.hypot(dx, dy) || 1;
for (let i = a + 1; i < b; i++) { const d = Math.abs((loop[i][0] - A[0]) * dy - (loop[i][1] - A[1]) * dx) / L; if (d > worst) { worst = d; wi = i; } }
if (worst > tol) { keep[wi] = 1; rec(a, wi); rec(wi, b); }
};
const far = loop.reduce((bi, p, i) => (Math.hypot(p[0] - loop[0][0], p[1] - loop[0][1]) > Math.hypot(loop[bi][0] - loop[0][0], loop[bi][1] - loop[0][1]) ? i : bi), 0);
keep[0] = keep[far] = 1;
rec(0, far);
const tail = [...loop.slice(far), loop[0]];
const keep2 = new Uint8Array(tail.length);
const rec2 = (a, b) => {
let worst = -1, wi = -1;
const A = tail[a], B = tail[b], dx = B[0] - A[0], dy = B[1] - A[1], L = Math.hypot(dx, dy) || 1;
for (let i = a + 1; i < b; i++) { const d = Math.abs((tail[i][0] - A[0]) * dy - (tail[i][1] - A[1]) * dx) / L; if (d > worst) { worst = d; wi = i; } }
if (worst > tol) { keep2[wi] = 1; rec2(a, wi); rec2(wi, b); }
};
rec2(0, tail.length - 1);
const out = loop.filter((_, i) => keep[i] && i <= far);
for (let i = 1; i < tail.length - 1; i++) if (keep2[i]) out.push(tail[i]);
return out;
}
Marking what a pass clears
64-bit
Marks every grid cell the tool covers along a pass.
cell cleared when its center is within R of the segment
Squared distances are compared with R², so no square root is taken. A cell is cleared when its center is covered; partly covered cells follow their center.
cam-adaptive.js
lines 147 to 165
PartCraft 0.8.1
/** Mark every cell within r of the polyline (closed when `closed`). */
function sweep(mask, ras, poly, r, closed) {
const { h, cols, rows, x0, y0 } = ras, rr = r * r;
const n = poly.length, m = closed ? n : n - 1;
for (let i = 0; i < m; i++) {
const a = poly[i], b = poly[(i + 1) % n];
const xa = Math.max(0, Math.floor((Math.min(a[0], b[0]) - r - x0) / h)), xb = Math.min(cols - 1, Math.ceil((Math.max(a[0], b[0]) + r - x0) / h));
const ya = Math.max(0, Math.floor((Math.min(a[1], b[1]) - r - y0) / h)), yb = Math.min(rows - 1, Math.ceil((Math.max(a[1], b[1]) + r - y0) / h));
const dx = b[0] - a[0], dy = b[1] - a[1], L2 = dx * dx + dy * dy;
for (let y = ya; y <= yb; y++) for (let x = xa; x <= xb; x++) {
const i2 = y * cols + x;
if (mask[i2]) continue;
const px = x0 + x * h - a[0], py = y0 + y * h - a[1];
const t = L2 > 0 ? Math.max(0, Math.min(1, (px * dx + py * dy) / L2)) : 0;
const ex = px - t * dx, ey = py - t * dy;
if (ex * ex + ey * ey <= rr) mask[i2] = 1;
}
}
}
Engagement check
64-bit
Re-simulates the passes on a finer grid and reports the largest engagement angle, the equivalent cutting width, uncut area and the smallest wall clearance.
angle = 360 × (rim points in material) ÷ 360; width = R (1 − cos angle); gouge = min(dist to walls − R − leave)
The angle is a count of 360 rim points, so it comes in whole degrees, measured one cell inside the rim at steps of one cell. The wall clearance uses the exact distance to the curves. Uncut area is a cell count times the cell area.
cam-adaptive.js
lines 281 to 330
PartCraft 0.8.1
/** Re-simulate the passes on a separate, finer raster: the engagement (material area removed per mm of path) and what is left. */
export function engagementCheck(loops, { R, entry, passes, closed = null, links = null, leave = 0, finish = null }, { h = R / 20 } = {}) {
const ras = rasterOf(loops, R, h, 2e6), { cols, rows, inside, wall } = ras, cell = ras.h;
const allowed = new Uint8Array(cols * rows);
for (let i = 0; i < cols * rows; i++) allowed[i] = inside[i] && wall[i] >= R + leave ? 1 : 0;
const dA = edt(allowed, cols, rows);
const mat = new Uint8Array(cols * rows);
for (let i = 0; i < cols * rows; i++) mat[i] = inside[i] ? 1 : 0;
const clearDisk = (p, r) => {
let n = 0;
const xa = Math.max(0, Math.floor((p[0] - r - ras.x0) / cell)), xb = Math.min(cols - 1, Math.ceil((p[0] + r - ras.x0) / cell));
const ya = Math.max(0, Math.floor((p[1] - r - ras.y0) / cell)), yb = Math.min(rows - 1, Math.ceil((p[1] + r - ras.y0) / cell));
for (let y = ya; y <= yb; y++) for (let x = xa; x <= xb; x++) { const i = y * cols + x; if (!mat[i]) continue; const dx = ras.x0 + x * cell - p[0], dy = ras.y0 + y * cell - p[1]; if (dx * dx + dy * dy <= r * r) { mat[i] = 0; n++; } }
return n * cell * cell;
};
clearDisk(entry.c, R + entry.r);
// the cutter's rim, sampled: the arc in material before each step cuts = the engagement angle there
const NR = 360, rim = Array.from({ length: NR }, (_, k) => [Math.cos(2 * Math.PI * k / NR), Math.sin(2 * Math.PI * k / NR)]);
const inMat = (q) => { const x = Math.round((q[0] - ras.x0) / cell), y = Math.round((q[1] - ras.y0) / cell); return x >= 0 && y >= 0 && x < cols && y < rows && mat[y * cols + x] === 1; };
const rr = R - cell; // just inside the rim, so the cells the step is about to cut are the ones counted
let angle = 0, sumA = 0, nA = 0, gouge = Infinity, at = null, pos = [entry.c[0] + entry.r, entry.c[1]], inCut = 0, pathLen = 0;
const step = cell;
const along = (a, b, pi) => {
const L = Math.hypot(b[0] - a[0], b[1] - a[1]), n = Math.max(1, Math.ceil(L / step));
for (let k = 1; k <= n; k++) {
const p = [a[0] + (b[0] - a[0]) * k / n, a[1] + (b[1] - a[1]) * k / n];
let m = 0;
for (const [cx, cy] of rim) if (inMat([p[0] + cx * rr, p[1] + cy * rr])) m++;
const ang = 360 * m / NR;
if (ang > angle) { angle = ang; at = { pass: pi, p }; }
sumA += ang; nA++; if (ang > 0) inCut += L / n; pathLen += L / n;
clearDisk(p, R);
}
};
for (const [pi, lp] of passes.entries()) {
const isClosed = closed ? closed[pi] : true;
if (!links || pi === 0 || links[pi - 1] !== 'up') along(pos, lp[0], pi); else clearDisk(lp[0], R);
for (let i = 0; i < (isClosed ? lp.length : lp.length - 1); i++) {
const a = lp[i], b = lp[(i + 1) % lp.length];
gouge = Math.min(gouge, O.distToLoops(a, loops) - R - leave);
along(a, b, pi);
}
pos = isClosed ? lp[0] : lp[lp.length - 1];
}
// the wall pass (exact, at R + leave) the operation ends a level with
for (const wl of finish || []) { for (const pr of wl) { const n = Math.max(1, Math.ceil(O.lengthOf(pr) / (cell / 2))); for (let k = 0; k <= n; k++) clearDisk(O.pointAt(pr, k / n), R); } }
let uncut = 0;
for (let i = 0; i < cols * rows; i++) if (mat[i] && dA[i] * cell <= R - 1.5 * cell) uncut++;
return { angle, width: R * (1 - Math.cos(Math.min(180, angle) * Math.PI / 180)), meanAngle: nA ? sumA / nA : 0, uncut: uncut * cell * cell, gouge, at, pathLen, inCut };
}
In detail
Method
Euclidean distance transform (the code cites Felzenszwalb and Huttenlocher 2012), contour of the distance field by marching squares, Douglas–Peucker simplification, as described in the file's header.
Number types
64-bit numbers and Float64Array for distances; Uint8Array for masks; Int32Array for indexes.
Tolerances
Grid cell R ÷ 12, enlarged by 1.25 until the grid has at most 300,000 cells. Allowed zone keeps half a cell of margin. Simplification tolerance cell ÷ 4. Wall guard: points nearer than R + leave are moved to R + leave + 1e-6 mm. Check grid: cell R ÷ 20, at most 2,000,000 cells; rim sampled at 360 points.
Limits
Pass positions and the reported leftover area are grid values. The engagement angle from the check has a resolution of 1° and one cell.
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.