diff --git a/src/geo/polygon.js b/src/geo/polygon.js index d00fbbbe..5d5fa478 100644 --- a/src/geo/polygon.js +++ b/src/geo/polygon.js @@ -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) { diff --git a/src/kiri-mode/cam/client.js b/src/kiri-mode/cam/client.js index b47c6897..db3b83b8 100644 --- a/src/kiri-mode/cam/client.js +++ b/src/kiri-mode/cam/client.js @@ -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; diff --git a/src/kiri-mode/cam/ops.js b/src/kiri-mode/cam/ops.js index 555e469f..b025431d 100644 --- a/src/kiri-mode/cam/ops.js +++ b/src/kiri-mode/cam/ops.js @@ -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); } } } diff --git a/src/kiri-mode/cam/slice.js b/src/kiri-mode/cam/slice.js index f67adaf7..ef4795f2 100644 --- a/src/kiri-mode/cam/slice.js +++ b/src/kiri-mode/cam/slice.js @@ -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 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 (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; }; diff --git a/src/kiri/widget.js b/src/kiri/widget.js index 61eb8ddf..8cb812bb 100644 --- a/src/kiri/widget.js +++ b/src/kiri/widget.js @@ -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 */