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

@ -4,7 +4,7 @@ import { $ } from '../../../moto/webui.js';
import { api } from '../../core/api.js';
import { consts } from '../../core/consts.js';
import { settings as setconf } from '../../core/settings.js';
import { calcTaperLength, calcTaperAngle } from './tool.js';
import { Tool, calcTaperAngle, calcTaperBallExtent, calcTaperLength } from './tool.js';
const DEG2RAD = Math.PI / 180;
@ -12,8 +12,7 @@ let { MODES } = consts,
DOC = document,
selectedTool = null,
editTools = null,
maxTool = 0,
toolNames = ['endmill','ballmill','tapermill','drill'];
maxTool = 0;
function settings() {
return api.conf.get();
@ -35,27 +34,110 @@ function renderTools() {
function selectTool(tool) {
const { ui } = api;
selectedTool = tool;
ui.toolName.value = tool.name;
ui.toolNum.value = tool.number;
ui.toolFluteDiam.value = tool.flute_diam;
ui.toolFluteLen.value = tool.flute_len;
ui.toolShaftDiam.value = tool.shaft_diam;
ui.toolShaftLen.value = tool.shaft_len;
ui.toolTaperTip.value = tool.taper_tip || 0;
ui.toolMetric.checked = tool.metric;
ui.toolType.selectedIndex = toolNames.indexOf(tool.type);
if (tool === 'tapermill') {
ui.toolTaperAngle.value = calcTaperAngle(
(tool.flute_diam - tool.taper_tip) / 2, tool.flute_len
).round(1);
} else if(tool === 'drill'){
ui.toolTaperAngle.value = 118;
api.util.rec2ui({
toolName: selectedTool.name,
toolType: selectedTool.type,
toolNum: selectedTool.number,
toolMetric: selectedTool.metric,
toolShaftDiam: selectedTool.shaft_diam,
toolShaftLen: selectedTool.shaft_len,
toolFluteDiam: selectedTool.flute_diam,
toolFluteLen: selectedTool.flute_len,
toolTaperAngle: selectedTool.taper_angle,
toolTaperTip: selectedTool.taper_tip,
},{
toolName: ui.toolName,
toolType: ui.toolType,
toolNum: ui.toolNum,
toolMetric: ui.toolMetric,
toolShaftDiam: ui.toolShaftDiam,
toolShaftLen: ui.toolShaftLen,
toolFluteDiam: ui.toolFluteDiam,
toolFluteLen: ui.toolFluteLen,
toolTaperAngle: ui.toolTaperAngle,
toolTaperTip: ui.toolTaperTip,
});
if (tool.type === 'tapermill' || tool.type === 'taperball') {
const taperLen = tool.flute_len;
ui.toolTaperAngle.value =
selectedTool.taper_angle =
calcTaperAngle( (tool.flute_diam - tool.taper_tip) / 2, taperLen ).round(1);
} else if (tool === 'drill') {
ui.toolTaperAngle.value = selectedTool.taper_angle = 90;
} else {
ui.toolTaperAngle.value = 0;
ui.toolTaperAngle.value = selectedTool.taper_angle = 0;
}
renderTool(tool);
}
export function updateTool(ev) {
const { ui } = api;
let changed = {
toolName: selectedTool.name,
toolType: selectedTool.type,
toolNum: selectedTool.number,
toolMetric: selectedTool.metric,
toolShaftDiam: selectedTool.shaft_diam,
toolShaftLen: selectedTool.shaft_len,
toolFluteDiam: selectedTool.flute_diam,
toolFluteLen: selectedTool.flute_len,
toolTaperAngle: selectedTool.taper_angle,
toolTaperTip: selectedTool.taper_tip,
};
api.util.ui2rec(changed,{
toolName: ui.toolName,
toolType: ui.toolType,
toolNum: ui.toolNum,
toolMetric: ui.toolMetric,
toolShaftDiam: ui.toolShaftDiam,
toolShaftLen: ui.toolShaftLen,
toolFluteDiam: ui.toolFluteDiam,
toolFluteLen: ui.toolFluteLen,
toolTaperAngle: ui.toolTaperAngle,
toolTaperTip: ui.toolTaperTip,
});
selectedTool.name = changed.toolName;
selectedTool.type = changed.toolType;
selectedTool.number = changed.toolNum;
selectedTool.metric = changed.toolMetric;
selectedTool.shaft_diam = changed.toolShaftDiam;
selectedTool.shaft_len = changed.toolShaftLen;
selectedTool.flute_diam = changed.toolFluteDiam;
selectedTool.flute_len = changed.toolFluteLen;
selectedTool.taper_angle = changed.toolTaperAngle;
selectedTool.taper_tip = changed.toolTaperTip;
if (selectedTool.type === 'tapermill' || selectedTool.type === 'taperball') {
const rad = (selectedTool.flute_diam - selectedTool.taper_tip) / 2;
const ballRadius = selectedTool.type === 'taperball' ? selectedTool.taper_tip / 2 : 0;
if (ev && ev.target === ui.toolTaperAngle) {
const angle = parseFloat(ev.target.value || 5);
const len = calcTaperLength(rad, angle * DEG2RAD);
selectedTool.flute_len = len + ballRadius;
ui.toolTaperAngle.value = angle.round(1);
ui.toolFluteLen.value = selectedTool.flute_len.round(4);
} else {
const taperLen = selectedTool.flute_len - ballRadius;
ui.toolTaperAngle.value =
selectedTool.taper_angle =
calcTaperAngle(rad, taperLen).round(1);
}
} else {
ui.toolTaperAngle.value = selectedTool.taper_angle = 0;
}
renderTools();
setToolChanged(true);
renderTool(selectedTool);
}
function otag(o) {
if (Array.isArray(o)) {
let out = []
@ -77,157 +159,89 @@ function otag(o) {
function renderTool(tool) {
const { ui } = api;
let type = selectedTool.type;
let taper = type=== 'tapermill'
let drill = type === 'drill'
const drillAngleRad = 140 * Math.PI / 180
ui.toolTaperAngle.disabled = taper ? undefined : 'true';
ui.toolTaperTip.disabled = taper ? undefined : 'true';
$('tool-view').innerHTML = '<svg id="tool-svg" width="100%" height="100%"></svg>';
setTimeout(() => {
let svg = $('tool-svg'),
pad = 10,
dim = { w: svg.clientWidth, h: svg.clientHeight },
max = { w: dim.w - pad * 2, h: dim.h - pad * 2},
off = { x: pad, y: pad },
isBall = type === "ballmill",
shaft_fill = "#cccccc",
flute_fill = "#dddddd",
stroke = "#777777",
stroke_width = 3,
stroke_thin = stroke_width / 2,
shaft = tool.shaft_len || 1,
flute = tool.flute_len || 1,
drillTip = drill ? 0.5 * tool.flute_diam * Math.sin(drillAngleRad) : 0,
total_len = shaft + flute+drillTip,
units = dim.h / total_len,
shaft_len = (shaft / total_len) * max.h,
flute_len = (flute / total_len) * max.h,
drill_tip_len = (drillTip / total_len) * max.h,
shaft_diam = tool.shaft_diam * units,
flute_diam = tool.flute_diam * units,
// total_wid = Math.max(flute_diam, shaft_diam),
shaft_off = (max.w - shaft_diam) / 2,
flute_off = (max.w - flute_diam) / 2,
taper_off = (max.w - (tool.taper_tip || 0) * units) / 2,
parts = [
// shaft rectangle
{ rect: {
x: off.x + shaft_off,
y: off.y,
width: max.w - shaft_off * 2,
height: shaft_len,
fill: shaft_fill,
stroke_width,
stroke
} }
];
if (taper) {
let yoff = off.y + shaft_len;
// let mid = dim.w / 2;
parts.push({path: {stroke_width, stroke, fill:flute_fill, d:[
`M ${off.x + flute_off} ${yoff}`,
`L ${off.x + taper_off} ${yoff + flute_len}`,
`L ${dim.w - off.x - taper_off} ${yoff + flute_len}`,
`L ${dim.w - off.x - flute_off} ${yoff}`,
`z`
].join('\n')}});
} else if(drill){
const x1 = off.x + flute_off,
y1 = off.y + shaft_len,
x2 = dim.w - off.x - flute_off,
y2 = y1 + flute_len,
xMid = dim.w / 2
let svg = $('tool-svg');
let pad = 10;
let dim = { w: svg.clientWidth, h: svg.clientHeight };
let max = { w: dim.w - pad * 2, h: dim.h - pad * 2 };
let off = { x: pad, y: pad };
parts.push({path: {stroke_width, stroke, fill:flute_fill, d:[
`M ${x1} ${y1}`, //move to top left
`L ${x1} ${y2}`, //line to bottom left
`L ${xMid} ${y2+drill_tip_len}`, //line to bottom mid point
`L ${x2} ${y2}`, //line to bottom right
`L ${x2} ${y1}`, //line to top right
`z`
].join('\n')}});
//add drill flute lines
parts.push({ line: {
x1, y1, x2, y2: (y1 + y2) / 2,
stroke, stroke_width: stroke_thin
} });
parts.push({ line: {
x1, y1: (y1 + y2) / 2, x2, y2,
stroke, stroke_width: stroke_thin
} });
} else {
let fl = isBall ? flute_len - flute_diam/2 : flute_len;
let x1 = off.x + flute_off;
let y1 = off.y + shaft_len;
let x2 = x1 + max.w - flute_off * 2;
let y2 = y1 + fl;
// flute rectangle
parts.push({ rect: {
x: off.x + flute_off,
y: off.y + shaft_len,
width: max.w - flute_off * 2,
height: fl,
fill: flute_fill,
stroke_width,
stroke,
} });
// hatch "fill" flute
parts.push({ line: { x1, y1, x2, y2, stroke, stroke_width: stroke_thin } });
parts.push({ line: {
x1: (x1 + x2) / 2, y1, x2, y2: (y1 + y2) / 2,
stroke, stroke_width: stroke_thin
} });
parts.push({ line: {
x1, y1: (y1 + y2) / 2, x2: (x1 + x2) / 2, y2,
stroke, stroke_width: stroke_thin
} });
}
if (isBall) {
let rad = (max.w - flute_off * 2) / 2;
let xend = dim.w - off.x - flute_off;
let yoff = off.y + shaft_len + flute_len + stroke_width/2 - flute_diam/2;
parts.push({path: {stroke_width, stroke, fill:flute_fill, d:[
`M ${off.x + flute_off} ${yoff}`,
`A ${rad} ${rad} 0 0 0 ${xend} ${yoff}`,
// `L ${off.x + flute_off} ${yoff}`
].join('\n')}})
// Create Tool instance and generate profile
const toolInst = new Tool(settings(), tool.id);
const resolution = (toolInst.maxDiameter() / toolInst.unitScale()) / 100;
toolInst.generateProfile(resolution);
const profile = toolInst.profile;
const { pix } = toolInst.profileDim;
// Extract cross-section at y=0 (center line)
// Profile is stored as [dx, dy, z_offset, dx, dy, z_offset, ...]
let crossSection = [];
for (let i = 0; i < profile.length; i += 3) {
let dx = profile[i];
let dy = profile[i + 1];
let z = profile[i + 2];
// Only take points where dy is approximately 0 (center line)
if (Math.abs(dy) < 0.01) {
crossSection.push({ x: dx * resolution, z });
}
}
// Section lengths
let slen = toolInst.shaftLength();
let flen = toolInst.fluteLength();
// Find bounds
let minZ = Math.min(...crossSection.map(p => p.z), -(slen + flen));
let maxZ = Math.max(...crossSection.map(p => p.z), 0);
let minX = Math.min(...crossSection.map(p => p.x));
let maxX = Math.max(...crossSection.map(p => p.x));
let zRange = maxZ - minZ;
let xRange = maxX - minX;
// Scale to fit
let scale = Math.min(max.h / zRange, max.w / xRange);
// Center horizontally if tool is narrower than viewport
let xOffset = off.x + (max.w - xRange * scale) / 2;
// Draw vertical lines for each point
let parts = [];
let stroke_width = 1;
crossSection.forEach(p => {
let x = xOffset + (p.x - minX) * scale;
let y1 = dim.h - off.y - (maxZ - p.z) * scale; // tip point
let y2 = dim.h - off.y - (flen) * scale; // top flute
let y3 = dim.h - off.y - (slen + flen) * scale; // top shaft
// flute
parts.push({ line: {
x1: x,
x2: x,
y1: y1,
y2: y2,
stroke: "#999999",
stroke_width
}});
// shaft
parts.push({ line: {
x1: x,
x2: x,
y1: y2,
y2: y3,
stroke: "#666666",
stroke_width
}});
});
svg.innerHTML = otag(parts);
}, 10);
}
export function updateTool(ev) {
const { ui } = api;
selectedTool.name = ui.toolName.value;
selectedTool.number = parseInt(ui.toolNum.value);
selectedTool.flute_diam = parseFloat(ui.toolFluteDiam.value);
selectedTool.flute_len = parseFloat(ui.toolFluteLen.value);
selectedTool.shaft_diam = parseFloat(ui.toolShaftDiam.value);
selectedTool.shaft_len = parseFloat(ui.toolShaftLen.value);
selectedTool.taper_tip = parseFloat(ui.toolTaperTip.value);
selectedTool.metric = ui.toolMetric.checked;
selectedTool.type = toolNames[ui.toolType.selectedIndex];
if (selectedTool.type === 'tapermill') {
const rad = (selectedTool.flute_diam - selectedTool.taper_tip) / 2;
if (ev && ev.target === ui.toolTaperAngle) {
const angle = parseFloat(ev.target.value);
const len = calcTaperLength(rad, angle * DEG2RAD);
selectedTool.flute_len = len;
ui.toolTaperAngle.value = angle.round(1);
ui.toolFluteLen.value = selectedTool.flute_len.round(4);
} else {
ui.toolTaperAngle.value = calcTaperAngle(rad, selectedTool.flute_len).round(1);
}
} else {
ui.toolTaperAngle.value = 0;
}
renderTools();
ui.toolSelect.selectedIndex = selectedTool.order;
setToolChanged(true);
renderTool(selectedTool);
}
function setToolChanged(changed) {
editTools.changed = changed;
api.ui.toolsSave.disabled = !changed;