CAM routing refactor, area operation, and taper ball tool implementation

Major routing improvements:
- Refactor depth-first routing algorithm for better path optimization
- Reimplemented outline, roughing, pocket, and trace as area operation wrappers
- Add pocket shadow travel routing with optimized descent using travel bounds
- Improve contour routing with arc detection and polyEmit arc strategy
- Add area smoothing and ease-down options
- Fix outline handling with thru holes and surface reversal on tip2tip
- Move widget shadow methods into widget class for faster hole detection
- Better camInnerFirst support and proper feed/plunge defaults

Tool system improvements:
- Implement taper ball tool type with full support
- Add renderTool2() for profile-based visualization
- Convert tool menu to init code with bound vars
- Fix auto tool and auto spindle for prep operations
- Update tool profile generation for taperball geometry
- Add calcTaperBallExtent() for proper ball tangency calculations

Code cleanup:
- Remove duplicated slicer code from specialized slicers
- Asyncify shadowAt operations
- Deprecate pocket engrave operation
- Simplify upNover implementation
- Fix animation with new tool+settings requirements
This commit is contained in:
Stewart Allen 2025-12-06 00:09:17 -05:00
commit e0b1c4e31f
44 changed files with 1607 additions and 2894 deletions

View file

@ -1,16 +1,7 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from './op.js';
import { Tool } from './tool.js';
import { generate as Topo } from './topo3.js';
import { newPolygon } from '../../../geo/polygon.js';
import { newSlice } from '../../core/slice.js';
import { polygons as POLY } from '../../../geo/polygons.js';
import { util as base_util } from '../../../geo/base.js';
import { calc_normal, calc_vertex } from '../../../geo/paths.js';
import { CAM } from './driver-be.js';
const DEG2RAG = Math.PI / 180;
import { OpArea } from './op-area.js';
class OpPocket extends CamOp {
constructor(state, op) {
@ -18,253 +9,37 @@ class OpPocket extends CamOp {
}
async slice(progress) {
const pocket = this;
let { op, state } = this;
let { tool, rate, down, plunge, expand, contour, smooth, tolerance } = op;
let { ov_botz, ov_conv } = op;
let { settings, widget, addSlices, zBottom, tabs, color } = state;
let { updateToolDiams, cutTabs, healPolys, shadowAt, workarea } = state;
zBottom = ov_botz ? workarea.bottom_stock + ov_botz : zBottom;
// generate tracing offsets from chosen features
let sliceOut;
let pockets = this.pockets = [];
let camTool = new Tool(settings, tool);
let toolDiam = camTool.fluteDiameter();
let toolOver = toolDiam * op.step;
let cutdir = ov_conv;
let engrave = contour && op.engrave;
let zTop = workarea.top_z;
let devel = settings.controller.devel;
let smoothVal = (smooth ?? 0) / 10;
if (contour) {
down = 0;
this.contour = {
axis: "-",
inside: true,
nogpu: true,
step: toolOver,
tolerance,
tool,
};
}
updateToolDiams(toolDiam);
if (tabs) {
tabs.forEach(tab => {
tab.off = POLY.expand([tab.poly], toolDiam / 2).flat();
});
}
function newPocket() {
pockets.push(sliceOut = []);
}
function newSliceOut(z) {
let slice = newSlice(z);
sliceOut.push(slice);
return slice;
}
async function clearZ(polys, z, down) {
if (down) {
// adjust step down to a value <= down that
// ends on the lowest z specified
let diff = zTop - z;
down = diff / Math.ceil(diff / down);
}
let zs = down ? base_util.lerp(zTop, z, down) : [ z ];
if (engrave) {
toolDiam = toolOver;
}
if (contour) {
expand = engrave ? 0 : expand;
} else if (expand) {
polys = POLY.offset(polys, expand);
}
let zpro = 0, zinc = 1 / (polys.length * zs.length);
for (let poly of polys) {
newPocket();
for (let z of zs) {
let clip = [], shadow;
if (contour) {
if (smooth) {
clip = POLY.offset(POLY.offset([ poly ], smoothVal), -smoothVal);
} else {
clip = [ poly ];
}
} else {
shadow = shadowAt(z);
if (smooth) {
shadow = POLY.setZ(POLY.offset(POLY.offset(shadow, smoothVal), -smoothVal), z);
}
POLY.subtract([ poly ], shadow, clip, undefined, undefined, 0);
if (op.outline) {
POLY.clearInner(clip);
}
}
if (clip.length === 0) {
continue;
}
let slice = newSliceOut(z);
let count = engrave ? 1 : 999;
slice.camTrace = { tool, rate, plunge };
if (toolDiam) {
const offs = contour ?
[ expand || (-0.02), -toolOver ] :
[ -toolDiam / 2, -toolOver ];
POLY.offset(clip, offs, {
count, outs: slice.camLines = [], flat:true, z, minArea: 0
});
} else {
// when engraving with a 0 width tip
slice.camLines = clip;
}
if (tabs) {
slice.camLines = cutTabs(tabs, POLY.flatten(slice.camLines, null, true), z);
} else {
slice.camLines = POLY.flatten(slice.camLines, null, true);
}
POLY.setWinding(slice.camLines, cutdir, false);
if (contour) {
slice.camLines = await pocket.conform(slice.camLines, op.refine, engrave, pct => {
progress(0.9 + (zpro + zinc * pct) * 0.1, "conform");
});
}
slice.output()
.setLayer(state.layername, {line: color}, false)
.addPolys(slice.camLines)
if (devel && shadow) slice.output()
.setLayer("pocket shadow", {line: 0xff8811}, false)
.addPolys(shadow);
if (!contour) {
progress(zpro, "pocket");
}
zpro += zinc;
addSlices(slice);
}
}
}
let surfaces = op.surfaces[widget.id] || [];
let vert = widget.getGeoVertices({ unroll: true, translate: true }).map(v => v.round(4));
// let vert = widget.getVertices().array.map(v => v.round(4));
let outline = [];
let faces = CAM.surface_find(widget, surfaces, (op.follow || 5) * DEG2RAG);
let zmin = Infinity;
let j=0, k=faces.length;
for (let face of faces) {
let i = face * 9;
outline.push(newPolygon()
.add(vert[i++], vert[i++], zmin = Math.min(zmin, vert[i++]))
.add(vert[i++], vert[i++], zmin = Math.min(zmin, vert[i++]))
.add(vert[i++], vert[i++], zmin = Math.min(zmin, vert[i++]))
);
}
zmin = Math.max(zBottom, zmin);
outline = POLY.union(outline, 0.0001, true);
outline = POLY.setWinding(outline, cutdir, false);
outline = healPolys(outline);
if (smooth) {
outline = POLY.offset(POLY.offset(outline, smoothVal), -smoothVal);
}
if (outline.length) {
// option to skip interior features (holes, pillars)
if (op.outline) {
POLY.clearInner(outline);
}
await clearZ(outline, zmin + 0.0001, down);
if (devel && sliceOut?.length) sliceOut[0].output()
.setLayer("pocket area", {line: 0x1188ff}, false)
.addPolys(outline)
progress(1, "pocket");
}
}
// mold cam output lines to the surface of the topo offset by tool geometry
async conform(camLines, refine, engrave, progress) {
if (!this.topo) {
console.log('deferred topo');
this.topo = await Topo({
// onupdate: (update, msg) => {
onupdate: (index, total, msg) => {
progress((index / total) * 0.9, msg);
},
ondone: (slices) => {
// console.log({ contour: slices });
},
contour: this.contour,
state: this.state
});
}
const topo = this.topo;
// re-segment polygon to a higher resolution
const hirez = camLines.map(p => p.segment(topo.tolerance * 2));
// walk points and offset from surface taking into account tool geometry
let steps = hirez.length;
let iter = 0;
for (let poly of hirez) {
for (let point of poly.points) {
point.z = engrave ? topo.zAtXY(point.x, point.y) : topo.toolAtXY(point.x, point.y);
}
progress((iter++ / steps) * 0.8);
}
steps = steps * refine;
iter = 0;
// walk points noting z deltas and smoothing z sawtooth patterns
for (let j=0; j<refine; j++) {
for (let poly of hirez) {
const points = poly.points, length = points.length;
let sn = []; // segment normals
for (let i=0; i<length; i++) {
let p1 = points[i];
let p2 = points[(i + 1) % length];
sn.push(calc_normal(p1, p2));
}
let vn = []; // vertex normals
for (let i=0; i<length; i++) {
let n1 = sn[(i + length - 1) % length];
let n2 = sn[i];
let vi = calc_vertex(n1, n2, 1);
vn.push(vi);
let vl = Math.abs(1 - vi.vl).round(2);
// vl should be close to zero on smooth / continuous curves
// factoring out hard turns, we smooth the z using the weighted
// z values of the points before and after the current point
if (vl === 0) {
let p0 = points[(i + length - 1) % length];
let p1 = points[i];
let p2 = points[(i + 1) % length];
p1.z = (p0.z + p2.z + p1.z) / 3;
}
}
progress((iter++ / steps) * 0.2 + 0.8);
}
}
// return hirez.map(p => p.midpoints(topo.tolerance * 8));
return hirez;
let { contour, down, expand, follow, outline, ov_botz, ov_topz } = op;
let { plunge, rate, refine, smooth, spindle, surfaces, tolerance, tool } = op;
let pocket = {
areas: {},
down,
expand,
follow,
mode: contour ? 'surface' : 'clear',
outline,
ov_botz,
ov_topz,
over: op.step,
plunge,
rate,
refine,
rename: op.rename ?? "pocket",
smooth,
spindle,
sr_type: 'offset',
surfaces,
tolerance,
tool,
tr_type: 'none',
};
this.op_pocket = new OpArea(state, pocket);
return this.op_pocket.slice(progress);
}
prepare(ops, progress) {
let { op, state, pockets } = this;
let { pocket, setTool, setSpindle, setTolerance } = ops;
let { process } = state.settings;
setTool(op.tool, op.rate);
setSpindle(op.spindle);
if (this.topo) {
setTolerance(this.topo.tolerance);
}
// eliminate empty pockets
pockets = pockets.filter(p => p.length);
// naive output order
for (let slices of pockets) {
pocket({
cutdir: op.ov_conv,
depthFirst: process.camDepthFirst && !state.isIndexed,
easeDown: op.down && process.easeDown ? op.down : 0,
progress: (n,m) => progress(n/m, "pocket"),
slices
});
}
return this.op_pocket.prepare(ops, progress);
}
}