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:
parent
761568b533
commit
e0b1c4e31f
44 changed files with 1607 additions and 2894 deletions
|
|
@ -204,7 +204,6 @@ const renamed = {
|
|||
roughingOn: "camRoughOn",
|
||||
roughingOver: "camRoughOver",
|
||||
roughingPlunge: "camRoughPlunge",
|
||||
roughingPocket: "camRoughVoid",
|
||||
roughingSpeed: "camRoughSpeed",
|
||||
roughingSpindle: "camRoughSpindle",
|
||||
roughingStock: "camRoughStock",
|
||||
|
|
@ -428,14 +427,18 @@ export const conf = {
|
|||
camAreaMode: "clear",
|
||||
camAreaTrace: "none",
|
||||
camAreaSurface: "linear",
|
||||
camAreaDirection: "climb",
|
||||
camAreaAngle: 0,
|
||||
camAreaOver: 0.4,
|
||||
camAreaDown: 1,
|
||||
camAreaSpeed: 1000,
|
||||
camAreaPlunge: 100,
|
||||
camAreaFollow: 15,
|
||||
camAreaExpand: 0,
|
||||
camAreaSmooth: 1,
|
||||
camAreaRefine: 0,
|
||||
camAreaSmooth: 1,
|
||||
camAreaDogbones: false,
|
||||
camAreaRevbones: false,
|
||||
camAreaOutline: false,
|
||||
camContourAngle: 85,
|
||||
camContourBottom: false,
|
||||
|
|
@ -528,23 +531,19 @@ export const conf = {
|
|||
camOriginOffZ: 0,
|
||||
camOriginTop: true,
|
||||
camOutlineDogbone: false,
|
||||
camOutlineRevbone: false,
|
||||
camOutlineDown: 3,
|
||||
camOutlineIn: false,
|
||||
camOutlineOmitThru: false,
|
||||
camOutlineOmitVoid: false,
|
||||
camOutlineOn: true,
|
||||
camOutlineOut: true,
|
||||
camOutlineOver: 0.4,
|
||||
camOutlineOverCount: 1,
|
||||
camOutlinePlunge: 250,
|
||||
camOutlineSpeed: 800,
|
||||
camOutlineSpindle: 1000,
|
||||
camOutlineTool: 1000,
|
||||
camOutlineTop: true,
|
||||
camOutlineWide: false,
|
||||
camPocketContour: false,
|
||||
camPocketDown: 1,
|
||||
camPocketEngrave: false,
|
||||
camPocketExpand: 0,
|
||||
camPocketFollow: 5,
|
||||
camPocketOutline: false,
|
||||
|
|
@ -562,7 +561,6 @@ export const conf = {
|
|||
camRegisterThru: 5,
|
||||
camRoughAll: true,
|
||||
camRoughDown: 2,
|
||||
camRoughFlat: true,
|
||||
camRoughIn: true,
|
||||
camRoughOmitThru: false,
|
||||
camRoughOmitVoid: false,
|
||||
|
|
@ -575,7 +573,6 @@ export const conf = {
|
|||
camRoughStockZ: 0,
|
||||
camRoughTool: 1000,
|
||||
camRoughTop: true,
|
||||
camRoughVoid: false,
|
||||
camStockClipTo: false,
|
||||
camStockIndexed: false,
|
||||
camStockIndexGrid: true,
|
||||
|
|
@ -603,7 +600,6 @@ export const conf = {
|
|||
camTraceType: "follow",
|
||||
camTraceZBottom: 0,
|
||||
camTraceZTop: 0,
|
||||
camTrueShadow: false,
|
||||
camZAnchor: "middle",
|
||||
camZBottom: 0,
|
||||
camZClearance: 1,
|
||||
|
|
|
|||
|
|
@ -130,6 +130,18 @@ const LISTS = {
|
|||
surftyp: [
|
||||
{ name: "linear" },
|
||||
{ name: "offset" },
|
||||
],
|
||||
direction: [
|
||||
{ name: "climb" },
|
||||
{ name: "conventional" },
|
||||
{ name: "alternating" },
|
||||
],
|
||||
camtool: [
|
||||
{ name: "flat end", id: "endmill" },
|
||||
{ name: "ball end", id: "ballmill" },
|
||||
{ name: "taper tip", id: "tapermill" },
|
||||
{ name: "taper ball", id: "taperball" },
|
||||
{ name: "drill bit", id: "drill" },
|
||||
]
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -918,25 +918,6 @@ function init_one() {
|
|||
deviceExport: $('device-exp'),
|
||||
deviceSave: $('device-save'),
|
||||
|
||||
toolsSave: $('tools-save'),
|
||||
toolsClose: $('tools-close'),
|
||||
toolsImport: $('tools-import'),
|
||||
toolsExport: $('tools-export'),
|
||||
toolSelect: $('tool-select'),
|
||||
toolAdd: $('tool-add'),
|
||||
toolCopy: $('tool-dup'),
|
||||
toolDelete: $('tool-del'),
|
||||
toolType: $('tool-type'),
|
||||
toolName: $('tool-name'),
|
||||
toolNum: $('tool-num'),
|
||||
toolFluteDiam: $('tool-fdiam'),
|
||||
toolFluteLen: $('tool-flen'),
|
||||
toolShaftDiam: $('tool-sdiam'),
|
||||
toolShaftLen: $('tool-slen'),
|
||||
toolTaperAngle: $('tool-tangle'),
|
||||
toolTaperTip: $('tool-ttip'),
|
||||
toolMetric: $('tool-metric'),
|
||||
|
||||
setMenu: $('set-menu'),
|
||||
settings: $('settings'),
|
||||
settingsBody: $('settingsBody'),
|
||||
|
|
|
|||
|
|
@ -473,7 +473,7 @@ class Print {
|
|||
};
|
||||
}
|
||||
|
||||
function outputPoint(point,lastP,emit,{center,arcPoints,retract}) {
|
||||
function outputPoint(point,lastP,emit,{retract}) {
|
||||
// non-move in a new plane means burp out
|
||||
// the old sequence and start a new one
|
||||
if (newlayer || (autolayer && seq.z != point.z)) {
|
||||
|
|
@ -508,7 +508,7 @@ class Print {
|
|||
}
|
||||
}
|
||||
// add point to current sequence
|
||||
scope.addOutput(seq, point, emit, pos.F, tool,{retract,arcPoints});
|
||||
scope.addOutput(seq, point, emit, pos.F, tool, {retract});
|
||||
scope.lastPos = Object.assign({}, pos);
|
||||
scope.lastPosE = pos.E;
|
||||
}
|
||||
|
|
@ -520,19 +520,14 @@ class Print {
|
|||
* @param {number} index - The line number of the g-code file that contains the G2 or G3 command.
|
||||
*/
|
||||
function G2G3(g2, line, index) {
|
||||
const axes = {};
|
||||
const {point, prevPoint, center} = processLine(line,axes);
|
||||
|
||||
// console.log(structuredClone({point,prevPoint,center}));
|
||||
|
||||
let arcPoints = arcToPath( prevPoint, point, 64,{ clockwise:g2,center}) ?? []
|
||||
let emit = g2 ? 2 : 3;
|
||||
|
||||
// console.log("clone point",structuredClone({point,prevPoint,center,arcPoints,emit}));
|
||||
// console.log("pointer point",{point,prevPoint,center,arcPoints,emit});
|
||||
|
||||
outputPoint(point,prevPoint,emit,{center,arcPoints});
|
||||
// scope.addOutput(seq, point, emit, pos.F, tool,{center,arcPoints});
|
||||
let axes = {};
|
||||
let { point, prevPoint, center } = processLine(line, axes);
|
||||
let arcPoints = arcToPath(prevPoint, point, 64, { clockwise: g2, center }) ?? [];
|
||||
for (let point of arcPoints) {
|
||||
outputPoint(point, prevPoint, 1, {});
|
||||
prevPoint = point;
|
||||
}
|
||||
outputPoint(point, prevPoint, 1, {});
|
||||
}
|
||||
|
||||
function G0G1(g0, line) {
|
||||
|
|
|
|||
|
|
@ -47,11 +47,12 @@ async function path(levels, update, opts = {}) {
|
|||
return [];
|
||||
}
|
||||
|
||||
const isCAM = is_cam();
|
||||
const dark = is_dark();
|
||||
const tools = opts.tools || {};
|
||||
const flat = opts.flat;
|
||||
const thin = opts.thin && !flat;
|
||||
const ckspeed = opts.speed !== false;
|
||||
const ckspeed = isCAM || opts.speed !== false;
|
||||
const headColor = 0x888888;
|
||||
const moveColor = opts.move >= 0 ? opts.move : (dark ? 0x666666 : 0xaaaaaa);
|
||||
const printColor = opts.print >= 0 ? opts.print : 0x777700;
|
||||
|
|
@ -186,27 +187,7 @@ async function path(levels, update, opts = {}) {
|
|||
if (arrowAll || lastOut.emit !== out.emit) {
|
||||
heads.push({p1: lastOutPoint, p2: outPoint});
|
||||
}
|
||||
const op = outPoint, lp = lastOutPoint;
|
||||
// const moved = Math.max(
|
||||
// Math.abs(op.x - lp.x),
|
||||
// Math.abs(op.y - lp.y),
|
||||
// Math.abs(op.z - lp.z));
|
||||
// if (moved < 0.0001) return;
|
||||
if (is_cam() && (out.emit == 2 || out.emit == 3 )) { // cam arc emit
|
||||
// checks if a new poly should be started
|
||||
if (!lastOut.emit || (ckspeed && out.speed !== lastOut.speed) || lastEnd) {
|
||||
current = newPolygon().setOpen();
|
||||
current.push(lastOutPoint);
|
||||
current.color = color(out);
|
||||
pushPrint(out.tool, current);
|
||||
}
|
||||
out.arcPoints.forEach(p => {
|
||||
current.push(p);
|
||||
})
|
||||
current.push(outPoint);
|
||||
} else if (out.emit) {
|
||||
// explicity G1 in CAM mode
|
||||
// just a non-move in other modes
|
||||
if (out.emit) {
|
||||
// checks if a new poly should be started
|
||||
if (!lastOut.emit || (ckspeed && out.speed !== lastOut.speed) || lastEnd) {
|
||||
current = newPolygon().setOpen();
|
||||
|
|
|
|||
|
|
@ -434,6 +434,7 @@ function newDiv(opt = {}) {
|
|||
(opt.addto || addTo).appendChild(div);
|
||||
if (opt.addto) lastDiv = addTo = div;
|
||||
if (opt.class) div.setAttribute('class', opt.class);
|
||||
if (opt.content) div.innerHTML = opt.content;
|
||||
if (opt.group !== false) {
|
||||
lastGroup?.push(div);
|
||||
div._group = groupName;
|
||||
|
|
@ -444,9 +445,10 @@ function newDiv(opt = {}) {
|
|||
return div;
|
||||
}
|
||||
|
||||
function newExpand(label, opt = {}, opteach = {}) {
|
||||
function newExpand(label, opt = {}) {
|
||||
let div = DOC.createElement('details');
|
||||
div.setAttribute('class', opt.class || 'f-col');
|
||||
if (opt.open) div.setAttribute('open', true);
|
||||
addModeControls(div, opt);
|
||||
|
||||
let summary = DOC.createElement('summary');
|
||||
|
|
@ -606,7 +608,7 @@ function newText(label, options) {
|
|||
function newInput(label, opt = {}) {
|
||||
let row = newDiv(opt),
|
||||
hide = opt.hide,
|
||||
size = opt.size || 5,
|
||||
size = opt.size ?? 5,
|
||||
height = opt.height || 0,
|
||||
ip = height > 1 ? DOC.createElement('textarea') : DOC.createElement('input'),
|
||||
action = opt.action || bindTo || inputAction;
|
||||
|
|
@ -630,6 +632,7 @@ function newInput(label, opt = {}) {
|
|||
row.style.display = hide ? 'none' : '';
|
||||
if (opt.disabled) ip.setAttribute("disabled", "true");
|
||||
if (opt.title) row.setAttribute("title", opt.title);
|
||||
if (opt.id) ip.setAttribute("id", opt.id);
|
||||
if (opt.convert) ip.convert = opt.convert.bind(ip);
|
||||
if (opt.bound) ip.bound = opt.bound;
|
||||
if (opt.action) action = opt.action;
|
||||
|
|
@ -737,6 +740,7 @@ function newSelect(label, options = {}, source) {
|
|||
}
|
||||
row.setAttribute("class", "var-row");
|
||||
row.style.display = hide ? 'none' : '';
|
||||
if (options.id) ip.setAttribute("id", options.id);
|
||||
if (options.convert) ip.convert = options.convert.bind(ip);
|
||||
if (options.disabled) ip.setAttribute("disabled", "true");
|
||||
if (options.title) row.setAttribute("title", options.title);
|
||||
|
|
|
|||
|
|
@ -2,8 +2,12 @@
|
|||
|
||||
import { base } from '../../geo/base.js';
|
||||
import { avgc } from './utils.js';
|
||||
import { verticesToPoints } from '../../geo/points.js';
|
||||
import { util as mesh_util } from '../../mesh/util.js';
|
||||
import { verticesToPoints } from '../../geo/points.js';
|
||||
import { newPoint } from '../../geo/point.js';
|
||||
import { newPolygon } from '../../geo/polygon.js';
|
||||
import { polygons as POLY } from '../../geo/polygons.js';
|
||||
import { checkOverUnderOn, intersectPoints } from '../../geo/slicer.js';
|
||||
|
||||
const { inRange, time } = base.util;
|
||||
const solid_opacity = 1.0;
|
||||
|
|
@ -707,7 +711,7 @@ class Widget {
|
|||
}
|
||||
// used by CAM.shadowAt()
|
||||
this.cache.geo = pos;
|
||||
this.cache.shadow = undefined;
|
||||
delete this.cache.shadow;
|
||||
return pos;
|
||||
}
|
||||
|
||||
|
|
@ -835,6 +839,115 @@ class Widget {
|
|||
hide() {
|
||||
this.mesh.visible = false;
|
||||
}
|
||||
|
||||
async shadowAt(z) {
|
||||
let shadows = this.cache.shadows;
|
||||
if (!shadows) {
|
||||
shadows = this.cache.shadows = {};
|
||||
}
|
||||
let cached = shadows[z];
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
// find closest shadow above and use to speed up delta shadow gen
|
||||
let zover = Object.keys(shadows).map(v => parseFloat(v)).filter(v => v > z);
|
||||
let minZabove = Math.min(Infinity, ...zover);
|
||||
let shadow = this.#computeShadowAt(z, minZabove);
|
||||
if (minZabove < Infinity) {
|
||||
shadow = POLY.union([...shadow, ...shadows[minZabove]], 0.001, true);
|
||||
}
|
||||
return shadows[z] = POLY.setZ(shadow, z);
|
||||
}
|
||||
|
||||
#ensureShadowCache() {
|
||||
// cache faces with normals up
|
||||
if (this.cache.shadow) {
|
||||
return;
|
||||
}
|
||||
const geo = this.cache.geo;
|
||||
const length = geo.length;
|
||||
const bounds = this.getBoundingBox();
|
||||
const stack = {};
|
||||
// const faces = [];
|
||||
for (let i = Math.floor(bounds.min.z); i <= Math.ceil(bounds.max.z); i++) {
|
||||
stack[i] = [];
|
||||
}
|
||||
for (let i = 0, ip = 0; i < length; i += 3) {
|
||||
const a = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]);
|
||||
const b = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]);
|
||||
const c = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]);
|
||||
const n = THREE.computeFaceNormal(a, b, c);
|
||||
if (n.z < 0.001) {
|
||||
continue;
|
||||
// faces.push(a, b, c);
|
||||
}
|
||||
const minZ = Math.floor(Math.min(a.z, b.z, c.z));
|
||||
const maxZ = Math.ceil(Math.max(a.z, b.z, c.z));
|
||||
for (let z = minZ; z <= maxZ; z++) {
|
||||
stack[z].push(a, b, c);
|
||||
}
|
||||
}
|
||||
this.cache.shadow = stack;
|
||||
}
|
||||
|
||||
// union triangles > z (opt cap < ztop) into polygon(s)
|
||||
#computeShadowAt(z, ztop) {
|
||||
this.#ensureShadowCache();
|
||||
const found = [];
|
||||
const stack = this.cache.shadow;
|
||||
let minZ = Math.floor(z);
|
||||
let maxZ = Math.ceil(z);
|
||||
let slices = [];
|
||||
for (let sz = minZ; sz <= maxZ; sz++) {
|
||||
slices.push(stack[sz] ?? []);
|
||||
}
|
||||
for (let faces of slices)
|
||||
for (let i = 0; i < faces.length; ) {
|
||||
const a = faces[i++];
|
||||
const b = faces[i++];
|
||||
const c = faces[i++];
|
||||
if (ztop && a.z > ztop && b.z > ztop && c.z > ztop) {
|
||||
// skip faces over top threshold
|
||||
continue;
|
||||
}
|
||||
if (a.z < z && b.z < z && c.z < z) {
|
||||
// skip faces under threshold
|
||||
continue;
|
||||
} else if (a.z >= z && b.z >= z && c.z >= z) {
|
||||
found.push([a, b, c]);
|
||||
} else {
|
||||
// check faces straddling threshold
|
||||
const where = { under: [], over: [], on: [] };
|
||||
checkOverUnderOn(newPoint(a.x, a.y, a.z), z, where);
|
||||
checkOverUnderOn(newPoint(b.x, b.y, b.z), z, where);
|
||||
checkOverUnderOn(newPoint(c.x, c.y, c.z), z, where);
|
||||
if (where.on.length === 0 && (where.over.length === 2 || where.under.length === 2)) {
|
||||
// compute two point intersections and construct line
|
||||
let line = intersectPoints(where.over, where.under, z);
|
||||
if (line.length === 2) {
|
||||
if (where.over.length === 2) {
|
||||
found.push([where.over[1], line[0], line[1]]);
|
||||
found.push([where.over[0], where.over[1], line[0]]);
|
||||
} else {
|
||||
found.push([where.over[0], line[0], line[1]]);
|
||||
}
|
||||
} else {
|
||||
console.log({ msg: "invalid ips", line: line, where: where });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let polys = found.map(a => {
|
||||
return newPolygon()
|
||||
.add(a[0].x, a[0].y, a[0].z)
|
||||
.add(a[1].x, a[1].y, a[1].z)
|
||||
.add(a[2].x, a[2].y, a[2].z);
|
||||
});
|
||||
polys = POLY.union(polys, 0, true);
|
||||
|
||||
return polys;
|
||||
}
|
||||
}
|
||||
|
||||
// Widget Grouping API
|
||||
|
|
|
|||
Loading…
Reference in a new issue