Merge branch 'dev-4.2-refactor-trace' into rel-4.2

This commit is contained in:
Stewart Allen 2025-06-22 15:18:04 -04:00
commit 0d2261566d
5 changed files with 195 additions and 92 deletions

View file

@ -1002,6 +1002,23 @@ class Polygon {
return this.z !== undefined ? this.z : this.points[i || 0]?.z || 0;
}
minZ() {
return Math.min(...this.points.map(p => p.z));
}
avgZ() {
return [...this.points.map(p => p.z)].reduce((a,v) => a+v) / this.points.length;
}
/**
* @param {*} z value target
* @param {*} epsilon allowed Z variance
* @returns {boolean} true if all points Z within epsilon of value
*/
onZ(z, epsilon = 10e-4) {
return Math.max(...this.points.map(p => Math.abs(z - p.z))) < epsilon;
}
/**
*/
render(layer, color, recursive, open) {

View file

@ -1008,7 +1008,6 @@ CAM.init = function(kiri, api) {
CAM.traces((ids) => {
api.hide.alert(alert);
alert = api.show.alert("[esc] cancels trace editing");
kiri.api.widgets.opacity(0.8);
kiri.api.widgets.for(widget => {
if (ids.indexOf(widget.id) >= 0) {
unselectTraces(widget, true);
@ -1042,6 +1041,7 @@ CAM.init = function(kiri, api) {
});
// ensure appropriate traces are toggled matching current record
kiri.api.widgets.for(widget => {
widget.setVisualState({ opacity: 0.25 });
let areas = (poppedRec.areas[widget.id] || []);
let stack = widget.trace_stack;
stack.meshes.forEach(mesh => {
@ -1070,6 +1070,7 @@ CAM.init = function(kiri, api) {
api.feature.hover = false;
api.feature.hoverAdds = false;
kiri.api.widgets.for(widget => {
widget.restoreVisualState();
if (widget.trace_stack) {
widget.trace_stack.hide();
widget.adds.removeAll(widget.trace_stack.meshes);
@ -1096,17 +1097,15 @@ CAM.init = function(kiri, api) {
color.r = colorSave.r;
color.g = colorSave.g;
color.b = colorSave.b;
lastTrace.position.z -= 0.01;
}
lastTrace = null;
if (data.type === 'platform') {
lastTrace = null;
return;
}
if (!data.int.object.trace) {
return;
}
lastTrace = data.int.object;
lastTrace.position.z += 0.01;
if (lastTrace.selected) {
let event = data.event;
let target = event.target;
@ -1115,11 +1114,8 @@ CAM.init = function(kiri, api) {
}
let material = lastTrace.material[0] || lastTrace.material;
let color = material.color;
let {r, g, b} = color;
material.colorSave = {r, g, b};
color.r = 0;
color.g = 0;
color.b = 1;
material.colorSave = color.clone();
color.setHex(isDark() ? 0x0066ff : 0x0000ff);
};
func.traceHoverUp = function(int, ev) {
if (!int) return;
@ -1128,8 +1124,9 @@ CAM.init = function(kiri, api) {
if (ev.metaKey || ev.ctrlKey) {
let { selected } = object;
let { widget, poly } = object.trace;
let avgZ = poly.avgZ();
for (let add of widget.adds) {
if (add.trace && add.selected !== selected && add.trace.poly.getZ() === poly.getZ()) {
if (add.trace && add.selected !== selected && add.trace.poly.onZ(avgZ)) {
func.traceToggle(add);
}
}
@ -1147,23 +1144,17 @@ CAM.init = function(kiri, api) {
let wlist = areas[widget.id] = areas[widget.id] || [];
obj.selected = !obj.selected;
if (!colorSave) {
colorSave = material.colorSave = {
r: color.r,
g: color.g,
b: color.b
};
colorSave = material.colorSave = color.clone();
}
if (obj.selected) {
obj.position.z += 0.01;
color.r = colorSave.r = 0.9;
color.g = colorSave.g = 0;
color.b = colorSave.b = 0.1;
color.setHex(isDark() ? 0xdd0011 : 0xff0033);
colorSave.r = color.r;
colorSave.g = color.g;
colorSave.b = color.b;
if (!skip) wlist.push(poly._trace);
} else {
obj.position.z -= 0.01;
color.r = colorSave.r = 0xaa/255;
color.g = colorSave.g = 0xaa/255;
color.b = colorSave.b = 0x55/255;
color.setHex(0xaaaa55);
colorSave.setHex(0xaaaa55);
if (!skip) wlist.remove(poly._trace);
}
API.conf.save();
@ -1192,9 +1183,6 @@ CAM.init = function(kiri, api) {
func.hover = func.selectHolesHover;
func.hoverUp = func.selectHolesHoverUp;
const widgets = kiri.api.widgets.all()
/**
* creates a mesh for a hole and adds it to a widget
@ -1712,13 +1700,11 @@ CAM.init = function(kiri, api) {
dogbone: 'camTraceDogbone',
revbone: 'camTraceDogbone',
merge: 'camTraceMerge',
select: 'camTraceMode',
ov_topz: 0,
ov_botz: 0,
ov_conv: '~camConventional',
}).inputs = {
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
select: UC.newSelect(LANG.cc_sele_s, {title:LANG.cc_sele_l}, "select"),
mode: UC.newSelect(LANG.cu_type_s, {title:LANG.cu_type_l}, "trace"),
offset: UC.newSelect(LANG.cc_offs_s, {title: LANG.cc_offs_l, show:() => (poppedRec.mode === 'follow')}, "traceoff"),
sep: UC.newBlank({class:"pop-sep"}),
@ -2162,16 +2148,18 @@ function validateTools(tools) {
}
}
function isDark() { return API.space.is_dark() };
function addbox() { return FDM.addbox(...arguments)};
function delbox() { return FDM.delbox(...arguments)};
function boxColor() {
return API.space.is_dark() ? 0x00ddff : 0x0000dd;
return isDark() ? 0x00ddff : 0x0000dd;
}
function boxOpacity() {
return API.space.is_dark() ? 0.75 : 0.6;
return isDark() ? 0.75 : 0.6;
}
function animate() {
@ -2254,7 +2242,7 @@ function updateStock() {
camStock.position.z = center.z;
camStock.rotation.x = currentIndex || 0;
camStock.lines.material.color =
new THREE.Color(API.space.is_dark() ? 0x555555 : 0xaaaaaa);
new THREE.Color(isDark() ? 0x555555 : 0xaaaaaa);
} else if (camStock) {
SPACE.world.remove(camStock);
camStock = null;

View file

@ -976,11 +976,12 @@ class OpTrace extends CamOp {
}
async slice(progress) {
const debug = false;
let { op, state } = this;
let { tool, rate, down, plunge, offset, offover, thru } = op;
let { ov_conv } = op;
let { settings, widget, addSlices, zThru, tabs, workarea } = state;
let { updateToolDiams, cutTabs, cutPolys, healPolys, color } = state;
let { updateToolDiams, cutTabs, cutPolys, healPolys, color, shadowAt } = state;
let { process, stock } = settings;
let { camStockClipTo } = process;
if (state.isIndexed) {
@ -1042,17 +1043,39 @@ class OpTrace extends CamOp {
.setLayer("trace follow", {line: color}, false)
.addPolys(slice.camLines)
}
function clearZ(polys, z, down) {
function clearZnew(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 ];
let nested = POLY.nest(polys);
for (let poly of nested) {
let zpro = 0, zinc = 1 / (polys.length * zs.length);
for (let poly of polys) {
// newPocket();
for (let z of zs) {
let clip = [], shadow;
shadow = shadowAt(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 = 999;
slice.camTrace = { tool, rate, plunge };
POLY.offset([ poly ], [ -toolDiam/2, -toolOver ], {
count:999, outs: slice.camLines = [], flat:true, z,
minArea: 0
});
if (toolDiam) {
const offs = [ -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 {
@ -1060,8 +1083,14 @@ class OpTrace extends CamOp {
}
POLY.setWinding(slice.camLines, cutdir, false);
slice.output()
.setLayer("trace clear", {line: color}, false)
.setLayer("trace", {line: color}, false)
.addPolys(slice.camLines)
if (debug && shadow) slice.output()
.setLayer("trace shadow", {line: 0xff8811}, false)
.addPolys(shadow)
progress(zpro, "trace");
zpro += zinc;
addSlices(slice);
}
}
}
@ -1176,7 +1205,7 @@ class OpTrace extends CamOp {
const zbo = widget.track.top - widget.track.box.d;
let zmap = {};
for (let poly of polys) {
let z = minZ(poly.getZ());
let z = minZ(poly.minZ());
if (offover) {
let pnew = POLY.offset([poly], -offover, { minArea: 0, open: true });
if (pnew) {
@ -1190,7 +1219,7 @@ class OpTrace extends CamOp {
(zmap[z] = zmap[z] || []).appendAll(poly);
}
for (let [zv, polys] of Object.entries(zmap)) {
clearZ(polys, parseFloat(zv), down);
clearZnew(polys, parseFloat(zv), down);
}
}
}

View file

@ -2,6 +2,7 @@
"use strict";
// dep: geo.line
// dep: geo.point
// dep: geo.polygons
// dep: kiri.slice
@ -14,7 +15,7 @@ gapp.register("kiri-mode.cam.slice", (root, exports) => {
const { base, kiri } = root;
const { util } = base;
const { driver, newSlice, setSliceTracker } = kiri;
const { polygons, newPoint, newPolygon } = base;
const { polygons, newLine, newPoint, newPolygon } = base;
const { CAM } = driver;
const { OPS } = CAM;
@ -379,60 +380,107 @@ CAM.addDogbones = function(poly, dist, reverse) {
};
CAM.traces = async function(settings, widget, single) {
if (widget.traces && widget.trace_single === single) {
// do no work if cached
if (widget.traces) {
return false;
}
let slicer = new kiri.cam_slicer(widget);
let zees = Object.assign({}, slicer.zFlat, slicer.zLine);
let indices = [...new Set(Object.keys(zees)
.map(kv => parseFloat(kv).round(5))
.appendAll(Object.entries(single ? slicer.zLine : {}).map(ze => {
let [ zk, zv ] = ze;
return zv > 1 ? parseFloat(zk).round(5) : null;
})
.filter(v => v !== null))
)]
.sort((a,b) => b - a);
let traces = [];
// find and trim polys (including open) to shadow
let oneach = data => {
if (single) {
for (let line of data.lines) {
if (line.p1.distTo2D(line.p2) > 1) {
traces.push(newPolygon().append(line.p1).append(line.p2).setOpen());
}
}
} else
base.polygons.flatten(data.tops).forEach(poly => {
poly.inner = null;
poly.parent = null;
let z = poly.getZ();
for (let i=0, il=traces.length; i<il; i++) {
let trace = traces[i];
let dz = Math.abs(z - trace.getZ());
// only compare polys farther apart in Z
if (dz > 0.01) {
continue;
}
// do not add duplicates
if (traces[i].isEquivalent(poly) && dz < 0.1) {
// console.log({ dup: poly });
return;
}
}
traces.push(poly);
});
};
// --- points → line segments ---
let edges = new THREE.EdgesGeometry(widget.mesh.geometry, 20);
let array = edges.attributes.position.array;
let pcache = {};
let points = new Array(2);
let lines = new Array(points.length / 2);
for (let i=0, j=0, k=0, l=array.length; i<l; ) {
let ps = [ array[i++], array[i++], array[i++] ];
let key = ps.map(v => (v * 10000) | 0).join(',');
let point = pcache[key];
if (!point) {
point = newPoint(ps[0], ps[1], ps[2], key);
point.lines = [];
pcache[key] = point;
}
points[j++ % 2] = point;
if (j % 2 === 0) {
let [ p0, p1 ] = points;
let line = lines[k++] = newLine(p0, p1);
p0.lines.push(line);
p1.lines.push(line);
}
}
// --- segments → chains (bidirectional walk) ---
let chains = [];
{
const segs = lines.slice(); // shallow copy
segs.forEach(s => (s.visited = false));
function step(dirPoint, prevSeg, pushFront, chain) {
let curr = dirPoint;
let prev = prevSeg;
while (curr.lines.length === 2) { // stay inside the arc
const next = curr.lines.find(l => !l.visited && l !== prev);
if (!next) break; // should not happen
next.visited = true;
if (pushFront) chain.unshift(next); // prepend or append?
else chain.push(next);
curr = next.p1 === curr ? next.p2 : next.p1;
prev = next;
}
}
for (const seed of segs) {
if (seed.visited) continue;
seed.visited = true; // always include the seed
const chain = [seed];
// grow backwards from p1 (prepend)
step(seed.p1, seed, true, chain);
// grow forwards from p2 (append)
step(seed.p2, seed, false, chain);
chains.push(chain);
}
}
// --- chains → polylines ---
const polylines = [];
for (const chain of chains) {
const poly = newPolygon().setOpen();
// choose any node whose degree !== 2; if none, it’s a closed loop
let start = null;
for (const s of chain) {
if (s.p1.lines.length !== 2) { start = s.p1; break; }
if (s.p2.lines.length !== 2) { start = s.p2; break; }
}
if (!start) start = chain[0].p1; // closed loop
let curr = start, prevSeg = null;
poly.push(curr); // first point
while (true) {
const nextSeg = curr.lines.find(
l => l !== prevSeg && chain.includes(l)
);
if (!nextSeg) break; // open end reached
curr = nextSeg.p1 === curr ? nextSeg.p2 : nextSeg.p1;
poly.push(curr); // << push *before* test
if (curr === start) break; // loop closed
prevSeg = nextSeg;
}
polylines.push(poly);
}
widget.traces = polylines;
let opts = { each: oneach, over: false, flatoff: 0, edges: true, openok: true, lines: true };
await slicer.slice(indices, opts);
// pick up bottom features
opts.over = true;
await slicer.slice(indices, opts);
widget.traces = traces;
widget.trace_single = single;
return true;
};

View file

@ -355,6 +355,27 @@ class Widget {
}
}
getVisualState() {
return {
edges: this.outline ? true : false,
wires: this.wire ? true : false,
opacity: this.getMaterial().opacity
};
}
setVisualState({ edges, wires, opacity}) {
this.cache.vizstate = this.getVisualState();
this.setEdges(edges ?? false);
this.setWireframe(wires ?? false);
this.setOpacity(opacity ?? 1);
}
restoreVisualState() {
if (this.cache.vizstate) {
this.setVisualState(this.cache.vizstate);
}
}
/**
* @param {number} value
*/