Generates the tool moves for facing, pockets, contours, drilling and chamfering: depth levels, entry helixes and ramps, lead arcs, tabs and hole depths.
How the precision is kept
XY moves follow the exact lines and arcs from the offset code, in 64-bit. Depths are divided evenly between top and bottom with the last level set to the bottom itself. Helical and ramp moves split the height in proportion to the length traveled. Clearance checks along straight links are sampled at fixed spacings.
Calculations under this policy
17 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 heights of the passes between the top and the bottom of a cut.
n = ceil((top − bottom) ÷ stepdown − 1e-9); z_i = top − (top − bottom)(i + 1) ÷ n; last = bottom
Levels are computed from the ends each time, not by repeated subtraction of the stepdown, and the last level is the bottom value itself.
cam-paths.js
lines 71 to 75
PartCraft 0.8.1
function levels(top, bottom, step) {
if (!(top > bottom + 1e-9)) return [];
const n = Math.max(1, Math.ceil((top - bottom) / step - 1e-9));
return Array.from({ length: n }, (_, i) => (i === n - 1 ? bottom : top - ((top - bottom) * (i + 1)) / n));
}
Clearance, retract and cut heights
64-bit
The heights an operation moves at, from the stock top and the operation's settings.
clearance = stock top + clearance; retract = min(clearance, stock top + retract); bottom = geometry bottom + offset
Sums of stored values; nothing is rounded.
cam-paths.js
lines 77 to 83
PartCraft 0.8.1
function heights(op, ctx, geomTop, geomBottom) {
const p = op.params, st = ctx.model.stock.max[2];
const top = p.top ?? geomTop ?? st;
const bottom = (p.bottom ?? geomBottom) + (p.bottom === null ? p.bottomOff : 0);
const clearance = st + p.clearance;
return { clearance, retract: Math.min(clearance, st + p.retract), feed: top + p.feedHeight, top, bottom };
}
Arc move with changing height
64-bit
Writes one line or arc as moves, splitting an arc into half turns and sharing the height change among them.
z after a piece = z + (z₁ − z₀) × piece length ÷ arc length; last piece ends at z₁
The final piece's end height is set to z₁ itself, so rounding in the proportional split does not reach the end point.
cam-paths.js
lines 49 to 61
PartCraft 0.8.1
/** One prim at z0 → z1 (lines become G1, arcs G2/G3, split ≤ 180°). */
prim(p, z0, z1, f) {
if (p.k === 'L') { this.feed(p.e[0], p.e[1], z1, f); return; }
const parts = O.limitArcs([p]);
let z = z0;
const L = O.lengthOf(p);
for (const q of parts) {
const zn = parts.length === 1 ? z1 : z + (z1 - z0) * (O.lengthOf(q) / L);
const end = [q.e[0], q.e[1], q === parts[parts.length - 1] ? z1 : zn];
if (!this.same(end)) { this.moves.push({ k: 'arc', p: end, c: q.c.slice(), sw: q.sw, f }); this.pos = end; }
z = zn;
}
}
Ramp along a path
64-bit
Lowers the tool linearly while it follows a chain of lines and arcs.
z = z₀ + (z₁ − z₀) × length so far ÷ total length
The running length is a sum added in order; the height at each joint is computed from that sum, not by adding height steps.
cam-paths.js
lines 63 to 68
PartCraft 0.8.1
/** Follow prims while z goes linearly from z0 to z1 over their length. */
ramp(prims, z0, z1, f) {
const L = prims.reduce((s, p) => s + O.lengthOf(p), 0);
let acc = 0;
for (const p of prims) { const a = z0 + (z1 - z0) * (acc / L); acc += O.lengthOf(p); this.prim(p, a, z0 + (z1 - z0) * (acc / L), f); }
}
Facing rows
64-bit
The positions of the back-and-forth rows that face the stock.
n = ceil(width ÷ (stepover × d) − 1e-9); row_i = b₀ + (b₁ − b₀) i ÷ n; run from a₀ − R − overhang to a₁ + R + overhang
Rows are evenly spaced between the stock's two sides, each computed from the ends.
How far along the first loop the tool travels while ramping down a level.
length needed = (z_from − z) ÷ tan(angle); whole loops = floor(needed ÷ L); remainder split off the loop
The remainder is needed − turns × L, a subtraction of nearly equal numbers when the length needed is close to a whole number of loops; a remainder below 1e-9 mm is dropped.
cam-paths.js
lines 336 to 344
PartCraft 0.8.1
const need = (zFrom - z) / Math.tan((p.rampAngle * Math.PI) / 180);
const L = O.loopLength(loop);
const turns = Math.floor(need / L);
const prims = [];
for (let i = 0; i < turns; i++) prims.push(...loop);
const rest = need - turns * L;
const tail = rest > 1e-9 ? O.splitAtLengths(loop, [rest])[0] : [];
prims.push(...tail);
P.ramp(prims, zFrom, z, sp.ramp);
Contour offset
64-bit
The path of the tool center round an outline.
d = R + stock to leave; outside: offset by −d (grow); inside: offset by +d
One exact offset of the outline. The cut direction is set by reversing the loops, which does not change their geometry.
cam-paths.js
lines 392 to 394
PartCraft 0.8.1
const d = R + p.leave;
const grow = set.side === 'outside';
let paths = O.offsetRegion(set.loops, grow ? -d : d);
Lead-in and lead-out arcs
64-bit
Quarter-circle arcs that bring the tool onto and off a contour through the air side.
center = start + ρ × left normal; quarter turn ending (or starting) at the contour's start; ρ halved up to 4 times until the arc is in air
The arcs are exact; whether they are clear of material is tested at points sampled along them at a chord tolerance of 0.01 mm. A radius of 0.01 mm or less gives no lead.
cam-paths.js
lines 455 to 471
PartCraft 0.8.1
/** Quarter-circle lead-in/out arcs on the air side (the tool's left: climb puts material on the right). */
function leadArcs(loop, rho, air) {
for (let k = 0; k < 4 && rho > 0.01; k++, rho /= 2) {
const P0 = loop[0].s, t0 = O.tangentAt(loop[0], 0);
const last = loop[loop.length - 1], t1 = O.tangentAt(last, 1);
const nA0 = [-t0[1], t0[0]], nA1 = [-t1[1], t1[0]];
const c0 = [P0[0] + rho * nA0[0], P0[1] + rho * nA0[1]];
const a0 = Math.atan2(P0[1] - c0[1], P0[0] - c0[0]);
const inn = O.arc(c0, rho, a0 - Math.PI / 2, Math.PI / 2, null, P0);
const c1 = [P0[0] + rho * nA1[0], P0[1] + rho * nA1[1]];
const a1 = Math.atan2(P0[1] - c1[1], P0[0] - c1[0]);
const out = O.arc(c1, rho, a1, Math.PI / 2, P0, null);
const pts = [...O.samplePrim(inn, 0.01).slice(0, -1), ...O.samplePrim(out, 0.01).slice(1)];
if (pts.every(air)) return { inn, out };
}
return null;
}
Holding tabs
64-bit
Where the tool lifts over tabs that keep the part attached.
tab k centered at (k + ½) L ÷ tabs; lifted over tab width + tool diameter; tab top = bottom + tab height
Tab positions are fractions of the loop length computed from the total each time. The lifted length adds one tool diameter so the tab left standing has the width asked for.
cam-paths.js
lines 472 to 486
PartCraft 0.8.1
function cutWithTabs(P, loop, z, set, h, p, tool, sp) {
const tabTop = h.bottom + p.tabHeight;
if (!(p.tabs > 0) || z >= tabTop - 1e-9 || set.side !== 'outside') { P.loop(loop, z, sp.feed); return; }
const L = O.loopLength(loop), w = p.tabWidth + tool.d;
if (w * p.tabs >= L) { P.loop(loop, z, sp.feed); return; }
const cuts = [];
for (let k = 0; k < p.tabs; k++) { const c = ((k + 0.5) * L) / p.tabs; cuts.push(c - w / 2, c + w / 2); }
const pieces = O.splitAtLengths(loop, cuts);
pieces.forEach((pc, i) => {
const onTab = i % 2 === 1;
if (!pc.length) return;
if (onTab) { P.feedZ(tabTop, sp.plunge); P.loop(pc, tabTop, sp.feed); P.feedZ(z, sp.plunge); }
else P.loop(pc, z, sp.feed);
});
}
Drill depth and R plane
64-bit
How deep each hole is drilled and the height the drill returns to between holes.
through: z = bottom − breakthrough − (tip length when full diameter is asked); R = min(retract, highest stock or part near the hole + R plane)
The tip length R ÷ tan(half point angle) comes from the tool profile. The hole's top and bottom come from the machine model: the exact edge height when one was within tolerance, otherwise the mesh's.
The highest part or stock under the drill as it moves from one hole to the next.
max of heights at 9 points across the tool, every 1 mm along the move
A sampled check: the move is tested at steps of at most 1 mm and at the center, four axis points and four diagonal points at 0.7 × the radius. The part heights are the mesh heights from cam.js.
cam-paths.js
lines 538 to 548
PartCraft 0.8.1
function sweptTop(a, b, r, ctx) {
const n = Math.max(2, Math.ceil(O.dist(a, b) / 1));
let m = -Infinity;
for (let i = 0; i <= n; i++) {
const q = [a[0] + (b[0] - a[0]) * i / n, a[1] + (b[1] - a[1]) * i / n];
for (const [dx, dy] of [[0, 0], [r, 0], [-r, 0], [0, r], [0, -r], [0.7 * r, 0.7 * r], [-0.7 * r, 0.7 * r], [0.7 * r, -0.7 * r], [-0.7 * r, -0.7 * r]]) {
m = Math.max(m, ctx.model.heightAt(q[0] + dx, q[1] + dy), ctx.stockAt(q[0] + dx, q[1] + dy, 0));
}
}
return m;
}
Chamfer depth and offset
64-bit
How deep and how far out a chamfer mill runs to cut a chamfer of a given width.
β = tool angle ÷ 2; depth = width ÷ tan β; offset = tip radius + tip offset × tan β; z = edge height − depth − tip offset
Closed-form cone geometry. The angle is converted with Math.PI ÷ 180. The check that the cone is wide enough has an absolute slack of 1e-9 mm.
The angle of the cutter's rim in material for a set cutting width, listed with the operation.
angle = acos(1 − min(1, w ÷ R)) × 180 ÷ π
The ratio is limited to 1 before acos. For a very small width, 1 − w ÷ R is close to 1, where acos loses digits; the widths used in practice are a sizable fraction of the radius.
Sample points along an arc move for the simulation overlay and checks.
step angle = 2 acos(1 − tol ÷ r); point i at a₀ + sweep × i ÷ n, z linear
The last point is the segment's stored end. Other points are computed from the center and the start radius.
cam-paths.js
lines 695 to 708
PartCraft 0.8.1
/** Points along one segment (arcs sampled at chord tolerance tol). */
export function segPoints(s, tol = 0.01) {
if (s.k !== 'arc') return [s.a, s.b];
const r = Math.hypot(s.a[0] - s.c[0], s.a[1] - s.c[1]);
const a0 = Math.atan2(s.a[1] - s.c[1], s.a[0] - s.c[0]);
const da = r > tol ? 2 * Math.acos(Math.max(-1, 1 - tol / r)) : Math.PI / 4;
const n = Math.max(2, Math.ceil(Math.abs(s.sw) / da));
const out = [];
for (let i = 0; i <= n; i++) {
const a = a0 + (s.sw * i) / n;
out.push(i === n ? s.b : [s.c[0] + r * Math.cos(a), s.c[1] + r * Math.sin(a), s.a[2] + ((s.b[2] - s.a[2]) * i) / n]);
}
return out;
}
In detail
Method
Offset clearing inside-out, contour at tool radius plus stock to leave, canned drilling cycles, chamfer by cone geometry, as described in the file's header.
Number types
64-bit numbers; moves are plain arrays [x, y, z].
Tolerances
A move shorter than 1e-9 mm in every axis is not emitted. Level counts use ceil(x − 1e-9). Stay-down links are checked every 0.25 mm against the walls with a 1e-6 mm slack. Holes match a requested diameter within 1e-3 mm. Drill links are checked every 1 mm at nine points across the tool.
Limits
Sampled link checks can miss an obstacle narrower than the sample spacing. Heights of the part between holes come from the mesh height query in cam.js.
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.