diff --git a/src/kiri-mode/cam/animate.js b/src/kiri-mode/cam/animate.js index b376da81..438df834 100644 --- a/src/kiri-mode/cam/animate.js +++ b/src/kiri-mode/cam/animate.js @@ -2,49 +2,53 @@ "use strict"; -self.kiri.load(function() { +// dep: kiri.api +// dep: kiri-mode.cam.driver +gapp.register("kiri-mode.cam.animate", [], (root, exports) => { - let KIRI = self.kiri, - CAM = KIRI.driver.CAM, - API, WORLD, SPACE, - meshes = {}, - unitScale = 1, - progress, toolPosX, toolPosY, toolPosZ, - speedValues = [ 25, 12, 6, 3 ], - speedNames = [ "1x", "2x", "4x", "8x" ], - speedMax = speedValues.length - 1, - speedIndex = 0, - speedLabel, - speed, - color = 0, - pauseButton, - playButton, - posOffset = { x:0, y:0, z:0 }; +const { kiri } = root; +const { api, driver } = kiri; +const { CAM } = driver; - // ---( CLIENT FUNCTIONS )--- +let meshes = {}, + unitScale = 1, + progress, toolPosX, toolPosY, toolPosZ, + speedValues = [ 25, 12, 6, 3 ], + speedNames = [ "1x", "2x", "4x", "8x" ], + speedMax = speedValues.length - 1, + speedIndex = 0, + speedLabel, + speed, + color = 0, + pauseButton, + playButton, + posOffset = { x:0, y:0, z:0 }; - if (KIRI.client) - CAM.animate_clear = function(api) { - API = api; - KIRI.client.animate_cleanup(); - SPACE = KIRI.space; - WORLD = SPACE.world; +// ---( CLIENT FUNCTIONS )--- + +kiri.load(() => { + if (!kiri.client) { + return; + } + + const { moto } = root; + const { space } = moto; + const { world } = space; + + function animate_clear(api) { + kiri.client.animate_cleanup(); $('layer-animate').innerHTML = ''; $('layer-toolpos').innerHTML = ''; Object.keys(meshes).forEach(id => deleteMesh(id)); - } + } - if (KIRI.client) - CAM.animate = function(api, delay) { - API = api; - SPACE = KIRI.space; - WORLD = SPACE.world; - KIRI.client.animate_setup(API.conf.get(), data => { + function animate(api, delay) { + kiri.client.animate_setup(api.conf.get(), data => { checkMeshCommands(data); if (!(data && data.mesh_add)) { return; } - const UC = API.uc; + const UC = api.uc; const layer = $('layer-animate'); layer.innerHTML = ''; UC.setGroup(layer); @@ -70,9 +74,109 @@ self.kiri.load(function() { toolPosZ = UC.newInput('z', {disabled: true, size: 7}); playButton.style.display = ''; pauseButton.style.display = 'none'; - API.event.emit('animate', 'CAM'); + api.event.emit('animate', 'CAM'); }); - }; + } + + gapp.overlay(kiri.client, { + animate(data, ondone) { + kiri.client.send("animate", data, ondone); + }, + + animate_setup(settings, ondone) { + color = settings.controller.dark ? 0x888888 : 0; + unitScale = settings.controller.units === 'in' ? 1/25.4 : 1; + kiri.client.send("animate_setup", {settings}, ondone); + }, + + animate_cleanup(data, ondone) { + kiri.client.send("animate_cleanup", data, ondone); + } + }); + + gapp.overlay(CAM, { + animate, + animate_clear + }); + + function meshAdd(id, ind, pos) { + const geo = new THREE.BufferGeometry(); + geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); + geo.setIndex(new THREE.BufferAttribute(new Uint32Array(ind), 1)); + const mat = new THREE.LineBasicMaterial({ + transparent: true, + opacity: 0.75, + color + }); + const mesh = new THREE.LineSegments(geo, mat); + world.add(mesh); + meshes[id] = mesh; + } + + function meshUpdates(id, updates) { + const mesh = meshes[id]; + if (!mesh) { + return; // animate cancelled + } + const mpos = mesh.geometry.attributes.position; + for (let i=0, il=updates.length; i { - CAM.animate(API, 50); + CAM.animate(api, 50); }, 250); } @@ -112,7 +216,7 @@ self.kiri.load(function() { mesh.position.x = pos.x; mesh.position.y = pos.y; mesh.position.z = pos.z; - SPACE.update(); + space.update(); if (id !== 0) { toolPosX.value = ((pos.x + posOffset.x) * unitScale).toFixed(2); toolPosY.value = ((pos.y + posOffset.y) * unitScale).toFixed(2); @@ -125,109 +229,19 @@ self.kiri.load(function() { } } - function meshAdd(id, ind, pos) { - const geo = new THREE.BufferGeometry(); - geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); - geo.setIndex(new THREE.BufferAttribute(new Uint32Array(ind), 1)); - const mat = new THREE.LineBasicMaterial({ - transparent: true, - opacity: 0.75, - color - }); - const mesh = new THREE.LineSegments(geo, mat); - WORLD.add(mesh); - meshes[id] = mesh; +}); + +// ---( WORKER FUNCTIONS )--- + +kiri.load(() => { + if (!kiri.worker) { + return; } - function meshUpdates(id, updates) { - const mesh = meshes[id]; - if (!mesh) { - return; // animate cancelled - } - const mpos = mesh.geometry.attributes.position; - for (let i=0, il=updates.length; i= 0) { + renderSpeed = data.speed; + } + if (data.steps > 0) { + stepsRemain = data.steps; + } + if (data.toend) { + skipMode = !skipMode; + } else { + skipMode = false; + } + checkStash(send); + if (animating) { + return send.done(); + } + renderPath(send); + }; + + kiri.worker.animate_cleanup = function(data, send) { + if (animating) { + animateClear = true; + } + }; + function createGrid(stepsX, stepsY, size, step) { const gridPoints = stepsX * stepsY; const pos = new Float32Array(gridPoints * 3); @@ -300,33 +339,6 @@ self.kiri.load(function() { return { pos, ind }; } - if (KIRI.worker) - KIRI.worker.animate = function(data, send) { - if (data.speed >= 0) { - renderSpeed = data.speed; - } - if (data.steps > 0) { - stepsRemain = data.steps; - } - if (data.toend) { - skipMode = !skipMode; - } else { - skipMode = false; - } - checkStash(send); - if (animating) { - return send.done(); - } - renderPath(send); - }; - - if (KIRI.worker) - KIRI.worker.animate_cleanup = function(data, send) { - if (animating) { - animateClear = true; - } - }; - let animateClear = false; let animating = false; let renderSpeed = 25; @@ -507,10 +519,10 @@ self.kiri.load(function() { pos[(dx * pix + dy) * 3 + 2] = -dz; } send.data({ mesh_add: { id:++toolID, pos, ind }}); - } + } // load renderer code in worker context only - if (KIRI.worker && false) + if (kiri.worker && false) fetch('/wasm/kiri-ani.wasm') .then(response => response.arrayBuffer()) .then(bytes => WebAssembly.instantiate(bytes, { @@ -535,3 +547,5 @@ self.kiri.load(function() { }); }); + +}); diff --git a/src/kiri-mode/cam/client.js b/src/kiri-mode/cam/client.js index 1350e3c9..189a5420 100644 --- a/src/kiri-mode/cam/client.js +++ b/src/kiri-mode/cam/client.js @@ -2,1278 +2,1278 @@ "use strict"; -(function() { +gapp.register("kiri-mode.cam.client", [], (root, exports) => { - let KIRI = self.kiri, - BASE = self.base, - CAM = KIRI.driver.CAM, - PRO = CAM.process, - newPolygon = BASE.newPolygon, - newPoint = BASE.newPoint, - isAnimate, - isArrange, - isCamMode, - isParsed, - camStock, - current, - poppedRec, - hoveredOp, - API, FDM, SPACE, STACKS, MODES, VIEWS, UI, UC, LANG; +const { base, kiri } = root; +const { newPoint, newPolygon } = base; +const { driver } = kiri; +const { CAM } = driver; - let zaxis = { x: 0, y: 0, z: 1 }, - popOp = {}, - func = { - hover: noop, - hoverUp: noop - }, - flipping; +let PRO = CAM.process, + isAnimate, + isArrange, + isCamMode, + isParsed, + camStock, + current, + poppedRec, + hoveredOp, + API, FDM, SPACE, STACKS, MODES, VIEWS, UI, UC, LANG; - CAM.restoreTabs = restoreTabs; +let zaxis = { x: 0, y: 0, z: 1 }, + popOp = {}, + func = { + hover: noop, + hoverUp: noop + }, + flipping; - CAM.init = function(kiri, api) { - FDM = KIRI.driver.FDM; +CAM.restoreTabs = restoreTabs; - // console.log({kiri,api}) - SPACE = kiri.space; - MODES = kiri.consts.MODES; - VIEWS = kiri.consts.VIEWS; - STACKS = api.const.STACKS; - LANG = api.const.LANG; - UI = api.ui; - UC = api.uc; - API = api; +CAM.init = function(kiri, api) { + FDM = kiri.driver.FDM; - // wire up animate button in ui - api.event.on("function.animate", (mode) => { - if (isAnimate) { - return; - } - if (isCamMode && !camStock) { - return api.show.alert("animation requires stock to be enabled"); - } - api.function.prepare(() => { - if (isCamMode && camStock) { - animate(); - } - }); - }); + // console.log({kiri,api}) + SPACE = kiri.space; + MODES = kiri.consts.MODES; + VIEWS = kiri.consts.VIEWS; + STACKS = api.const.STACKS; + LANG = api.const.LANG; + UI = api.ui; + UC = api.uc; + API = api; - api.event.on("function.export", (mode) => { - if (isAnimate) { - CAM.animate_clear(api); - isAnimate = false; - } - }); - - api.event.on("mode.set", (mode) => { - isCamMode = mode === 'CAM'; - $('set-tools').style.display = isCamMode ? '' : 'none'; - kiri.space.platform.setColor(isCamMode ? 0xeeeeee : 0xcccccc); - updateStock(undefined, 'internal'); - UI.func.animate.style.display = isCamMode ? '' : 'none'; - if (!isCamMode) { - func.tabClear(); - func.traceDone(); - UI.label.slice.innerText = LANG.slice; - UI.label.preview.innerText = LANG.preview; - UI.label.export.innerText = LANG.export; - } else { - UI.label.slice.innerText = LANG.start; - } - // do not persist traces across page reloads - func.traceClear(); - func.opRender(); - }); - - api.event.on("view.set", (mode) => { - isArrange = (mode === VIEWS.ARRANGE); - isAnimate = false; - CAM.animate_clear(api); - func.tabDone(); - func.traceDone(); - func.opRender(); - }); - - api.event.on("settings.saved", (settings) => { - current = settings; - let proc = settings.process; - let hasTabs = false; - let hasTraces = false; - if (isCamMode && proc.ops) { - proc.ops = proc.ops.filter(v => v); - } - // for any tabs or traces to set markers - for (let widget of API.widgets.all()) { - let wannot = widget.anno; - if (wannot.tab && wannot.tab.length) hasTabs = true; - if (wannot.trace && wannot.trace.length) hasTraces = true; - } - // show/hide dots in enabled process pop buttons - api.ui.camTabs.marker.style.display = hasTabs ? 'flex' : 'none'; - api.ui.camStock.marker.style.display = proc.camStockOn ? 'flex' : 'none'; - updateStock(settings, 'settings.saved.internal'); - }); - - api.event.on("settings.load", (settings) => { - func.opRender(); - if (!isCamMode) return; - restoreTabs(api.widgets.all()); - }); - - api.event.on([ - "init-done", - // update stock when modes change? - "slice.end", - "preview.end", - // update stock when preferences change - // "boolean.click", - "boolean.update", - // update stock when objects move - "platform.layout", - "selection.drag", - "selection.move", - // force bounds update, too - "selection.scale", - "selection.rotate" - ], updateStock); - - api.event.on("preview.end", () => { - isParsed = false; - if (isCamMode && camStock) { - let bounds = STACKS.getStack("bounds"); - if (bounds) bounds.button("animate", animate); - } - }); - - api.event.on("code.loaded", (info) => { - if (isCamMode && camStock) { - isParsed = true; - let parse = STACKS.getStack("parse", SPACE.world); - if (parse) parse.button("animate", animate); - } - }); - - $('op-add').onmouseenter = () => { - if (func.unpop) func.unpop(); - }; - - $('op-add-list').onclick = (ev) => { - let settings = API.conf.get(); - let { process, device } = settings; - switch (ev.target.innerText) { - case "level": return func.opAddLevel(); - case "rough": return func.opAddRough(); - case "outline": return func.opAddOutline(); - case "contour": - let oplist = current.process.ops, axis = "X"; - for (let op of oplist) { - if (op.type === "contour" && op.axis === "X") { - axis = "Y"; - } - } - return func.opAddContour(axis); - case "register": return func.opAddRegister('X', 2); - case "drill": return func.opAddDrill(); - case "trace": return func.opAddTrace(); - case "flip": - // only one flip op permitted - for (let op of current.process.ops) { - if (op.type === 'flip') { - return; - } - } - return func.opAddFlip(); - } - }; - - func.opAddLevel = () => { - func.opAdd(popOp.level.new()); - }; - - func.opAddRough = () => { - func.opAdd(popOp.rough.new()); - }; - - func.opAddOutline = () => { - func.opAdd(popOp.outline.new()); - }; - - func.opAddContour = (axis) => { - let rec = popOp.contour.new(); - rec.axis = axis.toUpperCase(); - func.opAdd(rec); - }; - - func.opAddTrace = () => { - let rec = popOp.trace.new(); - rec.areas = { /* widget.id: [ polygons... ] */ }; - func.opAdd(rec); - }; - - func.opAddDrill = () => { - func.opAdd(popOp.drill.new()); - }; - - func.opAddRegister = (axis, points) => { - let rec = popOp.register.new(); - rec.axis = axis.toUpperCase(); - rec.points = points; - func.opAdd(rec); - }; - - func.opAddFlip = () => { - func.opAdd(popOp.flip.new()); - }; - - // TAB/TRACE BUTTON HANDLERS - api.event.on("button.click", target => { - let process = API.conf.get().process; - switch (target) { - case api.ui.tabAdd: - return func.tabAdd(); - case api.ui.tabDun: - return func.tabDone(); - case api.ui.tabClr: - api.uc.confirm("clear tabs?").then(ok => { - if (ok) func.tabClear(); - }); - break; - } - }); - - // OPS FUNCS - api.event.on("cam.op.add", func.opAdd = (rec) => { - if (!isCamMode) return; - let oplist = current.process.ops; - if (oplist.indexOf(rec) < 0) { - oplist.push(rec); - API.conf.save(); - func.opRender(); - } - }); - - api.event.on("cam.op.del", func.opDel = (rec) => { - if (!isCamMode) return; - let oplist = current.process.ops; - let pos = oplist.indexOf(rec); - if (pos >= 0) { - oplist.splice(pos,1); - API.conf.save(); - func.opRender(); - } - }); - - api.event.on("cam.op.render", func.opRender = () => { - $('camops').style.display = isCamMode && isArrange ? 'flex' : ''; - let oplist = current.process.ops; - if (!(isCamMode && oplist)) return; - let mark = Date.now(); - let html = []; - $('ophint').style.display = oplist.length === 0 ? '' : 'none'; - let bind = {}; - let scale = API.view.unit_scale(); - oplist.forEach((rec,i) => { - html.appendAll([ - `
`, - ``, - ``, - `
` - ]); - bind[mark+i] = rec; - }); - let listel = $('oplist'); - listel.innerHTML = html.join(''); - let bounds = []; - let unpop = null; - // drag and drop re-ordering - for (let [id, rec] of Object.entries(bind)) { - $(`${id}-x`).onmousedown = (ev) => { - ev.stopPropagation(); - ev.preventDefault(); - func.traceDone(); - func.tabDone(); - func.opDel(rec); - }; - let el = $(id); - if (rec.disabled) { - el.classList.add("disabled"); - } - bounds.push(el); - let timer = null; - let inside = true; - let poprec = popOp[rec.type]; - el.unpop = () => { - let pos = [...el.childNodes].indexOf(poprec.div); - if (pos >= 0) { - el.removeChild(poprec.div); - } - }; - el.onmouseenter = (ev) => { - if (unpop) unpop(); - unpop = func.unpop = el.unpop; - inside = true; - // pointer to current rec for trace editing - poppedRec = rec; - poprec.use(rec); - hoveredOp = el; - el.appendChild(poprec.div); - // option click event appears latent - // and overides the sticky settings - setTimeout(() => { - UC.setSticky(false); - }, 0); - }; - el.onmouseleave = () => { - inside = false; - clearTimeout(timer); - timer = setTimeout(() => { - if (!inside && poprec.using(rec) && !UC.isSticky()) { - el.unpop(); - } - }, 250); - }; - el.rec = rec; - el.onmousedown = (ev) => { - func.traceDone(); - let target = ev.target, clist = target.classList; - if (!clist.contains("draggable")) { - return; - } - if (ev.ctrlKey || ev.metaKey) { - rec.disabled = !rec.disabled; - ev.preventDefault(); - ev.stopPropagation(); - if (rec.disabled) { - clist.add("disabled"); - } else { - clist.remove("disabled"); - } - return true; - } - clist.add("drag"); - ev.stopPropagation(); - ev.preventDefault(); - let tracker = UI.tracker; - tracker.style.display = 'block'; - let cancel = tracker.onmouseup = (ev) => { - oplist = current.process.ops; - clist.remove("drag"); - tracker.style.display = 'none'; - if (ev) { - ev.stopPropagation(); - ev.preventDefault(); - } - oplist.length = 0; - for (let child of listel.childNodes) { - oplist.push(child.rec); - } - API.conf.save(); - func.opRender(); - }; - tracker.onmousemove = (ev) => { - ev.stopPropagation(); - ev.preventDefault(); - if (ev.buttons === 0) { - return cancel(); - } - for (let el of bounds) { - if (el === target) continue; - let rect = el.getBoundingClientRect(); - let left = rect.left; - let right = rect.left + rect.width; - if (ev.pageX >= left && ev.pageX <= right) { - let mid = (left + right) / 2; - try { listel.removeChild(target); } catch (e) { } - el.insertAdjacentElement(ev.pageX < mid ? "beforebegin" : "afterend", target); - } - } - }; - }; - } - }); - - func.opFlip = () => { - let widgets = API.widgets.all(); - let { process } = current; - let { ops, op2 } = process; - // add flip singleton to b-side - let add2 = op2.length === 0; - let axis = poppedRec.axis; - flipping = true; - process.camZAnchor = { - top: "bottom", - bottom: "top", - middle: "middle" - }[process.camZAnchor]; - // flip tabs - for (let widget of widgets) { - let anno = API.widgets.annotate(widget.id).tab || []; - let wbm = widget.bounds.max.z; - for (let tab of anno) { - let box = widget.tabs[tab.id].box; - let bpo = box.position; - let xr = 0, yr = 0; - let flz = wbm - bpo.z; - if (axis === 'X') { - tab.pos.y = -tab.pos.y; - bpo.y = -bpo.y; - xr = Math.PI / 2; - } - if (axis === 'Y') { - tab.pos.x = -tab.pos.x; - bpo.x = -bpo.x; - yr = Math.PI / 2; - } - tab.pos.z = bpo.z = flz; - let { rot } = tab; - let qat = new THREE.Quaternion(rot._x, rot._y, rot._z, rot._w); - let eul = new THREE.Euler().setFromQuaternion(qat); - eul._z = -eul._z; - tab.rot = new THREE.Quaternion().setFromEuler(eul); - } - clearTabs(widget, true); - restoreTabs([widget]); - } - // flip widget - if (axis === 'X') { - API.selection.rotate(Math.PI, 0, 0); - } - if (axis === 'Y') { - API.selection.rotate(0, Math.PI, 0); - } - flipping = false; - process.ops = op2; - process.op2 = ops; - // special case, auto-adjust camZBottom - if (process.camZBottom && widgets.length === 1) { - process.camZBottom = widgets[0].bounds.max.z - process.camZBottom; - API.util.rec2ui(process); - } - for (let op of op2) { - if (op.type === 'flip') { - op.axis = poppedRec.axis; - } - } - if (add2) { - func.opAdd(poppedRec); - } else { - func.opRender(); - } - }; - - // TAB FUNCS - let showTab, lastTab, tab, iw, ic; - api.event.on("cam.tabs.add", func.tabAdd = () => { - func.traceDone(); - alert = api.show.alert("[esc] cancels tab editing"); - api.feature.hover = true; - func.hover = func.tabHover; - func.hoverUp = func.tabHoverUp; - }); - api.event.on("cam.tabs.done", func.tabDone = () => { - delbox('tabb'); - api.hide.alert(alert); - api.feature.hover = false; - if (lastTab) { - lastTab.box.material.color.r = 0; - lastTab = null; - } - }); - api.event.on("cam.tabs.clear", func.tabClear = () => { - func.tabDone(); - API.widgets.all().forEach(widget => { - clearTabs(widget); - widget.saveState(); - }); - API.conf.save(); - }); - func.tabHover = function(data) { - delbox('tabb'); - const { int, type, point } = data; - const object = int ? int.object : null; - const tab = int ? object.tab : null; - if (lastTab) { - lastTab.box.material.color.r = 0; - lastTab = null; - } - if (tab) { - tab.box.material.color.r = 0.5; - lastTab = tab; - return; - } - if (type !== 'widget') { - iw = null; - return; - } - let n = int.face.normal; - iw = int.object.widget; - ic = int.point; - // only near vertical faces - if (Math.abs(n.z) > 0.3) { - return; - } - showTab = createTabBox(iw, ic, n); - }; - func.tabHoverUp = function(int) { - delbox('tabb'); - if (lastTab) { - const {widget, box, id} = lastTab; - widget.adds.remove(box); - widget.mesh.remove(box); - delete widget.tabs[id]; - let ta = API.widgets.annotate(widget.id).tab; - let ix = 0; - ta.forEach((rec,i) => { - if (rec.id === id) { - ix = i; - } - }); - ta.splice(ix,1); - API.conf.save(); - widget.saveState(); - return; - } - if (!iw) return; - let ip = iw.track.pos; - let wa = api.widgets.annotate(iw.id); - let wt = (wa.tab = wa.tab || []); - let pos = { - x: showTab.pos.x - ip.x, - y: -showTab.pos.z - ip.y, - z: showTab.pos.y + ip.z, - } - let id = Date.now(); - let { dim, rot } = showTab; - let rec = { pos, dim, rot, id }; - wt.push(Object.clone(rec)); - addWidgetTab(iw, rec); - API.conf.save(); - iw.saveState(); - }; - - // TRACE FUNCS - let traceOn = false, lastTrace; - func.traceAdd = (ev) => { - func.unpop(); - func.tabDone(); - func.traceDone(); - alert = api.show.alert("analyzing parts...", 1000); - traceOn = hoveredOp; - traceOn.classList.add("editing"); - 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); - widget.trace_stack = null; - } - if (widget.trace_stack) { - widget.adds.appendAll(widget.trace_stack.meshes); - widget.trace_stack.show(); - return; - } - let areas = (poppedRec.areas[widget.id] || []); - let stack = new KIRI.Stack(widget.mesh); - widget.trace_stack = stack; - widget.traces.forEach(poly => { - let match = areas.filter(arr => poly.matches(arr)); - let layers = new KIRI.Layers(); - layers.setLayer("trace", {line: 0xaaaa55}, false).addPoly(poly); - stack.addLayers(layers); - stack.new_meshes.forEach(mesh => { - mesh.trace = {widget, poly}; - // ensure trace poly singleton from matches - if (match.length > 0) { - poly._trace = match[0]; - } else { - poly._trace = poly.toArray(); - } - }); - widget.adds.appendAll(stack.new_meshes); - }); - }); - // ensure appropriate traces are toggled matching current record - KIRI.api.widgets.for(widget => { - let areas = (poppedRec.areas[widget.id] || []); - let stack = widget.trace_stack; - stack.meshes.forEach(mesh => { - let { poly } = mesh.trace; - let match = areas.filter(arr => poly.matches(arr)); - if (match.length > 0) { - if (!mesh.selected) { - func.traceToggle(mesh, true); - } - } else if (mesh.selected) { - func.traceToggle(mesh, true); - } - }); - }); - }, poppedRec.single); - api.feature.hover = true; - api.feature.hoverAdds = true; - func.hover = func.traceHover; - func.hoverUp = func.traceHoverUp; - }; - func.traceDone = () => { - if (!traceOn) { - return; - } - func.unpop(); - traceOn.classList.remove("editing"); - traceOn = false; - KIRI.api.widgets.opacity(1); - api.hide.alert(alert); - api.feature.hover = false; - api.feature.hoverAdds = false; - KIRI.api.widgets.for(widget => { - if (widget.trace_stack) { - widget.trace_stack.hide(); - widget.adds.removeAll(widget.trace_stack.meshes); - } - }); - }; - api.event.on("cam.trace.clear", func.traceClear = () => { - func.traceDone(); - API.widgets.all().forEach(widget => { - unselectTraces(widget); - }); - API.conf.save(); - }); - func.traceHover = function(data) { - if (lastTrace) { - let { color, colorSave } = lastTrace.material[0]; - color.r = colorSave.r; - color.g = colorSave.g; - color.b = colorSave.b; - lastTrace.position.z -= 0.05; - } - if (data.type === 'platform') { - lastTrace = null; - return; - } - if (!data.int.object.trace) { - return; - } - lastTrace = data.int.object; - lastTrace.position.z += 0.05; - if (lastTrace.selected) { - let event = data.event; - let target = event.target; - let { clientX, clientY } = event; - let { offsetWidth, offsetHeight } = target; - } - let material = lastTrace.material[0]; - let color = material.color; - let {r, g, b} = color; - material.colorSave = {r, g, b}; - color.r = 0; - color.g = 0; - color.b = 1; - }; - func.traceHoverUp = function(int, ev) { - if (!int) return; - let { object } = int; - func.traceToggle(object); - if (ev.metaKey || ev.ctrlKey) { - let { selected } = object; - let { widget, poly } = object.trace; - for (let add of widget.adds) { - if (add.selected !== selected && add.trace.poly.getZ() === poly.getZ()) { - func.traceToggle(add); - } - } - } - }; - func.traceToggle = function(obj, skip) { - let material = obj.material[0]; - let { color, colorSave } = material; - let { widget, poly } = obj.trace; - let areas = poppedRec.areas; - if (!areas) { - return; - } - 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 - }; - } - if (obj.selected) { - obj.position.z += 0.05; - color.r = colorSave.r = 0.9; - color.g = colorSave.g = 0; - color.b = colorSave.b = 0.1; - if (!skip) wlist.push(poly._trace); - } else { - obj.position.z -= 0.05; - color.r = colorSave.r = 0xaa/255; - color.g = colorSave.g = 0xaa/255; - color.b = colorSave.b = 0x55/255; - if (!skip) wlist.remove(poly._trace); - } - API.conf.save(); - }; - - // COMMON TAB/TRACE EVENT HANDLERS - api.event.on("slice.begin", () => { - if (isCamMode) { - func.tabDone(); - func.traceDone(); - } - }); - api.event.on("key.esc", () => { - if (isCamMode) { - func.tabDone(); - func.traceDone(); - } - }); - api.event.on("selection.scale", () => { - if (isCamMode) { - func.tabClear(); - func.traceClear(); - } - }); - api.event.on("widget.duplicate", (widget, oldwidget) => { - if (!isCamMode) { - return; - } - if (traceOn) { - func.traceDone(); - } - unselectTraces(widget); - if (flipping) { - return; - } - let ann = API.widgets.annotate(widget.id); - if (ann.tab) { - ann.tab.forEach((tab,i) => { - tab.id = Date.now() + i; - }); - restoreTabs([widget]); - } - }); - api.event.on("widget.mirror", widget => { - if (!isCamMode) { - return; - } - if (traceOn) { - func.traceDone(); - } - unselectTraces(widget); - if (flipping) { - return; - } - mirrorTabs(widget); - }); - api.event.on("widget.rotate", rot => { - if (!isCamMode) { - return; - } - let {widget, x, y, z} = rot; - if (traceOn) { - func.traceDone(); - } - unselectTraces(widget); - if (flipping) { - return; - } - if (x || y) { - clearTabs(widget); - } else { - rotateTabs(widget, x, y, z); - } - }); - api.event.on("mouse.hover.up", rec => { - if (!isCamMode) { - return; - } - let { object, event } = rec; - func.hoverUp(object, event); - }); - api.event.on("mouse.hover", data => { - if (!isCamMode) { - return; - } - func.hover(data); - }); - - function mirrorTabs(widget) { - let tabs = API.widgets.annotate(widget.id).tab || []; - tabs.forEach(rec => { - let { id, pos, rot } = rec; - let tab = widget.tabs[id]; - let e = new THREE.Euler().setFromQuaternion(rot); - e._z = Math.PI - e._z; - let { _x, _y, _z, _w } = rec.rot; - let or = new THREE.Quaternion(_x, _y, _z, _w); - let nr = new THREE.Quaternion().setFromEuler(e); - let ra = or.angleTo(nr); - console.log({or, nr, ra}); - rec.rot = nr; - // let m4 = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(0,0,e._z)); - // tab.box.geometry.applyMatrix4(m4); - tab.box.position.x = pos.x = -pos.x; - }); - SPACE.update(); - } - - function rotateTabs(widget, x, y, z) { - let tabs = API.widgets.annotate(widget.id).tab || []; - tabs.forEach(rec => { - let { id, pos, rot } = rec; - let coff = widget.track.center; - let tab = widget.tabs[id]; - let m4 = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(x || 0, y || 0, z || 0)); - // update position vector - let vc = new THREE.Vector3(pos.x, pos.y, pos.z).applyMatrix4(m4); - // update rotation quaternion - rec.rot = new THREE.Quaternion().multiplyQuaternions( - new THREE.Quaternion(rot._x, rot._y, rot._z, rot._w), - new THREE.Quaternion().setFromRotationMatrix(m4) - ); - tab.box.geometry.applyMatrix4(m4); - tab.box.position.x = pos.x = vc.x - coff.dx; - tab.box.position.y = pos.y = vc.y - coff.dy; - tab.box.position.z = pos.z = vc.z; - }); - SPACE.update(); - } - - function hasSpindle() { - return current.device.spindleMax > 0; - } - - createPopOp('level', { - tool: 'camLevelTool', - spindle: 'camLevelSpindle', - step: 'camLevelOver', - rate: 'camLevelSpeed', - down: 'camLevelDown' - }).inputs = { - tool: UC.newSelect(LANG.cc_tool, {}, "tools"), - sep: UC.newBlank({class:"pop-sep"}), - spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), - step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,1.0)}), - rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), - down: UC.newInput(LANG.cc_loff_s, {title:LANG.cc_loff_l, convert:UC.toFloat, units:true}), - }; - - createPopOp('rough', { - tool: 'camRoughTool', - spindle: 'camRoughSpindle', - down: 'camRoughDown', - step: 'camRoughOver', - rate: 'camRoughSpeed', - plunge: 'camRoughPlunge', - leave: 'camRoughStock', - voids: 'camRoughVoid', - flats: 'camRoughFlat', - inside: 'camRoughIn', - top: 'camRoughTop' - }).inputs = { - tool: UC.newSelect(LANG.cc_tool, {}, "tools"), - sep: UC.newBlank({class:"pop-sep"}), - spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), - down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true}), - step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,1.0)}), - rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), - plunge: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), - leave: UC.newInput(LANG.cr_lsto_s, {title:LANG.cr_lsto_l, convert:UC.toFloat, units:true}), - sep: UC.newBlank({class:"pop-sep"}), - voids: UC.newBoolean(LANG.cr_clrp_s, undefined, {title:LANG.cr_clrp_l}), - flats: UC.newBoolean(LANG.cr_clrf_s, undefined, {title:LANG.cr_clrf_l}), - top: UC.newBoolean(LANG.cr_clrt_s, undefined, {title:LANG.cr_clrt_l}), - inside: UC.newBoolean(LANG.cr_olin_s, undefined, {title:LANG.cr_olin_l}) - }; - - createPopOp('outline', { - tool: 'camOutlineTool', - spindle: 'camOutlineSpindle', - step: 'camOutlineOver', - down: 'camOutlineDown', - rate: 'camOutlineSpeed', - plunge: 'camOutlinePlunge', - dogbones: 'camOutlineDogbone', - omitthru: 'camOutlineOmitThru', - outside: 'camOutlineOut', - inside: 'camOutlineIn', - wide: 'camOutlineWide' - }).inputs = { - tool: UC.newSelect(LANG.cc_tool, {}, "tools"), - sep: UC.newBlank({class:"pop-sep"}), - spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), - down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true}), - step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,1.0), show:() => popOp.outline.rec.wide}), - rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), - plunge: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), - sep: UC.newBlank({class:"pop-sep"}), - dogbones: UC.newBoolean(LANG.co_dogb_s, undefined, {title:LANG.co_dogb_l, show:(op) => { return !op.inputs.wide.checked }}), - inside: UC.newBoolean(LANG.co_olin_s, undefined, {title:LANG.co_olin_l, show:(op) => { return !op.inputs.outside.checked }}), - outside: UC.newBoolean(LANG.co_olot_s, undefined, {title:LANG.co_olot_l, show:(op) => { return !op.inputs.inside.checked }}), - omitthru: UC.newBoolean(LANG.co_omit_s, undefined, {title:LANG.co_omit_l, xshow:(op) => { return op.inputs.outside.checked }}), - wide: UC.newBoolean(LANG.co_wide_s, undefined, {title:LANG.co_wide_l, show:(op) => { return !op.inputs.inside.checked }}) - }; - - createPopOp('contour', { - tool: 'camContourTool', - spindle: 'camContourSpindle', - step: 'camContourOver', - rate: 'camContourSpeed', - angle: 'camContourAngle', - tolerance: 'camTolerance', - curves: 'camContourCurves', - inside: 'camContourIn', - axis: 'X' - }).inputs = { - tool: UC.newSelect(LANG.cc_tool, {}, "tools"), - axis: UC.newSelect(LANG.cd_axis, {}, "regaxis"), - sep: UC.newBlank({class:"pop-sep"}), - spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), - step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,1.0)}), - rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), - angle: UC.newInput(LANG.cf_angl_s, {title:LANG.cf_angl_l, convert:UC.toFloat, bound:UC.bound(45,90)}), - tolerance: UC.newInput(LANG.ou_toll_s, {title:LANG.ou_toll_l, convert:UC.toFloat, bound:UC.bound(0,10.0), units:true}), - sep: UC.newBlank({class:"pop-sep"}), - curves: UC.newBoolean(LANG.cf_curv_s, undefined, {title:LANG.cf_curv_l}), - inside: UC.newBoolean(LANG.cf_olin_s, undefined, {title:LANG.cf_olin_l}) - }; - - createPopOp('trace', { - mode: 'camTraceType', - offset: 'camTraceOffset', - spindle: 'camTraceSpindle', - tool: 'camTraceTool', - step: 'camTraceOver', - down: 'camTraceDown', - rate: 'camTraceSpeed', - plunge: 'camTracePlunge', - single: 'camTraceLines', - }).inputs = { - tool: UC.newSelect(LANG.cc_tool, {}, "tools"), - sep: UC.newBlank({class:"pop-sep"}), - 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"), - spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), - step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,1.0), show:(op) => popOp.trace.rec.mode === "clear"}), - down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true}), - rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), - plunge: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), - sep: UC.newBlank({class:"pop-sep"}), - single: UC.newBoolean(LANG.cc_sngl_s, undefined, {title:LANG.cc_sngl_l}), - select: UC.newRow([ - UC.newButton(undefined, func.traceAdd, {icon:''}), - UC.newButton(undefined, func.traceDone, {icon:''}), - ], {class:"ext-buttons f-row"}), - }; - - createPopOp('drill', { - tool: 'camDrillTool', - spindle: 'camDrillSpindle', - down: 'camDrillDown', - rate: 'camDrillDownSpeed', - dwell: 'camDrillDwell', - lift: 'camDrillLift', - mark: 'camDrillMark' - }).inputs = { - tool: UC.newSelect(LANG.cc_tool, {}, "tools"), - sep: UC.newBlank({class:"pop-sep"}), - spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), - down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true}), - rate: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), - dwell: UC.newInput(LANG.cd_dwll_s, {title:LANG.cd_dwll_l, convert:UC.toFloat}), - lift: UC.newInput(LANG.cd_lift_s, {title:LANG.cd_lift_l, convert:UC.toFloat, units:true, show:() => !poppedRec.mark}), - mark: UC.newBoolean(LANG.cd_mark_s, undefined, {title:LANG.cd_mark_l}), - }; - - createPopOp('register', { - tool: 'camDrillTool', - spindle: 'camDrillSpindle', - down: 'camDrillDown', - rate: 'camDrillDownSpeed', - dwell: 'camDrillDwell', - lift: 'camDrillLift', - feed: 'camRegisterSpeed', - }).inputs = { - tool: UC.newSelect(LANG.cc_tool, {}, "tools"), - axis: UC.newSelect(LANG.cd_axis, {}, "regaxis"), - points: UC.newSelect(LANG.cd_points, {show:() => poppedRec.axis !== '-'}, "regpoints"), - sep: UC.newBlank({class:"pop-sep"}), - spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), - feed: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true, show:() => poppedRec.axis === '-'}), - down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true}), - rate: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), - dwell: UC.newInput(LANG.cd_dwll_s, {title:LANG.cd_dwll_l, convert:UC.toFloat, show:() => poppedRec.axis !== '-'}), - lift: UC.newInput(LANG.cd_lift_s, {title:LANG.cd_lift_l, convert:UC.toFloat, units:true, show:() => poppedRec.axis !== '-'}) - }; - - createPopOp('flip', { - axis: 'camFlipAxis' - }).inputs = { - axis: UC.newSelect(LANG.cd_axis, {}, "regaxis"), - sep: UC.newBlank({class:"pop-sep"}), - action: UC.newRow([ - UC.newButton(LANG.cc_flip, func.opFlip) - ], {class:"ext-buttons f-row"}) - }; - }; - - function createPopOp(type, map) { - let op = popOp[type] = { - div: UC.newElement('div', { id:`${type}-op`, class:"cam-pop-op" }), - use: (rec) => { - op.rec = rec; - for (let [key, val] of Object.entries(op.inputs)) { - let type = val.type; - let from = map[key]; - if (rec[key] === undefined && type && from) { - let newval = current.process[from]; - rec[key] = current.process[from]; - } - } - API.util.rec2ui(rec, op.inputs); - op.hideshow(); - }, - using: (rec) => { - return op.rec === rec; - }, - bind: (ev) => { - API.util.ui2rec(op.rec, op.inputs); - for (let [key, val] of Object.entries(op.rec)) { - let saveTo = map[key]; - if (saveTo) { - current.process[saveTo] = val; - } - } - API.conf.save(); - op.hideshow(); - }, - new: () => { - let rec = { type }; - for (let [key, val] of Object.entries(map)) { - rec[key] = current.process[val]; - } - return rec; - }, - hideshow: () => { - for (let inp of Object.values(op.inputs)) { - let parent = inp.parentElement; - if (parent.setVisible && parent.__opt.show) { - parent.setVisible(parent.__opt.show(op)); - } - } - }, - group: [] - }; - UC.restore({ - addTo: op.div, - bindTo: op.bind, - lastDiv: op.div, - lastGroup: op.group - }); - return op; - } - - function createTabBox(iw, ic, n) { - const { track } = iw; - const { stock, bounds, process } = API.conf.get(); - const { camTabsWidth, camTabsHeight, camTabsDepth, camTabsMidline } = process; - const sz = stock.z || bounds.max.z; - const zto = sz - iw.track.top; - const zp = sz - track.box.d + process.camZBottom - zto + (camTabsMidline ? 0 : camTabsHeight / 2); - ic.x += n.x * camTabsDepth / 2; // offset from part - ic.z -= n.y * camTabsDepth / 2; // offset swap z,y - ic.y = zp; // offset swap in world space y,z - const rot = new THREE.Quaternion().setFromAxisAngle(zaxis, Math.atan2(n.y, n.x)); - const pos = { x:ic.x, y:ic.y, z:ic.z }; - const dim = { x:camTabsDepth, y:camTabsWidth, z:camTabsHeight }; - const tab = addbox(pos, 0x0000dd, 'tabb', dim, { rotate: rot }); - return { pos, dim, rot, tab, width: camTabsWidth, height: camTabsHeight }; - } - - function addWidgetTab(widget, rec) { - const { pos, dim, rot, id } = rec; - const tabs = widget.tabs = (widget.tabs || {}); - // prevent duplicate restore from repeated settings load calls - if (!tabs[id]) { - pos.box = addbox( - pos, 0x0000dd, id, - dim, { group: widget.mesh, rotate: rot } - ); - pos.box.tab = Object.assign({widget, id}, pos); - widget.adds.push(pos.box); - tabs[id] = pos; - } - } - - function restoreTabs(widgets) { - widgets.forEach(widget => { - const tabs = API.widgets.annotate(widget.id).tab || []; - tabs.forEach(rec => { - rec = Object.clone(rec); - rec.rot = new THREE.Quaternion(rec.rot._x ,rec.rot._y, rec.rot._z, rec.rot._w); - addWidgetTab(widget, rec); - }); - }); - } - - function clearTabs(widget, skiprec) { - Object.values(widget.tabs || {}).forEach(rec => { - widget.adds.remove(rec.box); - widget.mesh.remove(rec.box); - }); - widget.tabs = {}; - if (!skiprec) { - delete API.widgets.annotate(widget.id).tab; - } - } - - function unselectTraces(widget, skip) { - if (widget.trace_stack) { - widget.trace_stack.meshes.forEach(mesh => { - if (mesh.selected) { - func.traceToggle(mesh, skip); - } - }); - } - } - - function addbox() { return FDM.addbox(...arguments)}; - function delbox() { return FDM.delbox(...arguments)}; - - function animate() { - isAnimate = true; - API.widgets.opacity(isParsed ? 0 : 0.75); - API.hide.slider(); - STACKS.clear(); - CAM.animate(API); - } - - function updateStock(args, event) { + // wire up animate button in ui + api.event.on("function.animate", (mode) => { if (isAnimate) { return; } - - STACKS.remove('bounds'); - if (isCamMode && isArrange) { - STACKS.clear(); + if (isCamMode && !camStock) { + return api.show.alert("animation requires stock to be enabled"); } + api.function.prepare(() => { + if (isCamMode && camStock) { + animate(); + } + }); + }); + api.event.on("function.export", (mode) => { + if (isAnimate) { + CAM.animate_clear(api); + isAnimate = false; + } + }); + + api.event.on("mode.set", (mode) => { + isCamMode = mode === 'CAM'; + $('set-tools').style.display = isCamMode ? '' : 'none'; + kiri.space.platform.setColor(isCamMode ? 0xeeeeee : 0xcccccc); + updateStock(undefined, 'internal'); + UI.func.animate.style.display = isCamMode ? '' : 'none'; if (!isCamMode) { - if (camStock) { - SPACE.world.remove(camStock); - camStock = null; - } - return; + func.tabClear(); + func.traceDone(); + UI.label.slice.innerText = LANG.slice; + UI.label.preview.innerText = LANG.preview; + UI.label.export.innerText = LANG.export; + } else { + UI.label.slice.innerText = LANG.start; } + // do not persist traces across page reloads + func.traceClear(); + func.opRender(); + }); - const refresh = (event === "selection.scale" || event === 'selection.rotate'); + api.event.on("view.set", (mode) => { + isArrange = (mode === VIEWS.ARRANGE); + isAnimate = false; + CAM.animate_clear(api); + func.tabDone(); + func.traceDone(); + func.opRender(); + }); - let settings = API.conf.get(); - let widgets = API.widgets.all(); + api.event.on("settings.saved", (settings) => { + current = settings; let proc = settings.process; - let enabled = UI.camStockOn.checked; - let offset = UI.camStockOffset.checked; - let stockSet = offset || (proc.camStockX && proc.camStockY && proc.camStockZ > 0); - let topZ = API.platform.top_z(); - let delta = 0; - let csox = 0; - let csoy = 0; - let stock = settings.stock = { }; - let compute = enabled && stockSet && (!offset || widgets.length); + let hasTabs = false; + let hasTraces = false; + if (isCamMode && proc.ops) { + proc.ops = proc.ops.filter(v => v); + } + // for any tabs or traces to set markers + for (let widget of API.widgets.all()) { + let wannot = widget.anno; + if (wannot.tab && wannot.tab.length) hasTabs = true; + if (wannot.trace && wannot.trace.length) hasTraces = true; + } + // show/hide dots in enabled process pop buttons + api.ui.camTabs.marker.style.display = hasTabs ? 'flex' : 'none'; + api.ui.camStock.marker.style.display = proc.camStockOn ? 'flex' : 'none'; + updateStock(settings, 'settings.saved.internal'); + }); - UI.func.animate.classList.add('disabled'); + api.event.on("settings.load", (settings) => { + func.opRender(); + if (!isCamMode) return; + restoreTabs(api.widgets.all()); + }); - // create/inject cam stock if stock size other than default - if (compute) { - UI.func.animate.classList.remove('disabled'); - let csx = proc.camStockX; - let csy = proc.camStockY; - let csz = proc.camStockZ; - if (offset) { - let min = { x: Infinity, y: Infinity, z: 0 }; - let max = { x: -Infinity, y: -Infinity, z: -Infinity }; - widgets.forEach(function(widget) { - let wbnd = widget.getBoundingBox(refresh); - let wpos = widget.track.pos; - min = { - x: Math.min(min.x, wpos.x + wbnd.min.x), - y: Math.min(min.y, wpos.y + wbnd.min.y), - z: 0 - }; - max = { - x: Math.max(max.x, wpos.x + wbnd.max.x), - y: Math.max(max.y, wpos.y + wbnd.max.y), - z: Math.max(max.z, wbnd.max.z) - }; + api.event.on([ + "init-done", + // update stock when modes change? + "slice.end", + "preview.end", + // update stock when preferences change + // "boolean.click", + "boolean.update", + // update stock when objects move + "platform.layout", + "selection.drag", + "selection.move", + // force bounds update, too + "selection.scale", + "selection.rotate" + ], updateStock); + + api.event.on("preview.end", () => { + isParsed = false; + if (isCamMode && camStock) { + let bounds = STACKS.getStack("bounds"); + if (bounds) bounds.button("animate", animate); + } + }); + + api.event.on("code.loaded", (info) => { + if (isCamMode && camStock) { + isParsed = true; + let parse = STACKS.getStack("parse", SPACE.world); + if (parse) parse.button("animate", animate); + } + }); + + $('op-add').onmouseenter = () => { + if (func.unpop) func.unpop(); + }; + + $('op-add-list').onclick = (ev) => { + let settings = API.conf.get(); + let { process, device } = settings; + switch (ev.target.innerText) { + case "level": return func.opAddLevel(); + case "rough": return func.opAddRough(); + case "outline": return func.opAddOutline(); + case "contour": + let oplist = current.process.ops, axis = "X"; + for (let op of oplist) { + if (op.type === "contour" && op.axis === "X") { + axis = "Y"; + } + } + return func.opAddContour(axis); + case "register": return func.opAddRegister('X', 2); + case "drill": return func.opAddDrill(); + case "trace": return func.opAddTrace(); + case "flip": + // only one flip op permitted + for (let op of current.process.ops) { + if (op.type === 'flip') { + return; + } + } + return func.opAddFlip(); + } + }; + + func.opAddLevel = () => { + func.opAdd(popOp.level.new()); + }; + + func.opAddRough = () => { + func.opAdd(popOp.rough.new()); + }; + + func.opAddOutline = () => { + func.opAdd(popOp.outline.new()); + }; + + func.opAddContour = (axis) => { + let rec = popOp.contour.new(); + rec.axis = axis.toUpperCase(); + func.opAdd(rec); + }; + + func.opAddTrace = () => { + let rec = popOp.trace.new(); + rec.areas = { /* widget.id: [ polygons... ] */ }; + func.opAdd(rec); + }; + + func.opAddDrill = () => { + func.opAdd(popOp.drill.new()); + }; + + func.opAddRegister = (axis, points) => { + let rec = popOp.register.new(); + rec.axis = axis.toUpperCase(); + rec.points = points; + func.opAdd(rec); + }; + + func.opAddFlip = () => { + func.opAdd(popOp.flip.new()); + }; + + // TAB/TRACE BUTTON HANDLERS + api.event.on("button.click", target => { + let process = API.conf.get().process; + switch (target) { + case api.ui.tabAdd: + return func.tabAdd(); + case api.ui.tabDun: + return func.tabDone(); + case api.ui.tabClr: + api.uc.confirm("clear tabs?").then(ok => { + if (ok) func.tabClear(); }); - csx += max.x - min.x; - csy += max.y - min.y; - csz += max.z - min.z; - csox = min.x + ((max.x - min.x) / 2); - csoy = min.y + ((max.y - min.y) / 2); + break; + } + }); + + // OPS FUNCS + api.event.on("cam.op.add", func.opAdd = (rec) => { + if (!isCamMode) return; + let oplist = current.process.ops; + if (oplist.indexOf(rec) < 0) { + oplist.push(rec); + API.conf.save(); + func.opRender(); + } + }); + + api.event.on("cam.op.del", func.opDel = (rec) => { + if (!isCamMode) return; + let oplist = current.process.ops; + let pos = oplist.indexOf(rec); + if (pos >= 0) { + oplist.splice(pos,1); + API.conf.save(); + func.opRender(); + } + }); + + api.event.on("cam.op.render", func.opRender = () => { + $('camops').style.display = isCamMode && isArrange ? 'flex' : ''; + let oplist = current.process.ops; + if (!(isCamMode && oplist)) return; + let mark = Date.now(); + let html = []; + $('ophint').style.display = oplist.length === 0 ? '' : 'none'; + let bind = {}; + let scale = API.view.unit_scale(); + oplist.forEach((rec,i) => { + html.appendAll([ + `
`, + ``, + ``, + `
` + ]); + bind[mark+i] = rec; + }); + let listel = $('oplist'); + listel.innerHTML = html.join(''); + let bounds = []; + let unpop = null; + // drag and drop re-ordering + for (let [id, rec] of Object.entries(bind)) { + $(`${id}-x`).onmousedown = (ev) => { + ev.stopPropagation(); + ev.preventDefault(); + func.traceDone(); + func.tabDone(); + func.opDel(rec); + }; + let el = $(id); + if (rec.disabled) { + el.classList.add("disabled"); } - if (!camStock) { - let geo = new THREE.BoxGeometry(1, 1, 1); - let mat = new THREE.MeshBasicMaterial({ - color: 0x777777, - opacity: 0.2, - transparent: true, - side:THREE.DoubleSide - }); - camStock = new THREE.Mesh(geo, mat); - camStock.renderOrder = 2; - SPACE.world.add(camStock); - } - stock = settings.stock = { - x: csx, - y: csy, - z: csz, - center: { - x: csox, - y: csoy, - z: csz / 2 + bounds.push(el); + let timer = null; + let inside = true; + let poprec = popOp[rec.type]; + el.unpop = () => { + let pos = [...el.childNodes].indexOf(poprec.div); + if (pos >= 0) { + el.removeChild(poprec.div); } }; - camStock.scale.x = csx + 0.005; - camStock.scale.y = csy + 0.005; - camStock.scale.z = csz + 0.005; - camStock.position.x = csox; - camStock.position.y = csoy; - camStock.position.z = csz / 2; - delta = csz - topZ; - } else if (camStock) { - SPACE.world.remove(camStock); - camStock = null; - delta = 0; + el.onmouseenter = (ev) => { + if (unpop) unpop(); + unpop = func.unpop = el.unpop; + inside = true; + // pointer to current rec for trace editing + poppedRec = rec; + poprec.use(rec); + hoveredOp = el; + el.appendChild(poprec.div); + // option click event appears latent + // and overides the sticky settings + setTimeout(() => { + UC.setSticky(false); + }, 0); + }; + el.onmouseleave = () => { + inside = false; + clearTimeout(timer); + timer = setTimeout(() => { + if (!inside && poprec.using(rec) && !UC.isSticky()) { + el.unpop(); + } + }, 250); + }; + el.rec = rec; + el.onmousedown = (ev) => { + func.traceDone(); + let target = ev.target, clist = target.classList; + if (!clist.contains("draggable")) { + return; + } + if (ev.ctrlKey || ev.metaKey) { + rec.disabled = !rec.disabled; + ev.preventDefault(); + ev.stopPropagation(); + if (rec.disabled) { + clist.add("disabled"); + } else { + clist.remove("disabled"); + } + return true; + } + clist.add("drag"); + ev.stopPropagation(); + ev.preventDefault(); + let tracker = UI.tracker; + tracker.style.display = 'block'; + let cancel = tracker.onmouseup = (ev) => { + oplist = current.process.ops; + clist.remove("drag"); + tracker.style.display = 'none'; + if (ev) { + ev.stopPropagation(); + ev.preventDefault(); + } + oplist.length = 0; + for (let child of listel.childNodes) { + oplist.push(child.rec); + } + API.conf.save(); + func.opRender(); + }; + tracker.onmousemove = (ev) => { + ev.stopPropagation(); + ev.preventDefault(); + if (ev.buttons === 0) { + return cancel(); + } + for (let el of bounds) { + if (el === target) continue; + let rect = el.getBoundingClientRect(); + let left = rect.left; + let right = rect.left + rect.width; + if (ev.pageX >= left && ev.pageX <= right) { + let mid = (left + right) / 2; + try { listel.removeChild(target); } catch (e) { } + el.insertAdjacentElement(ev.pageX < mid ? "beforebegin" : "afterend", target); + } + } + }; + }; } + }); - const {x, y, z} = stock; - if (x && y && z && !STACKS.getStack('bounds')) { - const render = new KIRI.Layers().setLayer('bounds', { face: 0xaaaaaa, line: 0xaaaaaa }); - const stack = STACKS.setFreeMem(false).create('bounds', SPACE.world); - stack.add(render.addPolys([ - newPolygon().centerRectangle({x:csox, y:csoy, z:0}, x, y), - newPolygon().centerRectangle({x:csox, y:csoy, z}, x, y) - ], { thin: true } )); - const hx = x/2, hy = y/2; - const sz = stock.z || 0; - stack.add(render.addLines([ - newPoint(csox + hx, csoy - hy, 0), newPoint(csox + hx, csoy - hy, sz), - newPoint(csox + hx, csoy + hy, 0), newPoint(csox + hx, csoy + hy, sz), - newPoint(csox - hx, csoy - hy, 0), newPoint(csox - hx, csoy - hy, sz), - newPoint(csox - hx, csoy + hy, 0), newPoint(csox - hx, csoy + hy, sz), - ], { thin: true })); - STACKS.setFreeMem(true); + func.opFlip = () => { + let widgets = API.widgets.all(); + let { process } = current; + let { ops, op2 } = process; + // add flip singleton to b-side + let add2 = op2.length === 0; + let axis = poppedRec.axis; + flipping = true; + process.camZAnchor = { + top: "bottom", + bottom: "top", + middle: "middle" + }[process.camZAnchor]; + // flip tabs + for (let widget of widgets) { + let anno = API.widgets.annotate(widget.id).tab || []; + let wbm = widget.bounds.max.z; + for (let tab of anno) { + let box = widget.tabs[tab.id].box; + let bpo = box.position; + let xr = 0, yr = 0; + let flz = wbm - bpo.z; + if (axis === 'X') { + tab.pos.y = -tab.pos.y; + bpo.y = -bpo.y; + xr = Math.PI / 2; + } + if (axis === 'Y') { + tab.pos.x = -tab.pos.x; + bpo.x = -bpo.x; + yr = Math.PI / 2; + } + tab.pos.z = bpo.z = flz; + let { rot } = tab; + let qat = new THREE.Quaternion(rot._x, rot._y, rot._z, rot._w); + let eul = new THREE.Euler().setFromQuaternion(qat); + eul._z = -eul._z; + tab.rot = new THREE.Quaternion().setFromEuler(eul); + } + clearTabs(widget, true); + restoreTabs([widget]); } + // flip widget + if (axis === 'X') { + API.selection.rotate(Math.PI, 0, 0); + } + if (axis === 'Y') { + API.selection.rotate(0, Math.PI, 0); + } + flipping = false; + process.ops = op2; + process.op2 = ops; + // special case, auto-adjust camZBottom + if (process.camZBottom && widgets.length === 1) { + process.camZBottom = widgets[0].bounds.max.z - process.camZBottom; + API.util.rec2ui(process); + } + for (let op of op2) { + if (op.type === 'flip') { + op.axis = poppedRec.axis; + } + } + if (add2) { + func.opAdd(poppedRec); + } else { + func.opRender(); + } + }; - API.platform.update_top_z(delta); - API.platform.update_origin(); + // TAB FUNCS + let showTab, lastTab, tab, iw, ic; + api.event.on("cam.tabs.add", func.tabAdd = () => { + func.traceDone(); + alert = api.show.alert("[esc] cancels tab editing"); + api.feature.hover = true; + func.hover = func.tabHover; + func.hoverUp = func.tabHoverUp; + }); + api.event.on("cam.tabs.done", func.tabDone = () => { + delbox('tabb'); + api.hide.alert(alert); + api.feature.hover = false; + if (lastTab) { + lastTab.box.material.color.r = 0; + lastTab = null; + } + }); + api.event.on("cam.tabs.clear", func.tabClear = () => { + func.tabDone(); + API.widgets.all().forEach(widget => { + clearTabs(widget); + widget.saveState(); + }); + API.conf.save(); + }); + func.tabHover = function(data) { + delbox('tabb'); + const { int, type, point } = data; + const object = int ? int.object : null; + const tab = int ? object.tab : null; + if (lastTab) { + lastTab.box.material.color.r = 0; + lastTab = null; + } + if (tab) { + tab.box.material.color.r = 0.5; + lastTab = tab; + return; + } + if (type !== 'widget') { + iw = null; + return; + } + let n = int.face.normal; + iw = int.object.widget; + ic = int.point; + // only near vertical faces + if (Math.abs(n.z) > 0.3) { + return; + } + showTab = createTabBox(iw, ic, n); + }; + func.tabHoverUp = function(int) { + delbox('tabb'); + if (lastTab) { + const {widget, box, id} = lastTab; + widget.adds.remove(box); + widget.mesh.remove(box); + delete widget.tabs[id]; + let ta = API.widgets.annotate(widget.id).tab; + let ix = 0; + ta.forEach((rec,i) => { + if (rec.id === id) { + ix = i; + } + }); + ta.splice(ix,1); + API.conf.save(); + widget.saveState(); + return; + } + if (!iw) return; + let ip = iw.track.pos; + let wa = api.widgets.annotate(iw.id); + let wt = (wa.tab = wa.tab || []); + let pos = { + x: showTab.pos.x - ip.x, + y: -showTab.pos.z - ip.y, + z: showTab.pos.y + ip.z, + } + let id = Date.now(); + let { dim, rot } = showTab; + let rec = { pos, dim, rot, id }; + wt.push(Object.clone(rec)); + addWidgetTab(iw, rec); + API.conf.save(); + iw.saveState(); + }; + + // TRACE FUNCS + let traceOn = false, lastTrace; + func.traceAdd = (ev) => { + func.unpop(); + func.tabDone(); + func.traceDone(); + alert = api.show.alert("analyzing parts...", 1000); + traceOn = hoveredOp; + traceOn.classList.add("editing"); + 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); + widget.trace_stack = null; + } + if (widget.trace_stack) { + widget.adds.appendAll(widget.trace_stack.meshes); + widget.trace_stack.show(); + return; + } + let areas = (poppedRec.areas[widget.id] || []); + let stack = new kiri.Stack(widget.mesh); + widget.trace_stack = stack; + widget.traces.forEach(poly => { + let match = areas.filter(arr => poly.matches(arr)); + let layers = new kiri.Layers(); + layers.setLayer("trace", {line: 0xaaaa55}, false).addPoly(poly); + stack.addLayers(layers); + stack.new_meshes.forEach(mesh => { + mesh.trace = {widget, poly}; + // ensure trace poly singleton from matches + if (match.length > 0) { + poly._trace = match[0]; + } else { + poly._trace = poly.toArray(); + } + }); + widget.adds.appendAll(stack.new_meshes); + }); + }); + // ensure appropriate traces are toggled matching current record + kiri.api.widgets.for(widget => { + let areas = (poppedRec.areas[widget.id] || []); + let stack = widget.trace_stack; + stack.meshes.forEach(mesh => { + let { poly } = mesh.trace; + let match = areas.filter(arr => poly.matches(arr)); + if (match.length > 0) { + if (!mesh.selected) { + func.traceToggle(mesh, true); + } + } else if (mesh.selected) { + func.traceToggle(mesh, true); + } + }); + }); + }, poppedRec.single); + api.feature.hover = true; + api.feature.hoverAdds = true; + func.hover = func.traceHover; + func.hoverUp = func.traceHoverUp; + }; + func.traceDone = () => { + if (!traceOn) { + return; + } + func.unpop(); + traceOn.classList.remove("editing"); + traceOn = false; + kiri.api.widgets.opacity(1); + api.hide.alert(alert); + api.feature.hover = false; + api.feature.hoverAdds = false; + kiri.api.widgets.for(widget => { + if (widget.trace_stack) { + widget.trace_stack.hide(); + widget.adds.removeAll(widget.trace_stack.meshes); + } + }); + }; + api.event.on("cam.trace.clear", func.traceClear = () => { + func.traceDone(); + API.widgets.all().forEach(widget => { + unselectTraces(widget); + }); + API.conf.save(); + }); + func.traceHover = function(data) { + if (lastTrace) { + let { color, colorSave } = lastTrace.material[0]; + color.r = colorSave.r; + color.g = colorSave.g; + color.b = colorSave.b; + lastTrace.position.z -= 0.05; + } + if (data.type === 'platform') { + lastTrace = null; + return; + } + if (!data.int.object.trace) { + return; + } + lastTrace = data.int.object; + lastTrace.position.z += 0.05; + if (lastTrace.selected) { + let event = data.event; + let target = event.target; + let { clientX, clientY } = event; + let { offsetWidth, offsetHeight } = target; + } + let material = lastTrace.material[0]; + let color = material.color; + let {r, g, b} = color; + material.colorSave = {r, g, b}; + color.r = 0; + color.g = 0; + color.b = 1; + }; + func.traceHoverUp = function(int, ev) { + if (!int) return; + let { object } = int; + func.traceToggle(object); + if (ev.metaKey || ev.ctrlKey) { + let { selected } = object; + let { widget, poly } = object.trace; + for (let add of widget.adds) { + if (add.selected !== selected && add.trace.poly.getZ() === poly.getZ()) { + func.traceToggle(add); + } + } + } + }; + func.traceToggle = function(obj, skip) { + let material = obj.material[0]; + let { color, colorSave } = material; + let { widget, poly } = obj.trace; + let areas = poppedRec.areas; + if (!areas) { + return; + } + 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 + }; + } + if (obj.selected) { + obj.position.z += 0.05; + color.r = colorSave.r = 0.9; + color.g = colorSave.g = 0; + color.b = colorSave.b = 0.1; + if (!skip) wlist.push(poly._trace); + } else { + obj.position.z -= 0.05; + color.r = colorSave.r = 0xaa/255; + color.g = colorSave.g = 0xaa/255; + color.b = colorSave.b = 0x55/255; + if (!skip) wlist.remove(poly._trace); + } + API.conf.save(); + }; + + // COMMON TAB/TRACE EVENT HANDLERS + api.event.on("slice.begin", () => { + if (isCamMode) { + func.tabDone(); + func.traceDone(); + } + }); + api.event.on("key.esc", () => { + if (isCamMode) { + func.tabDone(); + func.traceDone(); + } + }); + api.event.on("selection.scale", () => { + if (isCamMode) { + func.tabClear(); + func.traceClear(); + } + }); + api.event.on("widget.duplicate", (widget, oldwidget) => { + if (!isCamMode) { + return; + } + if (traceOn) { + func.traceDone(); + } + unselectTraces(widget); + if (flipping) { + return; + } + let ann = API.widgets.annotate(widget.id); + if (ann.tab) { + ann.tab.forEach((tab,i) => { + tab.id = Date.now() + i; + }); + restoreTabs([widget]); + } + }); + api.event.on("widget.mirror", widget => { + if (!isCamMode) { + return; + } + if (traceOn) { + func.traceDone(); + } + unselectTraces(widget); + if (flipping) { + return; + } + mirrorTabs(widget); + }); + api.event.on("widget.rotate", rot => { + if (!isCamMode) { + return; + } + let {widget, x, y, z} = rot; + if (traceOn) { + func.traceDone(); + } + unselectTraces(widget); + if (flipping) { + return; + } + if (x || y) { + clearTabs(widget); + } else { + rotateTabs(widget, x, y, z); + } + }); + api.event.on("mouse.hover.up", rec => { + if (!isCamMode) { + return; + } + let { object, event } = rec; + func.hoverUp(object, event); + }); + api.event.on("mouse.hover", data => { + if (!isCamMode) { + return; + } + func.hover(data); + }); + + function mirrorTabs(widget) { + let tabs = API.widgets.annotate(widget.id).tab || []; + tabs.forEach(rec => { + let { id, pos, rot } = rec; + let tab = widget.tabs[id]; + let e = new THREE.Euler().setFromQuaternion(rot); + e._z = Math.PI - e._z; + let { _x, _y, _z, _w } = rec.rot; + let or = new THREE.Quaternion(_x, _y, _z, _w); + let nr = new THREE.Quaternion().setFromEuler(e); + let ra = or.angleTo(nr); + console.log({or, nr, ra}); + rec.rot = nr; + // let m4 = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(0,0,e._z)); + // tab.box.geometry.applyMatrix4(m4); + tab.box.position.x = pos.x = -pos.x; + }); SPACE.update(); } -})(); + function rotateTabs(widget, x, y, z) { + let tabs = API.widgets.annotate(widget.id).tab || []; + tabs.forEach(rec => { + let { id, pos, rot } = rec; + let coff = widget.track.center; + let tab = widget.tabs[id]; + let m4 = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(x || 0, y || 0, z || 0)); + // update position vector + let vc = new THREE.Vector3(pos.x, pos.y, pos.z).applyMatrix4(m4); + // update rotation quaternion + rec.rot = new THREE.Quaternion().multiplyQuaternions( + new THREE.Quaternion(rot._x, rot._y, rot._z, rot._w), + new THREE.Quaternion().setFromRotationMatrix(m4) + ); + tab.box.geometry.applyMatrix4(m4); + tab.box.position.x = pos.x = vc.x - coff.dx; + tab.box.position.y = pos.y = vc.y - coff.dy; + tab.box.position.z = pos.z = vc.z; + }); + SPACE.update(); + } + + function hasSpindle() { + return current.device.spindleMax > 0; + } + + createPopOp('level', { + tool: 'camLevelTool', + spindle: 'camLevelSpindle', + step: 'camLevelOver', + rate: 'camLevelSpeed', + down: 'camLevelDown' + }).inputs = { + tool: UC.newSelect(LANG.cc_tool, {}, "tools"), + sep: UC.newBlank({class:"pop-sep"}), + spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), + step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,1.0)}), + rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), + down: UC.newInput(LANG.cc_loff_s, {title:LANG.cc_loff_l, convert:UC.toFloat, units:true}), + }; + + createPopOp('rough', { + tool: 'camRoughTool', + spindle: 'camRoughSpindle', + down: 'camRoughDown', + step: 'camRoughOver', + rate: 'camRoughSpeed', + plunge: 'camRoughPlunge', + leave: 'camRoughStock', + voids: 'camRoughVoid', + flats: 'camRoughFlat', + inside: 'camRoughIn', + top: 'camRoughTop' + }).inputs = { + tool: UC.newSelect(LANG.cc_tool, {}, "tools"), + sep: UC.newBlank({class:"pop-sep"}), + spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), + down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true}), + step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,1.0)}), + rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), + plunge: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), + leave: UC.newInput(LANG.cr_lsto_s, {title:LANG.cr_lsto_l, convert:UC.toFloat, units:true}), + sep: UC.newBlank({class:"pop-sep"}), + voids: UC.newBoolean(LANG.cr_clrp_s, undefined, {title:LANG.cr_clrp_l}), + flats: UC.newBoolean(LANG.cr_clrf_s, undefined, {title:LANG.cr_clrf_l}), + top: UC.newBoolean(LANG.cr_clrt_s, undefined, {title:LANG.cr_clrt_l}), + inside: UC.newBoolean(LANG.cr_olin_s, undefined, {title:LANG.cr_olin_l}) + }; + + createPopOp('outline', { + tool: 'camOutlineTool', + spindle: 'camOutlineSpindle', + step: 'camOutlineOver', + down: 'camOutlineDown', + rate: 'camOutlineSpeed', + plunge: 'camOutlinePlunge', + dogbones: 'camOutlineDogbone', + omitthru: 'camOutlineOmitThru', + outside: 'camOutlineOut', + inside: 'camOutlineIn', + wide: 'camOutlineWide' + }).inputs = { + tool: UC.newSelect(LANG.cc_tool, {}, "tools"), + sep: UC.newBlank({class:"pop-sep"}), + spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), + down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true}), + step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,1.0), show:() => popOp.outline.rec.wide}), + rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), + plunge: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), + sep: UC.newBlank({class:"pop-sep"}), + dogbones: UC.newBoolean(LANG.co_dogb_s, undefined, {title:LANG.co_dogb_l, show:(op) => { return !op.inputs.wide.checked }}), + inside: UC.newBoolean(LANG.co_olin_s, undefined, {title:LANG.co_olin_l, show:(op) => { return !op.inputs.outside.checked }}), + outside: UC.newBoolean(LANG.co_olot_s, undefined, {title:LANG.co_olot_l, show:(op) => { return !op.inputs.inside.checked }}), + omitthru: UC.newBoolean(LANG.co_omit_s, undefined, {title:LANG.co_omit_l, xshow:(op) => { return op.inputs.outside.checked }}), + wide: UC.newBoolean(LANG.co_wide_s, undefined, {title:LANG.co_wide_l, show:(op) => { return !op.inputs.inside.checked }}) + }; + + createPopOp('contour', { + tool: 'camContourTool', + spindle: 'camContourSpindle', + step: 'camContourOver', + rate: 'camContourSpeed', + angle: 'camContourAngle', + tolerance: 'camTolerance', + curves: 'camContourCurves', + inside: 'camContourIn', + axis: 'X' + }).inputs = { + tool: UC.newSelect(LANG.cc_tool, {}, "tools"), + axis: UC.newSelect(LANG.cd_axis, {}, "regaxis"), + sep: UC.newBlank({class:"pop-sep"}), + spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), + step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,1.0)}), + rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), + angle: UC.newInput(LANG.cf_angl_s, {title:LANG.cf_angl_l, convert:UC.toFloat, bound:UC.bound(45,90)}), + tolerance: UC.newInput(LANG.ou_toll_s, {title:LANG.ou_toll_l, convert:UC.toFloat, bound:UC.bound(0,10.0), units:true}), + sep: UC.newBlank({class:"pop-sep"}), + curves: UC.newBoolean(LANG.cf_curv_s, undefined, {title:LANG.cf_curv_l}), + inside: UC.newBoolean(LANG.cf_olin_s, undefined, {title:LANG.cf_olin_l}) + }; + + createPopOp('trace', { + mode: 'camTraceType', + offset: 'camTraceOffset', + spindle: 'camTraceSpindle', + tool: 'camTraceTool', + step: 'camTraceOver', + down: 'camTraceDown', + rate: 'camTraceSpeed', + plunge: 'camTracePlunge', + single: 'camTraceLines', + }).inputs = { + tool: UC.newSelect(LANG.cc_tool, {}, "tools"), + sep: UC.newBlank({class:"pop-sep"}), + 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"), + spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), + step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,1.0), show:(op) => popOp.trace.rec.mode === "clear"}), + down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true}), + rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), + plunge: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), + sep: UC.newBlank({class:"pop-sep"}), + single: UC.newBoolean(LANG.cc_sngl_s, undefined, {title:LANG.cc_sngl_l}), + select: UC.newRow([ + UC.newButton(undefined, func.traceAdd, {icon:''}), + UC.newButton(undefined, func.traceDone, {icon:''}), + ], {class:"ext-buttons f-row"}), + }; + + createPopOp('drill', { + tool: 'camDrillTool', + spindle: 'camDrillSpindle', + down: 'camDrillDown', + rate: 'camDrillDownSpeed', + dwell: 'camDrillDwell', + lift: 'camDrillLift', + mark: 'camDrillMark' + }).inputs = { + tool: UC.newSelect(LANG.cc_tool, {}, "tools"), + sep: UC.newBlank({class:"pop-sep"}), + spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), + down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true}), + rate: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), + dwell: UC.newInput(LANG.cd_dwll_s, {title:LANG.cd_dwll_l, convert:UC.toFloat}), + lift: UC.newInput(LANG.cd_lift_s, {title:LANG.cd_lift_l, convert:UC.toFloat, units:true, show:() => !poppedRec.mark}), + mark: UC.newBoolean(LANG.cd_mark_s, undefined, {title:LANG.cd_mark_l}), + }; + + createPopOp('register', { + tool: 'camDrillTool', + spindle: 'camDrillSpindle', + down: 'camDrillDown', + rate: 'camDrillDownSpeed', + dwell: 'camDrillDwell', + lift: 'camDrillLift', + feed: 'camRegisterSpeed', + }).inputs = { + tool: UC.newSelect(LANG.cc_tool, {}, "tools"), + axis: UC.newSelect(LANG.cd_axis, {}, "regaxis"), + points: UC.newSelect(LANG.cd_points, {show:() => poppedRec.axis !== '-'}, "regpoints"), + sep: UC.newBlank({class:"pop-sep"}), + spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), + feed: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true, show:() => poppedRec.axis === '-'}), + down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true}), + rate: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), + dwell: UC.newInput(LANG.cd_dwll_s, {title:LANG.cd_dwll_l, convert:UC.toFloat, show:() => poppedRec.axis !== '-'}), + lift: UC.newInput(LANG.cd_lift_s, {title:LANG.cd_lift_l, convert:UC.toFloat, units:true, show:() => poppedRec.axis !== '-'}) + }; + + createPopOp('flip', { + axis: 'camFlipAxis' + }).inputs = { + axis: UC.newSelect(LANG.cd_axis, {}, "regaxis"), + sep: UC.newBlank({class:"pop-sep"}), + action: UC.newRow([ + UC.newButton(LANG.cc_flip, func.opFlip) + ], {class:"ext-buttons f-row"}) + }; +}; + +function createPopOp(type, map) { + let op = popOp[type] = { + div: UC.newElement('div', { id:`${type}-op`, class:"cam-pop-op" }), + use: (rec) => { + op.rec = rec; + for (let [key, val] of Object.entries(op.inputs)) { + let type = val.type; + let from = map[key]; + if (rec[key] === undefined && type && from) { + let newval = current.process[from]; + rec[key] = current.process[from]; + } + } + API.util.rec2ui(rec, op.inputs); + op.hideshow(); + }, + using: (rec) => { + return op.rec === rec; + }, + bind: (ev) => { + API.util.ui2rec(op.rec, op.inputs); + for (let [key, val] of Object.entries(op.rec)) { + let saveTo = map[key]; + if (saveTo) { + current.process[saveTo] = val; + } + } + API.conf.save(); + op.hideshow(); + }, + new: () => { + let rec = { type }; + for (let [key, val] of Object.entries(map)) { + rec[key] = current.process[val]; + } + return rec; + }, + hideshow: () => { + for (let inp of Object.values(op.inputs)) { + let parent = inp.parentElement; + if (parent.setVisible && parent.__opt.show) { + parent.setVisible(parent.__opt.show(op)); + } + } + }, + group: [] + }; + UC.restore({ + addTo: op.div, + bindTo: op.bind, + lastDiv: op.div, + lastGroup: op.group + }); + return op; +} + +function createTabBox(iw, ic, n) { + const { track } = iw; + const { stock, bounds, process } = API.conf.get(); + const { camTabsWidth, camTabsHeight, camTabsDepth, camTabsMidline } = process; + const sz = stock.z || bounds.max.z; + const zto = sz - iw.track.top; + const zp = sz - track.box.d + process.camZBottom - zto + (camTabsMidline ? 0 : camTabsHeight / 2); + ic.x += n.x * camTabsDepth / 2; // offset from part + ic.z -= n.y * camTabsDepth / 2; // offset swap z,y + ic.y = zp; // offset swap in world space y,z + const rot = new THREE.Quaternion().setFromAxisAngle(zaxis, Math.atan2(n.y, n.x)); + const pos = { x:ic.x, y:ic.y, z:ic.z }; + const dim = { x:camTabsDepth, y:camTabsWidth, z:camTabsHeight }; + const tab = addbox(pos, 0x0000dd, 'tabb', dim, { rotate: rot }); + return { pos, dim, rot, tab, width: camTabsWidth, height: camTabsHeight }; +} + +function addWidgetTab(widget, rec) { + const { pos, dim, rot, id } = rec; + const tabs = widget.tabs = (widget.tabs || {}); + // prevent duplicate restore from repeated settings load calls + if (!tabs[id]) { + pos.box = addbox( + pos, 0x0000dd, id, + dim, { group: widget.mesh, rotate: rot } + ); + pos.box.tab = Object.assign({widget, id}, pos); + widget.adds.push(pos.box); + tabs[id] = pos; + } +} + +function restoreTabs(widgets) { + widgets.forEach(widget => { + const tabs = API.widgets.annotate(widget.id).tab || []; + tabs.forEach(rec => { + rec = Object.clone(rec); + rec.rot = new THREE.Quaternion(rec.rot._x ,rec.rot._y, rec.rot._z, rec.rot._w); + addWidgetTab(widget, rec); + }); + }); +} + +function clearTabs(widget, skiprec) { + Object.values(widget.tabs || {}).forEach(rec => { + widget.adds.remove(rec.box); + widget.mesh.remove(rec.box); + }); + widget.tabs = {}; + if (!skiprec) { + delete API.widgets.annotate(widget.id).tab; + } +} + +function unselectTraces(widget, skip) { + if (widget.trace_stack) { + widget.trace_stack.meshes.forEach(mesh => { + if (mesh.selected) { + func.traceToggle(mesh, skip); + } + }); + } +} + +function addbox() { return FDM.addbox(...arguments)}; +function delbox() { return FDM.delbox(...arguments)}; + +function animate() { + isAnimate = true; + API.widgets.opacity(isParsed ? 0 : 0.75); + API.hide.slider(); + STACKS.clear(); + CAM.animate(API); +} + +function updateStock(args, event) { + if (isAnimate) { + return; + } + + STACKS.remove('bounds'); + if (isCamMode && isArrange) { + STACKS.clear(); + } + + if (!isCamMode) { + if (camStock) { + SPACE.world.remove(camStock); + camStock = null; + } + return; + } + + const refresh = (event === "selection.scale" || event === 'selection.rotate'); + + let settings = API.conf.get(); + let widgets = API.widgets.all(); + let proc = settings.process; + let enabled = UI.camStockOn.checked; + let offset = UI.camStockOffset.checked; + let stockSet = offset || (proc.camStockX && proc.camStockY && proc.camStockZ > 0); + let topZ = API.platform.top_z(); + let delta = 0; + let csox = 0; + let csoy = 0; + let stock = settings.stock = { }; + let compute = enabled && stockSet && (!offset || widgets.length); + + UI.func.animate.classList.add('disabled'); + + // create/inject cam stock if stock size other than default + if (compute) { + UI.func.animate.classList.remove('disabled'); + let csx = proc.camStockX; + let csy = proc.camStockY; + let csz = proc.camStockZ; + if (offset) { + let min = { x: Infinity, y: Infinity, z: 0 }; + let max = { x: -Infinity, y: -Infinity, z: -Infinity }; + widgets.forEach(function(widget) { + let wbnd = widget.getBoundingBox(refresh); + let wpos = widget.track.pos; + min = { + x: Math.min(min.x, wpos.x + wbnd.min.x), + y: Math.min(min.y, wpos.y + wbnd.min.y), + z: 0 + }; + max = { + x: Math.max(max.x, wpos.x + wbnd.max.x), + y: Math.max(max.y, wpos.y + wbnd.max.y), + z: Math.max(max.z, wbnd.max.z) + }; + }); + csx += max.x - min.x; + csy += max.y - min.y; + csz += max.z - min.z; + csox = min.x + ((max.x - min.x) / 2); + csoy = min.y + ((max.y - min.y) / 2); + } + if (!camStock) { + let geo = new THREE.BoxGeometry(1, 1, 1); + let mat = new THREE.MeshBasicMaterial({ + color: 0x777777, + opacity: 0.2, + transparent: true, + side:THREE.DoubleSide + }); + camStock = new THREE.Mesh(geo, mat); + camStock.renderOrder = 2; + SPACE.world.add(camStock); + } + stock = settings.stock = { + x: csx, + y: csy, + z: csz, + center: { + x: csox, + y: csoy, + z: csz / 2 + } + }; + camStock.scale.x = csx + 0.005; + camStock.scale.y = csy + 0.005; + camStock.scale.z = csz + 0.005; + camStock.position.x = csox; + camStock.position.y = csoy; + camStock.position.z = csz / 2; + delta = csz - topZ; + } else if (camStock) { + SPACE.world.remove(camStock); + camStock = null; + delta = 0; + } + + const {x, y, z} = stock; + if (x && y && z && !STACKS.getStack('bounds')) { + const render = new kiri.Layers().setLayer('bounds', { face: 0xaaaaaa, line: 0xaaaaaa }); + const stack = STACKS.setFreeMem(false).create('bounds', SPACE.world); + stack.add(render.addPolys([ + newPolygon().centerRectangle({x:csox, y:csoy, z:0}, x, y), + newPolygon().centerRectangle({x:csox, y:csoy, z}, x, y) + ], { thin: true } )); + const hx = x/2, hy = y/2; + const sz = stock.z || 0; + stack.add(render.addLines([ + newPoint(csox + hx, csoy - hy, 0), newPoint(csox + hx, csoy - hy, sz), + newPoint(csox + hx, csoy + hy, 0), newPoint(csox + hx, csoy + hy, sz), + newPoint(csox - hx, csoy - hy, 0), newPoint(csox - hx, csoy - hy, sz), + newPoint(csox - hx, csoy + hy, 0), newPoint(csox - hx, csoy + hy, sz), + ], { thin: true })); + STACKS.setFreeMem(true); + } + + API.platform.update_top_z(delta); + API.platform.update_origin(); + SPACE.update(); +} + +}); diff --git a/src/kiri-mode/cam/driver.js b/src/kiri-mode/cam/driver.js index 12b24737..6ece3f7a 100644 --- a/src/kiri-mode/cam/driver.js +++ b/src/kiri-mode/cam/driver.js @@ -2,54 +2,55 @@ "use strict"; -(function() { +// dep: main.kiri +// use: kiri.api +// use: kiri.widgets +gapp.register("kiri-mode.cam.driver", [], (root, exports) => { - const KIRI = self.kiri, - CAM = KIRI.driver.CAM = { - // init, // src/mode/cam/client.js - // slice, // src/mode/cam/slice.js - // prepare, // src/mode/cam/prepare.js - // export // src/mode/cam/export.js - }, - CPRO = CAM.process = { - LEVEL: 1, - ROUGH: 2, - OUTLINE: 3, - CONTOUR_X: 4, - CONTOUR_Y: 5, - TRACE: 6, - DRILL: 7 - }; +const { kiri } = root; +const { driver } = kiri; - // defer loading until KIRI.client and KIRI.worker exist - KIRI.load(function(API) { +const CAM = driver.CAM = {}; - if (KIRI.client) - CAM.traces = function(ondone, single) { - KIRI.client.sync(); - let settings = API.conf.get(); - let widgets = API.widgets.map(); - KIRI.client.send("cam_traces", { settings, single }, output => { - let ids = []; - KIRI.codec.decode(output).forEach(rec => { - ids.push(rec.id); - widgets[rec.id].traces = rec.traces; - }); - ondone(ids); - }); - }; +CAM.process = { + LEVEL: 1, + ROUGH: 2, + OUTLINE: 3, + CONTOUR_X: 4, + CONTOUR_Y: 5, + TRACE: 6, + DRILL: 7 +}; - if (KIRI.worker) - KIRI.worker.cam_traces = function(data, send) { - const { settings, single } = data; - const widgets = Object.values(wcache); - const fresh = widgets.filter(widget => CAM.traces(settings, widget, single)); - send.done(KIRI.codec.encode(fresh.map(widget => { return { - id: widget.id, - traces: widget.traces, - } } ))); - }; +// defer loading until kiri.client and kiri.worker exist +kiri.load(api => { + if (kiri.client) + CAM.traces = function(ondone, single) { + kiri.client.sync(); + const settings = api.conf.get(); + const widgets = api.widgets.map(); + kiri.client.send("cam_traces", { settings, single }, output => { + const ids = []; + kiri.codec.decode(output).forEach(rec => { + ids.push(rec.id); + widgets[rec.id].traces = rec.traces; + }); + ondone(ids); }); + }; -})(); + if (kiri.worker) + kiri.worker.cam_traces = function(data, send) { + const { settings, single } = data; + const widgets = Object.values(wcache); + const fresh = widgets.filter(widget => CAM.traces(settings, widget, single)); + send.done(kiri.codec.encode(fresh.map(widget => { return { + id: widget.id, + traces: widget.traces, + } } ))); + }; + +}); + +}); diff --git a/src/kiri-mode/cam/export.js b/src/kiri-mode/cam/export.js index 1eac7673..702eafe6 100644 --- a/src/kiri-mode/cam/export.js +++ b/src/kiri-mode/cam/export.js @@ -2,333 +2,335 @@ "use strict"; -(function() { +// dep: geo.base +// dep: geo.polygons +gapp.register("kiri-mode.cam.export", [], (root, exports) => { - let KIRI = self.kiri, - BASE = self.base, - POLY = BASE.polygons, - UTIL = BASE.util, - CAM = KIRI.driver.CAM, - PRO = CAM.process; +const { base, kiri } = root; +const { polygons, util } = base; +const { driver } = kiri; +const { CAM } = driver; - /** - * @returns {Array} gcode lines - */ - CAM.export = function(print, online) { - let widget = print.widgets[0]; - if (!widget) return; +const POLY = polygons; +const PRO = CAM.process; - let i, - time = 0, - lines = 0, - bytes = 0, - opnum = 0, - factor = 1, - output = [], - spindle = 0, - settings = print.settings, - device = settings.device, - gcodes = settings.device || {}, - origin = settings.origin || { x: 0, y: 0, z: 0 }, - tools = settings.tools, - space = gcodes.gcodeSpace ? ' ' : '', - isRML = device.gcodeFExt.toLowerCase() === 'rml', - stripComments = gcodes.gcodeStrip || false, - cmdToolChange = gcodes.gcodeChange || [ "M6 T{tool}" ], - cmdSpindle = gcodes.gcodeSpindle || [ "M3 S{speed}" ], - cmdDwell = gcodes.gcodeDwell || [ "G4 P{time}" ], - dev = settings.device, - spro = settings.process, - maxZd = spro.camFastFeedZ, - maxXYd = spro.camFastFeed, - decimals = BASE.config.gcode_decimals || 4, - pos = { x:null, y:null, z:null, f:null, t:null }, - line, - cidx, - mode, - point, - points = 0, - stock = settings.stock || { }, - hasStock = spro.camStockOn && stock.x && stock.y && stock.z, - ztOff = hasStock ? stock.z - widget.track.top : 0, - bounds = widget.getBoundingBox(), - zmax = hasStock ? stock.z : bounds.max.z, - runbox = { - max: { x:-Infinity, y:-Infinity, z:-Infinity}, - min: { x:Infinity, y:Infinity, z:Infinity} - }, - offset = { - x: -origin.x, - y: origin.y - }, - consts = { - tool: 0, - tool_name: "unknown", - top: (offset ? dev.bedDepth : dev.bedDepth/2), - left: (offset ? 0 : -dev.bedWidth/2), - right: (offset ? dev.bedWidth : dev.bedWidth/2), - bottom: (offset ? 0 : -dev.bedDepth/2), - time_sec: 0, - time_ms: 0, - time: 0 - } +/** + * @returns {Array} gcode lines + */ +CAM.export = function(print, online) { + const widget = print.widgets[0]; + if (!widget) return; - function section(section) { - append(); - online({ section }); + const { settings } = print; + const { device, tools } = settings; + + let i, + time = 0, + lines = 0, + bytes = 0, + opnum = 0, + factor = 1, + output = [], + spindle = 0, + origin = settings.origin || { x: 0, y: 0, z: 0 }, + space = device.gcodeSpace ? ' ' : '', + isRML = device.gcodeFExt.toLowerCase() === 'rml', + stripComments = device.gcodeStrip || false, + cmdToolChange = device.gcodeChange || [ "M6 T{tool}" ], + cmdSpindle = device.gcodeSpindle || [ "M3 S{speed}" ], + cmdDwell = device.gcodeDwell || [ "G4 P{time}" ], + dev = settings.device, + spro = settings.process, + maxZd = spro.camFastFeedZ, + maxXYd = spro.camFastFeed, + decimals = base.config.gcode_decimals || 4, + pos = { x:null, y:null, z:null, f:null, t:null }, + line, + cidx, + mode, + point, + points = 0, + stock = settings.stock || { }, + hasStock = spro.camStockOn && stock.x && stock.y && stock.z, + ztOff = hasStock ? stock.z - widget.track.top : 0, + bounds = widget.getBoundingBox(), + zmax = hasStock ? stock.z : bounds.max.z, + runbox = { + max: { x:-Infinity, y:-Infinity, z:-Infinity}, + min: { x:Infinity, y:Infinity, z:Infinity} + }, + offset = { + x: -origin.x, + y: origin.y + }, + consts = { + tool: 0, + tool_name: "unknown", + top: (offset ? dev.bedDepth : dev.bedDepth/2), + left: (offset ? 0 : -dev.bedWidth/2), + right: (offset ? dev.bedWidth : dev.bedWidth/2), + bottom: (offset ? 0 : -dev.bedDepth/2), + time_sec: 0, + time_ms: 0, + time: 0 } - function append(line) { - if (line) { - lines++; - bytes += line.length; - output.append(line); - } - if (!line || output.length > 1000) { - online(output.join("\r\n")); - output = []; - } - } - - function filterEmit(array, consts) { - if (!array) return; - for (i=0; i= 0) { - line = line.substring(0, cidx).trim(); - if (line.length === 0) continue; - } - if (line.indexOf('G20') === 0) { - factor = 1/25.4; - consts.top = (offset ? dev.bedDepth : dev.bedDepth/2) * factor; - consts.left = (offset ? 0 : -dev.bedWidth/2) * factor; - consts.right = (offset ? dev.bedWidth : dev.bedWidth/2) * factor; - consts.bottom = (offset ? 0 : -dev.bedDepth/2) * factor; - } else if (line.indexOf('G21') === 0) { - factor = 1; - } - append(line); - } - } - - function add0(val, opt) { - let s = val.toString(), - d = s.indexOf("."); - if (d < 0) { - return opt ? s : s + '.0'; - } else { - return val.toFixed(decimals); - } - } - - function toolByNumber(number) { - for (let i=0; i { - let tool = toolz[tn]; - append(`; tool=${tn} flute=${tool.flute_diam} len=${tool.flute_len} metric=${tool.metric}`); - }); - } - - // emit gcode preamble - filterEmit(gcodes.gcodePre, consts); - - // emit all points in layer/point order - print.output.forEach(function (layerout) { - if (mode !== layerout.mode) { - if (mode && !stripComments) append("; ending " + mode + " op after " + Math.round(time/60) + " seconds"); - mode = layerout.mode; - if (mode) section(`op-${opnum++}-${mode}`); - if (!stripComments) append("; starting " + mode + " op"); - } - if (layerout.spindle && layerout.spindle !== spindle) { - spindle = layerout.spindle; - if (spindle > 0) { - let speed = Math.abs(spindle); - filterEmit(cmdSpindle, { - speed, spindle: speed, rpm: speed - }); - } else { - append("M4"); - } - // append((spindle > 0 ? "M3" : "M4") + " S" + Math.abs(spindle)); - } - layerout.forEach(function(out) { - moveTo(out); - }); - }); - if (mode && !stripComments) append("; ending " + mode + " op after " + Math.round(time/60) + " seconds"); - - // emit gcode post - section('footer'); - filterEmit(gcodes.gcodePost, consts); - - // flush buffered gcode + function section(section) { append(); + online({ section }); + } - print.time = time; - print.lines = lines; - print.bytes = bytes + lines - 1; - print.bounds = runbox; - }; + function append(line) { + if (line) { + lines++; + bytes += line.length; + output.append(line); + } + if (!line || output.length > 1000) { + online(output.join("\r\n")); + output = []; + } + } -})(); + function filterEmit(array, consts) { + if (!array) return; + for (i=0; i= 0) { + line = line.substring(0, cidx).trim(); + if (line.length === 0) continue; + } + if (line.indexOf('G20') === 0) { + factor = 1/25.4; + consts.top = (offset ? dev.bedDepth : dev.bedDepth/2) * factor; + consts.left = (offset ? 0 : -dev.bedWidth/2) * factor; + consts.right = (offset ? dev.bedWidth : dev.bedWidth/2) * factor; + consts.bottom = (offset ? 0 : -dev.bedDepth/2) * factor; + } else if (line.indexOf('G21') === 0) { + factor = 1; + } + append(line); + } + } + + function add0(val, opt) { + let s = val.toString(), + d = s.indexOf("."); + if (d < 0) { + return opt ? s : s + '.0'; + } else { + return val.toFixed(decimals); + } + } + + function toolByNumber(number) { + for (let i=0; i { + let tool = toolz[tn]; + append(`; tool=${tn} flute=${tool.flute_diam} len=${tool.flute_len} metric=${tool.metric}`); + }); + } + + // emit gcode preamble + filterEmit(device.gcodePre, consts); + + // emit all points in layer/point order + print.output.forEach(function (layerout) { + if (mode !== layerout.mode) { + if (mode && !stripComments) append("; ending " + mode + " op after " + Math.round(time/60) + " seconds"); + mode = layerout.mode; + if (mode) section(`op-${opnum++}-${mode}`); + if (!stripComments) append("; starting " + mode + " op"); + } + if (layerout.spindle && layerout.spindle !== spindle) { + spindle = layerout.spindle; + if (spindle > 0) { + let speed = Math.abs(spindle); + filterEmit(cmdSpindle, { + speed, spindle: speed, rpm: speed + }); + } else { + append("M4"); + } + // append((spindle > 0 ? "M3" : "M4") + " S" + Math.abs(spindle)); + } + layerout.forEach(function(out) { + moveTo(out); + }); + }); + if (mode && !stripComments) append("; ending " + mode + " op after " + Math.round(time/60) + " seconds"); + + // emit gcode post + section('footer'); + filterEmit(device.gcodePost, consts); + + // flush buffered gcode + append(); + + print.time = time; + print.lines = lines; + print.bytes = bytes + lines - 1; + print.bounds = runbox; +}; + +}); diff --git a/src/kiri-mode/cam/ops.js b/src/kiri-mode/cam/ops.js index 2cd71fc2..97cc7595 100644 --- a/src/kiri-mode/cam/ops.js +++ b/src/kiri-mode/cam/ops.js @@ -2,1346 +2,1348 @@ "use strict"; -(function() { +// dep: geo.base +// dep: geo.paths +// dep: geo.point +// dep: geo.polygons +// dep: kiri.slice +gapp.register("kiri-mode.cam.ops", [], (root, exports) => { - let KIRI = self.kiri, - BASE = self.base, - UTIL = BASE.util, - CAM = KIRI.driver.CAM, - PRO = CAM.process, - POLY = BASE.polygons, - newSlice = KIRI.newSlice, - newPoint = BASE.newPoint, - newPolygon = BASE.newPolygon; +const { base, kiri } = root; +const { paths, polygons, newPoint, newPolygon } = base; +const { poly2polyEmit, tip2tipEmit } = paths; +const { driver, newSlice } = kiri; +const { CAM } = driver; - class CamOp { - constructor(state, op) { - this.state = state; - this.op = op - } +const POLY = polygons; - type() { - return this.op.type; - } - - weight() { - return 1; - } - - slice() { } - - prepare() { } +class CamOp { + constructor(state, op) { + this.state = state; + this.op = op } - class OpLevel extends CamOp { - constructor(state, op) { - super(state, op); - } - - slice(progress) { - let { op, state } = this; - let { settings, widget, sliceAll } = state; - let { updateToolDiams, thruHoles, tabs, cutTabs } = state; - let { bounds, zMax, ztOff, color } = state; - let { stock } = settings; - - let toolDiam = new CAM.Tool(settings, op.tool).fluteDiameter(); - let stepOver = toolDiam * op.step; - updateToolDiams(toolDiam); - - let path = newPolygon().setOpen(), - center = {x:0, y:0, z:0}, - x1 = bounds.min.x, - y1 = bounds.min.y, - x2 = bounds.max.x, - y2 = bounds.max.y, - z = bounds.max.z - (op.down || 0); - - if (stock.x && stock.y && stock.z) { - x1 = -stock.x / 2; - y1 = -stock.y / 2; - x2 = -x1; - y2 = -y1; - z = zMax + ztOff - (op.down || 0); - } - - let ei = 0, - xd = x2 - x1, - xi = xd / Math.floor(xd / stepOver); - - for (let x = x1, lx = x, ei = 0; x <= x2; x += xi, ei++, lx = x) { - if (ei % 2 === 0) { - path.add(x,y1,z).add(x,y2,z); - } else { - path.add(x,y2,z).add(x,y1,z); - } - } - - let slice = newSlice(z); - this.lines = slice.camLines = [ path ]; - slice.output() - .setLayer("level", {face: color, line: color}) - .addPolys(this.lines); - sliceAll.push(slice); - } - - prepare(ops, progress) { - let { op, state, lines } = this; - let { setTool, setSpindle } = ops; - let { polyEmit, newLayer } = ops; - - setTool(op.tool, op.rate); - setSpindle(op.spindle); - polyEmit(lines[0]); - newLayer(); - } + type() { + return this.op.type; } - class OpRough extends CamOp { - constructor(state, op) { - super(state, op); + weight() { + return 1; + } + + slice() { } + + prepare() { } +} + +class OpLevel extends CamOp { + constructor(state, op) { + super(state, op); + } + + slice(progress) { + let { op, state } = this; + let { settings, widget, sliceAll } = state; + let { updateToolDiams, thruHoles, tabs, cutTabs } = state; + let { bounds, zMax, ztOff, color } = state; + let { stock } = settings; + + let toolDiam = new CAM.Tool(settings, op.tool).fluteDiameter(); + let stepOver = toolDiam * op.step; + updateToolDiams(toolDiam); + + let path = newPolygon().setOpen(), + center = {x:0, y:0, z:0}, + x1 = bounds.min.x, + y1 = bounds.min.y, + x2 = bounds.max.x, + y2 = bounds.max.y, + z = bounds.max.z - (op.down || 0); + + if (stock.x && stock.y && stock.z) { + x1 = -stock.x / 2; + y1 = -stock.y / 2; + x2 = -x1; + y2 = -y1; + z = zMax + ztOff - (op.down || 0); } - slice(progress) { - let { op, state } = this; - let { settings, widget, slicer, sliceAll, unsafe, color } = state; - let { updateToolDiams, thruHoles, tabs, cutTabs, cutPolys } = state; - let { tshadow, shadowTop, ztOff, zBottom, zMax } = state; - let { process, stock } = settings; + let ei = 0, + xd = x2 - x1, + xi = xd / Math.floor(xd / stepOver); - if (op.down <= 0) { - throw `invalid step down "${op.down}"`; - } - - let roughIn = op.inside; - let roughTop = op.top; - let roughDown = op.down; - let roughLeave = op.leave; - let toolDiam = new CAM.Tool(settings, op.tool).fluteDiameter(); - let trueShadow = process.camTrueShadow === true; - - // create facing slices - if (roughTop) { - let shadow = tshadow.clone(); - let step = toolDiam * op.step; - let inset = POLY.offset(shadow, roughIn ? step : step + roughLeave + toolDiam / 2); - let facing = POLY.offset(inset, -step, { count: 999, flat: true }); - let zdiv = ztOff / roughDown; - let zstep = (zdiv % 1 > 0) ? ztOff / (Math.floor(zdiv) + 1) : roughDown; - if (ztOff === 0) { - // compensate for lack of z top offset in this scenario - ztOff = zstep; - } - let zsteps = Math.round(ztOff / zstep); - let camFaces = this.camFaces = []; - let zstart = zMax + ztOff - zstep; - for (let z = zstart; zsteps > 0; zsteps--) { - let slice = shadowTop.slice.clone(false); - slice.z = z; - slice.camLines = POLY.setZ(facing.clone(true), slice.z); - slice.output() - .setLayer("face", {face: color, line: color}) - .addPolys(slice.camLines); - sliceAll.push(slice); - camFaces.push(slice); - z -= zstep; - } - } - - // create roughing slices - updateToolDiams(toolDiam); - - let flats = []; - let shadow = []; - let slices = []; - let indices = slicer.interval(roughDown, { - down: true, min: zBottom, fit: true, off: 0.01 - }); - // shift out first (top-most) slice - indices.shift(); - if (op.flats) { - let flatArea = (Math.PI * (toolDiam/2) * (toolDiam/2)) / 2; - let flats = Object.entries(slicer.zFlat) - .filter(row => row[1] > flatArea) - .map(row => row[0]) - .map(v => parseFloat(v).round(5)) - .filter(v => v >= zBottom); - flats.forEach(v => { - if (!indices.contains(v)) { - indices.push(v); - } - }); - indices = indices.sort((a,b) => { return b - a }); - // if layer is not on a flat and next one is, - // then move this layer up to mid-point to previous layer - // this is not perfect. the best method is to interpolate - // between flats so that each step is < step down. on todo list - for (let i=1; i (parseFloat(v) - 0.01).round(5)) - .filter(v => v > 0 && indices.indexOf(v) < 0); - indices = indices.appendAll(flats).sort((a,b) => b-a); + path.add(x,y2,z).add(x,y1,z); } + } - // console.log('indices', ...indices, {zBottom}); - slicer.slice(indices, { each: (data, index, total) => { - shadow = unsafe ? data.tops : POLY.union(shadow.slice().appendAll(data.tops), 0.01, true); - if (flats.indexOf(data.z) >= 0) { - // exclude flats injected to complete shadow - return; - } - data.shadow = trueShadow ? CAM.shadowAt(widget, data.z) : shadow.clone(true); - data.slice.shadow = data.shadow; - // data.slice.tops[0].inner = data.shadow; - // data.slice.tops[0].inner = POLY.setZ(tshadow.clone(true), data.z); - slices.push(data.slice); - progress((index / total) * 0.5); - }, genso: true }); + let slice = newSlice(z); + this.lines = slice.camLines = [ path ]; + slice.output() + .setLayer("level", {face: color, line: color}) + .addPolys(this.lines); + sliceAll.push(slice); + } - if (trueShadow) { - shadow = tshadow.clone(true); - } else { - shadow = POLY.union(shadow.appendAll(shadowTop.tops), 0.01, true); + prepare(ops, progress) { + let { op, state, lines } = this; + let { setTool, setSpindle } = ops; + let { polyEmit, newLayer } = ops; + + setTool(op.tool, op.rate); + setSpindle(op.spindle); + polyEmit(lines[0]); + newLayer(); + } +} + +class OpRough extends CamOp { + constructor(state, op) { + super(state, op); + } + + slice(progress) { + let { op, state } = this; + let { settings, widget, slicer, sliceAll, unsafe, color } = state; + let { updateToolDiams, thruHoles, tabs, cutTabs, cutPolys } = state; + let { tshadow, shadowTop, ztOff, zBottom, zMax } = state; + let { process, stock } = settings; + + if (op.down <= 0) { + throw `invalid step down "${op.down}"`; + } + + let roughIn = op.inside; + let roughTop = op.top; + let roughDown = op.down; + let roughLeave = op.leave; + let toolDiam = new CAM.Tool(settings, op.tool).fluteDiameter(); + let trueShadow = process.camTrueShadow === true; + + // create facing slices + if (roughTop) { + let shadow = tshadow.clone(); + let step = toolDiam * op.step; + let inset = POLY.offset(shadow, roughIn ? step : step + roughLeave + toolDiam / 2); + let facing = POLY.offset(inset, -step, { count: 999, flat: true }); + let zdiv = ztOff / roughDown; + let zstep = (zdiv % 1 > 0) ? ztOff / (Math.floor(zdiv) + 1) : roughDown; + if (ztOff === 0) { + // compensate for lack of z top offset in this scenario + ztOff = zstep; } - - // inset or eliminate thru holes from shadow - shadow = POLY.flatten(shadow.clone(true), [], true); - thruHoles.forEach(hole => { - shadow = shadow.map(p => { - if (p.isEquivalent(hole)) { - // eliminate thru holes when roughing voids enabled - if (op.voids) { - return undefined; - } - let po = POLY.offset([p], -(toolDiam / 2 + roughLeave + 0.01)); - return po ? po[0] : undefined; - } else { - return p; - } - }).filter(p => p); - }); - shadow = POLY.nest(shadow); - - // shell = shadow expanded by half tool diameter + leave stock - const sadd = roughIn ? toolDiam / 2 : toolDiam / 2; - const shell = POLY.offset(shadow, sadd + roughLeave); - - slices.forEach((slice, index) => { - let offset = [shell.clone(true),slice.shadow.clone(true)].flat(); - let flat = POLY.flatten(offset, [], true); - let nest = POLY.setZ(POLY.nest(flat), slice.z); - - // inset offset array by 1/2 diameter then by tool overlap % - offset = POLY.offset(nest, [-(toolDiam / 2 + roughLeave), -toolDiam * op.step], { - minArea: 0.1, - z: slice.z, - count: 999, - flat: true, - call: (polys, count, depth) => { - // used in depth-first path creation - polys.forEach(p => { - p.depth = depth; - if (p.inner) { - p.inner.forEach(p => p.depth = depth); - } - }); - } - }) || []; - - // add outside pass if not inside only - if (!roughIn) { - const outside = POLY.offset(shadow.clone(), toolDiam / 2 + roughLeave, {z: slice.z}); - if (outside) { - outside.forEach(p => p.depth = -p.depth); - offset.appendAll(outside); - } - } - - if (tabs) { - tabs.forEach(tab => { - tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); - }); - offset = cutTabs(tabs, offset, slice.z); - } - - if (!offset) return; - - if (process.camStockClipTo && stock.x && stock.y && stock.center) { - let rect = newPolygon().centerRectangle(stock.center, stock.x, stock.y); - offset = cutPolys([rect], offset, slice.z, true); - } - - // elimate double inset on inners - offset.forEach(op => { - if (op.inner) { - let pv1 = op.perimeter(); - let newinner = []; - op.inner.forEach(oi => { - let pv2 = oi.perimeter(); - let pct = pv1 > pv2 ? pv2/pv1 : pv1/pv2; - if (pct < 0.98) { - newinner.push(oi); - } - }); - op.inner = newinner; - } - }); - - slice.camLines = offset; - if (false) slice.output() - .setLayer("slice", {line: 0xaaaa00}, true) - .addPolys(slice.topPolys()) - // .setLayer("top shadow", {line: 0x0000aa}) - // .addPolys(tshadow) - // .setLayer("rough shadow", {line: 0x00aa00}) - // .addPolys(shadow) - // .setLayer("rough shell", {line: 0xaa0000}) - // .addPolys(shell); + let zsteps = Math.round(ztOff / zstep); + let camFaces = this.camFaces = []; + let zstart = zMax + ztOff - zstep; + for (let z = zstart; zsteps > 0; zsteps--) { + let slice = shadowTop.slice.clone(false); + slice.z = z; + slice.camLines = POLY.setZ(facing.clone(true), slice.z); slice.output() - .setLayer("roughing", {face: color, line: color}) - .addPolys(offset); - progress(0.5 + (index / slices.length) * 0.5); + .setLayer("face", {face: color, line: color}) + .addPolys(slice.camLines); + sliceAll.push(slice); + camFaces.push(slice); + z -= zstep; + } + } + + // create roughing slices + updateToolDiams(toolDiam); + + let flats = []; + let shadow = []; + let slices = []; + let indices = slicer.interval(roughDown, { + down: true, min: zBottom, fit: true, off: 0.01 + }); + // shift out first (top-most) slice + indices.shift(); + if (op.flats) { + let flatArea = (Math.PI * (toolDiam/2) * (toolDiam/2)) / 2; + let flats = Object.entries(slicer.zFlat) + .filter(row => row[1] > flatArea) + .map(row => row[0]) + .map(v => parseFloat(v).round(5)) + .filter(v => v >= zBottom); + flats.forEach(v => { + if (!indices.contains(v)) { + indices.push(v); + } }); - - this.sliceOut = slices.filter(slice => slice.camLines); - sliceAll.appendAll(this.sliceOut); - } - - prepare(ops, progress) { - let { op, state, sliceOut, camFaces } = this; - let { setTool, setSpindle, setPrintPoint } = ops; - let { polyEmit, poly2polyEmit, depthRoughPath } = ops; - let { camOut, newLayer, printPoint } = ops; - let { settings, widget } = state; - let { process, controller } = settings; - - let danger = controller.danger; - let easeDown = process.camEaseDown; - let cutdir = process.camConventional; - let depthFirst = process.camDepthFirst; - let depthData = []; - - setTool(op.tool, op.rate, op.plunge); - setSpindle(op.spindle); - - for (let slice of (camFaces || [])) { - const level = []; - for (let poly of slice.camLines) { - level.push(poly); - if (poly.inner) { - poly.inner.forEach(function(inner) { - level.push(inner); - }); - } + indices = indices.sort((a,b) => { return b - a }); + // if layer is not on a flat and next one is, + // then move this layer up to mid-point to previous layer + // this is not perfect. the best method is to interpolate + // between flats so that each step is < step down. on todo list + for (let i=1; i a.filter(p => !isNeg(p.depth))); - let itops = ins.map(level => { - return POLY.nest(level.filter(poly => poly.depth === 0).clone()); - }); - let outs = depthData.map(a => a.filter(p => isNeg(p.depth))); - let otops = outs.map(level => { - return POLY.nest(level.filter(poly => poly.depth === 0).clone()); - }); - printPoint = depthRoughPath(printPoint, 0, ins, itops, polyEmit, false, ease); - printPoint = depthRoughPath(printPoint, 0, outs, otops, polyEmit, false, ease); - } - - setPrintPoint(printPoint); - } - } - - class OpOutline extends CamOp { - constructor(state, op) { - super(state, op); - } - - slice(progress) { - let { op, state } = this; - let { settings, widget, slicer, sliceAll, tshadow, thruHoles, unsafe, color } = state; - let { updateToolDiams, zThru, zBottom, shadowTop, tabs, cutTabs, cutPolys } = state; - let { process, stock } = settings; - - if (op.down <= 0) { - throw `invalid step down "${op.down}"`; - } - - let toolDiam = this.toolDiam = new CAM.Tool(settings, op.tool).fluteDiameter(); - updateToolDiams(toolDiam); - - let shadow = []; - let slices = []; - let indices = slicer.interval(op.down, { down: true, min: zBottom, fit: true, off: 0.01 }); - let trueShadow = process.camTrueShadow === true; - // shift out first (top-most) slice - indices.shift(); + } else { // add flats to shadow - const flats = Object.keys(slicer.zFlat) + flats = Object.keys(slicer.zFlat) .map(v => (parseFloat(v) - 0.01).round(5)) .filter(v => v > 0 && indices.indexOf(v) < 0); indices = indices.appendAll(flats).sort((a,b) => b-a); - // console.log('indices', ...indices, {zBottom, slicer}); - if (op.outside && !op.inside) { - // console.log({outline_bypass: indices, down: op.down}); - indices.forEach((ind,i) => { - if (flats.indexOf(ind) >= 0) { - // exclude flats - return; - } - let slice = newSlice(ind); - slice.shadow = shadow.clone(true); - slices.push(slice); - }); - } else - slicer.slice(indices, { each: (data, index, total) => { - shadow = unsafe ? data.tops : POLY.union(shadow.slice().appendAll(data.tops), 0.01, true); - if (flats.indexOf(data.z) >= 0) { - // exclude flats injected to complete shadow - return; - } - data.shadow = trueShadow ? CAM.shadowAt(widget, data.z) : shadow.clone(true); - data.slice.shadow = data.shadow; - // data.slice.tops[0].inner = data.shadow; - // data.slice.tops[0].inner = POLY.setZ(tshadow.clone(true), data.z); - slices.push(data.slice); - // data.slice.xray(); - // onupdate(0.2 + (index/total) * 0.1, "outlines"); - progress((index / total) * 0.5); - }, genso: true }); - shadow = POLY.union(shadow.appendAll(shadowTop.tops), 0.01, true); + } - // extend cut thru (only when z bottom is 0) - if (zThru) { - let last = slices[slices.length-1]; - for (let zneg of BASE.util.lerp(0, zThru, op.down)) { - let add = last.clone(true); - add.tops.forEach(top => top.poly.setZ(add.z)); - add.shadow = last.shadow.clone(true); - add.z -= zneg; - slices.push(add); + // console.log('indices', ...indices, {zBottom}); + slicer.slice(indices, { each: (data, index, total) => { + shadow = unsafe ? data.tops : POLY.union(shadow.slice().appendAll(data.tops), 0.01, true); + if (flats.indexOf(data.z) >= 0) { + // exclude flats injected to complete shadow + return; + } + data.shadow = trueShadow ? CAM.shadowAt(widget, data.z) : shadow.clone(true); + data.slice.shadow = data.shadow; + // data.slice.tops[0].inner = data.shadow; + // data.slice.tops[0].inner = POLY.setZ(tshadow.clone(true), data.z); + slices.push(data.slice); + progress((index / total) * 0.5); + }, genso: true }); + + if (trueShadow) { + shadow = tshadow.clone(true); + } else { + shadow = POLY.union(shadow.appendAll(shadowTop.tops), 0.01, true); + } + + // inset or eliminate thru holes from shadow + shadow = POLY.flatten(shadow.clone(true), [], true); + thruHoles.forEach(hole => { + shadow = shadow.map(p => { + if (p.isEquivalent(hole)) { + // eliminate thru holes when roughing voids enabled + if (op.voids) { + return undefined; + } + let po = POLY.offset([p], -(toolDiam / 2 + roughLeave + 0.01)); + return po ? po[0] : undefined; + } else { + return p; + } + }).filter(p => p); + }); + shadow = POLY.nest(shadow); + + // shell = shadow expanded by half tool diameter + leave stock + const sadd = roughIn ? toolDiam / 2 : toolDiam / 2; + const shell = POLY.offset(shadow, sadd + roughLeave); + + slices.forEach((slice, index) => { + let offset = [shell.clone(true),slice.shadow.clone(true)].flat(); + let flat = POLY.flatten(offset, [], true); + let nest = POLY.setZ(POLY.nest(flat), slice.z); + + // inset offset array by 1/2 diameter then by tool overlap % + offset = POLY.offset(nest, [-(toolDiam / 2 + roughLeave), -toolDiam * op.step], { + minArea: 0.1, + z: slice.z, + count: 999, + flat: true, + call: (polys, count, depth) => { + // used in depth-first path creation + polys.forEach(p => { + p.depth = depth; + if (p.inner) { + p.inner.forEach(p => p.depth = depth); + } + }); + } + }) || []; + + // add outside pass if not inside only + if (!roughIn) { + const outside = POLY.offset(shadow.clone(), toolDiam / 2 + roughLeave, {z: slice.z}); + if (outside) { + outside.forEach(p => p.depth = -p.depth); + offset.appendAll(outside); } } - slices.forEach(slice => { - let tops = slice.shadow; + if (tabs) { + tabs.forEach(tab => { + tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); + }); + offset = cutTabs(tabs, offset, slice.z); + } - // outside only (use tshadow for entire cut) - if (op.outside) { - tops = tshadow; - } + if (!offset) return; - if (op.omitthru) { - // eliminate thru holes from shadow - for (let hole of thruHoles) { - for (let top of tops) { - if (!top.inner) continue; - top.inner = top.inner.filter(innr => { - return !innr.isEquivalent(hole); - }); + if (process.camStockClipTo && stock.x && stock.y && stock.center) { + let rect = newPolygon().centerRectangle(stock.center, stock.x, stock.y); + offset = cutPolys([rect], offset, slice.z, true); + } + + // elimate double inset on inners + offset.forEach(op => { + if (op.inner) { + let pv1 = op.perimeter(); + let newinner = []; + op.inner.forEach(oi => { + let pv2 = oi.perimeter(); + let pct = pv1 > pv2 ? pv2/pv1 : pv1/pv2; + if (pct < 0.98) { + newinner.push(oi); } - } + }); + op.inner = newinner; } + }); - let offset = POLY.expand(tops, toolDiam / 2, slice.z); - if (!(offset && offset.length)) { + slice.camLines = offset; + if (false) slice.output() + .setLayer("slice", {line: 0xaaaa00}, true) + .addPolys(slice.topPolys()) + // .setLayer("top shadow", {line: 0x0000aa}) + // .addPolys(tshadow) + // .setLayer("rough shadow", {line: 0x00aa00}) + // .addPolys(shadow) + // .setLayer("rough shell", {line: 0xaa0000}) + // .addPolys(shell); + slice.output() + .setLayer("roughing", {face: color, line: color}) + .addPolys(offset); + progress(0.5 + (index / slices.length) * 0.5); + }); + + this.sliceOut = slices.filter(slice => slice.camLines); + sliceAll.appendAll(this.sliceOut); + } + + prepare(ops, progress) { + let { op, state, sliceOut, camFaces } = this; + let { setTool, setSpindle, setPrintPoint } = ops; + let { polyEmit, depthRoughPath } = ops; + let { camOut, newLayer, printPoint } = ops; + let { settings, widget } = state; + let { process, controller } = settings; + + let danger = controller.danger; + let easeDown = process.camEaseDown; + let cutdir = process.camConventional; + let depthFirst = process.camDepthFirst; + let depthData = []; + + setTool(op.tool, op.rate, op.plunge); + setSpindle(op.spindle); + + for (let slice of (camFaces || [])) { + const level = []; + for (let poly of slice.camLines) { + level.push(poly); + if (poly.inner) { + poly.inner.forEach(function(inner) { + level.push(inner); + }); + } + } + // set winding specified in output + POLY.setWinding(level, cutdir, false); + printPoint = poly2polyEmit(level, printPoint, function(poly, index, count) { + poly.forEachPoint(function(point, pidx, points, offset) { + camOut(point.clone(), offset !== 0); + }, true, index); + }); + newLayer(); + } + + let index = 0; + for (let slice of sliceOut) { + let polys = [], t = [], c = []; + POLY.flatten(slice.camLines).forEach(function (poly) { + let child = poly.parent; + if (depthFirst) { poly = poly.clone(); poly.parent = child ? 1 : 0 } + if (child) c.push(poly); else t.push(poly); + poly.layer = depthData.layer; + polys.push(poly); + }); + + // set cut direction on outer polys + POLY.setWinding(t, cutdir); + // set cut direction on inner polys + POLY.setWinding(c, !cutdir); + + if (depthFirst) { + depthData.push(polys); + } else { + printPoint = poly2polyEmit(polys, printPoint, function(poly, index, count) { + poly.forEachPoint(function(point, pidx, points, offset) { + camOut(point.clone(), offset !== 0); + }, poly.isClosed(), index); + }, { swapdir: false }); + newLayer(); + } + progress(++index / sliceOut.length, "routing"); + } + + function isNeg(v) { + return v < 0 || (v === 0 && 1/v === -Infinity); + } + + if (depthFirst) { + let ease = danger && op.down && easeDown ? op.down : 0; + let ins = depthData.map(a => a.filter(p => !isNeg(p.depth))); + let itops = ins.map(level => { + return POLY.nest(level.filter(poly => poly.depth === 0).clone()); + }); + let outs = depthData.map(a => a.filter(p => isNeg(p.depth))); + let otops = outs.map(level => { + return POLY.nest(level.filter(poly => poly.depth === 0).clone()); + }); + printPoint = depthRoughPath(printPoint, 0, ins, itops, polyEmit, false, ease); + printPoint = depthRoughPath(printPoint, 0, outs, otops, polyEmit, false, ease); + } + + setPrintPoint(printPoint); + } +} + +class OpOutline extends CamOp { + constructor(state, op) { + super(state, op); + } + + slice(progress) { + let { op, state } = this; + let { settings, widget, slicer, sliceAll, tshadow, thruHoles, unsafe, color } = state; + let { updateToolDiams, zThru, zBottom, shadowTop, tabs, cutTabs, cutPolys } = state; + let { process, stock } = settings; + + if (op.down <= 0) { + throw `invalid step down "${op.down}"`; + } + + let toolDiam = this.toolDiam = new CAM.Tool(settings, op.tool).fluteDiameter(); + updateToolDiams(toolDiam); + + let shadow = []; + let slices = []; + let indices = slicer.interval(op.down, { down: true, min: zBottom, fit: true, off: 0.01 }); + let trueShadow = process.camTrueShadow === true; + // shift out first (top-most) slice + indices.shift(); + // add flats to shadow + const flats = Object.keys(slicer.zFlat) + .map(v => (parseFloat(v) - 0.01).round(5)) + .filter(v => v > 0 && indices.indexOf(v) < 0); + indices = indices.appendAll(flats).sort((a,b) => b-a); + // console.log('indices', ...indices, {zBottom, slicer}); + if (op.outside && !op.inside) { + // console.log({outline_bypass: indices, down: op.down}); + indices.forEach((ind,i) => { + if (flats.indexOf(ind) >= 0) { + // exclude flats return; } + let slice = newSlice(ind); + slice.shadow = shadow.clone(true); + slices.push(slice); + }); + } else + slicer.slice(indices, { each: (data, index, total) => { + shadow = unsafe ? data.tops : POLY.union(shadow.slice().appendAll(data.tops), 0.01, true); + if (flats.indexOf(data.z) >= 0) { + // exclude flats injected to complete shadow + return; + } + data.shadow = trueShadow ? CAM.shadowAt(widget, data.z) : shadow.clone(true); + data.slice.shadow = data.shadow; + // data.slice.tops[0].inner = data.shadow; + // data.slice.tops[0].inner = POLY.setZ(tshadow.clone(true), data.z); + slices.push(data.slice); + // data.slice.xray(); + // onupdate(0.2 + (index/total) * 0.1, "outlines"); + progress((index / total) * 0.5); + }, genso: true }); + shadow = POLY.union(shadow.appendAll(shadowTop.tops), 0.01, true); - // when pocket only, drop first outer poly - // if it matches the shell and promote inner polys - if (op.inside) { - let shell = POLY.expand(tops.clone(), toolDiam / 2); - offset = POLY.filter(offset, [], function(poly) { - if (poly.area() < 1) { - return null; - } - for (let sp=0; sp { - // clone removes inners but the real solution is - // to limit expanded shells to through holes - POLY.expand([op.clone(true)], stepover, slice.z, offset, 1); + // extend cut thru (only when z bottom is 0) + if (zThru) { + let last = slices[slices.length-1]; + for (let zneg of base.util.lerp(0, zThru, op.down)) { + let add = last.clone(true); + add.tops.forEach(top => top.poly.setZ(add.z)); + add.shadow = last.shadow.clone(true); + add.z -= zneg; + slices.push(add); + } + } + + slices.forEach(slice => { + let tops = slice.shadow; + + // outside only (use tshadow for entire cut) + if (op.outside) { + tops = tshadow; + } + + if (op.omitthru) { + // eliminate thru holes from shadow + for (let hole of thruHoles) { + for (let top of tops) { + if (!top.inner) continue; + top.inner = top.inner.filter(innr => { + return !innr.isEquivalent(hole); }); } } - - if (tabs) { - tabs.forEach(tab => { - tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); - }); - offset = cutTabs(tabs, offset, slice.z); - } - - if (op.dogbones && !op.wide) { - CAM.addDogbones(offset, toolDiam / 5); - } - - if (process.camStockClipTo && stock.x && stock.y && stock.center) { - let rect = newPolygon().centerRectangle(stock.center, stock.x, stock.y); - offset = cutPolys([rect], offset, slice.z, true); - } - - // offset.xout(`slice ${slice.z}`); - slice.camLines = offset; - }); - - // project empty up and render - for (let slice of slices) { - if (false) slice.output() - .setLayer("slice", {line: 0xaaaa00}, false) - .addPolys(slice.topPolys()) - slice.output() - .setLayer("outline", {face: color, line: color}) - .addPolys(slice.camLines); } - sliceAll.appendAll(slices); - this.sliceOut = slices; - } - - prepare(ops, progress) { - let { op, state, sliceOut } = this; - let { setTool, setSpindle, setPrintPoint } = ops; - let { polyEmit, poly2polyEmit, depthOutlinePath } = ops; - let { camOut, newLayer, printPoint } = ops; - let { settings, widget } = state; - let { process, controller } = settings; - - let danger = controller.danger; - let easeDown = process.camEaseDown; - let toolDiam = this.toolDiam; - let cutdir = process.camConventional; - let depthFirst = process.camDepthFirst; - let depthData = []; - - setTool(op.tool, op.rate, op.plunge); - setSpindle(op.spindle); - - if (!process.camOutlinePocket) { - cutdir = !cutdir; + let offset = POLY.expand(tops, toolDiam / 2, slice.z); + if (!(offset && offset.length)) { + return; } - for (let slice of sliceOut) { - let polys = [], t = [], c = []; - POLY.flatten(slice.camLines).forEach(function (poly) { - let child = poly.parent; - if (depthFirst) { poly = poly.clone(); poly.parent = child ? 1 : 0 } - if (child) c.push(poly); else t.push(poly); - poly.layer = depthData.layer; - polys.push(poly); + // when pocket only, drop first outer poly + // if it matches the shell and promote inner polys + if (op.inside) { + let shell = POLY.expand(tops.clone(), toolDiam / 2); + offset = POLY.filter(offset, [], function(poly) { + if (poly.area() < 1) { + return null; + } + for (let sp=0; sp { + // clone removes inners but the real solution is + // to limit expanded shells to through holes + POLY.expand([op.clone(true)], stepover, slice.z, offset, 1); + }); } } - if (depthFirst) { - let flatLevels = depthData.map(level => { - return POLY.flatten(level.clone(true), [], true).filter(p => !(p.depth = 0)); - }).filter(l => l.length > 0); - if (flatLevels.length && flatLevels[0].length) { - // start with the smallest polygon on the top - printPoint = flatLevels[0] - .sort((a,b) => { return a.area() - b.area() })[0] - .average(); - // experimental start of ease down - let ease = danger && op.down && easeDown ? op.down : 0; - printPoint = depthOutlinePath(printPoint, 0, flatLevels, toolDiam, polyEmit, false, ease); - printPoint = depthOutlinePath(printPoint, 0, flatLevels, toolDiam, polyEmit, true, ease); - } + if (tabs) { + tabs.forEach(tab => { + tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); + }); + offset = cutTabs(tabs, offset, slice.z); } - setPrintPoint(printPoint); + if (op.dogbones && !op.wide) { + CAM.addDogbones(offset, toolDiam / 5); + } + + if (process.camStockClipTo && stock.x && stock.y && stock.center) { + let rect = newPolygon().centerRectangle(stock.center, stock.x, stock.y); + offset = cutPolys([rect], offset, slice.z, true); + } + + // offset.xout(`slice ${slice.z}`); + slice.camLines = offset; + }); + + // project empty up and render + for (let slice of slices) { + if (false) slice.output() + .setLayer("slice", {line: 0xaaaa00}, false) + .addPolys(slice.topPolys()) + slice.output() + .setLayer("outline", {face: color, line: color}) + .addPolys(slice.camLines); } + + sliceAll.appendAll(slices); + this.sliceOut = slices; } - class OpContour extends CamOp { - constructor(state, op) { - super(state, op); + prepare(ops, progress) { + let { op, state, sliceOut } = this; + let { setTool, setSpindle, setPrintPoint } = ops; + let { polyEmit, depthOutlinePath } = ops; + let { camOut, newLayer, printPoint } = ops; + let { settings, widget } = state; + let { process, controller } = settings; + + let danger = controller.danger; + let easeDown = process.camEaseDown; + let toolDiam = this.toolDiam; + let cutdir = process.camConventional; + let depthFirst = process.camDepthFirst; + let depthData = []; + + setTool(op.tool, op.rate, op.plunge); + setSpindle(op.spindle); + + if (!process.camOutlinePocket) { + cutdir = !cutdir; } - slice(progress) { - let { op, state } = this; - let { sliceAll } = state; - // we need topo for safe travel moves when roughing and outlining - // not generated when drilling-only. then all z moves use bounds max. - // also generates x and y contouring when selected - let topo = new CAM.Topo({ - // onupdate: (update, msg) => { - onupdate: (index, total, msg) => { - progress(index / total, msg); - }, - ondone: (slices) => { - this.sliceOut = slices; - sliceAll.appendAll(slices); - }, - contour: op, - state: state + for (let slice of sliceOut) { + let polys = [], t = [], c = []; + POLY.flatten(slice.camLines).forEach(function (poly) { + let child = poly.parent; + if (depthFirst) { poly = poly.clone(); poly.parent = child ? 1 : 0 } + if (child) c.push(poly); else t.push(poly); + poly.layer = depthData.layer; + polys.push(poly); }); - // computed if set to 0 - this.tolerance = topo.tolerance; - } - prepare(ops, progress) { - let { op, state, sliceOut } = this; - let { setTolerance, setTool, setSpindle, setPrintPoint } = ops; - let { polyEmit, poly2polyEmit, tip2tipEmit } = ops; - let { camOut, newLayer, printPoint, lastPoint } = ops; - let { bounds, zmax } = ops; - let { settings, widget } = state; - let { process } = settings; - - let toolDiam = this.toolDiam = new CAM.Tool(settings, op.tool).fluteDiameter(); - let stepover = toolDiam * op.step * 2; - let cutdir = process.camConventional; - let depthFirst = process.camDepthFirst; - let depthData = []; - - setTool(op.tool, op.rate, op.plunge); - setSpindle(op.spindle); - setTolerance(this.tolerance); - - printPoint = newPoint(bounds.min.x,bounds.min.y,zmax); - - for (let slice of sliceOut) { - if (!slice.camLines) continue; - let polys = [], poly, emit; - slice.camLines.forEach(function (poly) { - if (depthFirst) poly = poly.clone(true); - polys.push({first:poly.first(), last:poly.last(), poly:poly}); - }); - if (depthFirst) { - depthData.appendAll(polys); - } else { - printPoint = tip2tipEmit(polys, printPoint, function(el, point, count) { - poly = el.poly; - if (poly.last() === point) { - poly.reverse(); - } - poly.forEachPoint(function(point, pidx) { - camOut(point.clone(), pidx > 0, stepover); - }, false); - }); - newLayer(); - } - } + // set cut direction on outer polys + POLY.setWinding(t, cutdir); + // set cut direction on inner polys + POLY.setWinding(c, !cutdir); if (depthFirst) { - printPoint = tip2tipEmit(depthData, printPoint, function(el, point, count) { - let poly = el.poly; + depthData.push(polys); + } else { + printPoint = poly2polyEmit(polys, printPoint, function(poly, index, count) { + poly.forEachPoint(function(point, pidx, points, offset) { + camOut(point.clone(), offset !== 0); + }, poly.isClosed(), index); + }, { swapdir: false }); + newLayer(); + } + } + + if (depthFirst) { + let flatLevels = depthData.map(level => { + return POLY.flatten(level.clone(true), [], true).filter(p => !(p.depth = 0)); + }).filter(l => l.length > 0); + if (flatLevels.length && flatLevels[0].length) { + // start with the smallest polygon on the top + printPoint = flatLevels[0] + .sort((a,b) => { return a.area() - b.area() })[0] + .average(); + // experimental start of ease down + let ease = danger && op.down && easeDown ? op.down : 0; + printPoint = depthOutlinePath(printPoint, 0, flatLevels, toolDiam, polyEmit, false, ease); + printPoint = depthOutlinePath(printPoint, 0, flatLevels, toolDiam, polyEmit, true, ease); + } + } + + setPrintPoint(printPoint); + } +} + +class OpContour extends CamOp { + constructor(state, op) { + super(state, op); + } + + slice(progress) { + let { op, state } = this; + let { sliceAll } = state; + // we need topo for safe travel moves when roughing and outlining + // not generated when drilling-only. then all z moves use bounds max. + // also generates x and y contouring when selected + let topo = new CAM.Topo({ + // onupdate: (update, msg) => { + onupdate: (index, total, msg) => { + progress(index / total, msg); + }, + ondone: (slices) => { + this.sliceOut = slices; + sliceAll.appendAll(slices); + }, + contour: op, + state: state + }); + // computed if set to 0 + this.tolerance = topo.tolerance; + } + + prepare(ops, progress) { + let { op, state, sliceOut } = this; + let { setTolerance, setTool, setSpindle, setPrintPoint } = ops; + let { polyEmit } = ops; + let { camOut, newLayer, printPoint, lastPoint } = ops; + let { bounds, zmax } = ops; + let { settings, widget } = state; + let { process } = settings; + + let toolDiam = this.toolDiam = new CAM.Tool(settings, op.tool).fluteDiameter(); + let stepover = toolDiam * op.step * 2; + let cutdir = process.camConventional; + let depthFirst = process.camDepthFirst; + let depthData = []; + + setTool(op.tool, op.rate, op.plunge); + setSpindle(op.spindle); + setTolerance(this.tolerance); + + printPoint = newPoint(bounds.min.x,bounds.min.y,zmax); + + for (let slice of sliceOut) { + if (!slice.camLines) continue; + let polys = [], poly, emit; + slice.camLines.forEach(function (poly) { + if (depthFirst) poly = poly.clone(true); + polys.push({first:poly.first(), last:poly.last(), poly:poly}); + }); + if (depthFirst) { + depthData.appendAll(polys); + } else { + printPoint = tip2tipEmit(polys, printPoint, function(el, point, count) { + poly = el.poly; if (poly.last() === point) { poly.reverse(); } poly.forEachPoint(function(point, pidx) { camOut(point.clone(), pidx > 0, stepover); }, false); - newLayer(); - return lastPoint(); }); + newLayer(); } + } - setPrintPoint(printPoint); + if (depthFirst) { + printPoint = tip2tipEmit(depthData, printPoint, function(el, point, count) { + let poly = el.poly; + if (poly.last() === point) { + poly.reverse(); + } + poly.forEachPoint(function(point, pidx) { + camOut(point.clone(), pidx > 0, stepover); + }, false); + newLayer(); + return lastPoint(); + }); + } + + setPrintPoint(printPoint); + } +} + +class OpTrace extends CamOp { + constructor(state, op) { + super(state, op); + } + + slice(progress) { + let { op, state } = this; + let { tool, rate, down, plunge, offset } = op; + let { settings, widget, sliceAll, zMax, zTop, zThru, tabs } = state; + let { updateToolDiams, cutTabs, cutPolys, healPolys } = state; + let { process, stock } = settings; + // generate tracing offsets from chosen features + let sliceOut = this.sliceOut = []; + let areas = op.areas[widget.id] || []; + let toolDiam = new CAM.Tool(settings, tool).fluteDiameter(); + let toolOver = toolDiam * op.step; + let cutdir = process.camConventional; + let polys = []; + let stockRect = stock.center && stock.x && stock.y ? + newPolygon().centerRectangle(stock.center, stock.x, stock.y) : undefined; + updateToolDiams(toolDiam); + if (tabs) { + tabs.forEach(tab => { + tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); + }); + } + for (let arr of areas) { + let poly = newPolygon().fromArray(arr); + POLY.setWinding([ poly ], cutdir, false); + polys.push(poly); + } + function newSliceOut(z) { + let slice = newSlice(z); + sliceAll.push(slice); + sliceOut.push(slice); + return slice; + } + function followZ(poly) { + let z = poly.getZ(); + let slice = newSliceOut(z); + slice.camTrace = { tool, rate, plunge }; + if (tabs) { + slice.camLines = cutTabs(tabs, [poly], z); + } else { + slice.camLines = [ poly ]; + } + if (process.camStockClipTo && stockRect) { + slice.camLines = cutPolys([stockRect], slice.camLines, z, true); + } + slice.output() + .setLayer("follow", {line: 0xaa00aa}, false) + .addPolys(slice.camLines) + } + function clearZ(polys, z, down) { + let zs = down ? base.util.lerp(zTop, z, down) : [ z ]; + let nested = POLY.nest(polys); + for (let poly of nested) { + for (let z of zs) { + let slice = newSliceOut(z); + slice.camTrace = { tool, rate, plunge }; + POLY.offset([ poly ], -toolOver, { + count:999, outs: slice.camLines = [], flat:true, z + }); + 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); + slice.output() + .setLayer("clear", {line: 0xaa00aa}, false) + .addPolys(slice.camLines) + } + } + } + function similar(v1, v2, epsilon = 0.01) { + return Math.abs(v1-v2) <= epsilon; + } + function centerPoly(p1, p2) { + // follow poly with most points + if (p2.length > p1.length) { + let t = p1; + p1 = p2; + p2 = t; + } + let np = newPolygon().setOpen(true); + for (let p of p1.points) { + let q = p2.findClosestPointTo(p); + np.push(p.midPointTo3D(q.point)); + } + return np; + } + function centerPolys(polys) { + // select open polys and sort by length + let ptst = polys.filter(p => p.isOpen()).sort((a,b) => b.perimeter() - a.perimeter()); + if (ptst.length < 2) { + return polys; + } + let pt = newPoint(0,0,0); + // ensure polys are ordered with start point closest to 0,0 + ptst.forEach(p => { + if (p.last().distTo2D(pt) < p.first().distTo2D(pt)) { + p.reverse(); + } + }); + let pout = polys.filter(p => p.isClosed()); + outer: for (let i=0,l=ptst.length; i p)); + return pout; + } + // connect selected segments if open and touching + polys = healPolys(polys); + // find center line for open polys spaced by tool diameter + polys = centerPolys(polys); + switch (op.mode) { + case "follow": + let routed = []; + poly2polyEmit(polys, newPoint(0,0,0), (poly, index, count, spoint) => { + routed.push(poly); + }); + for (let poly of routed) { + let offdist = 0; + switch (offset) { + case "outside": offdist = toolDiam / 2; break; + case "inside": offdist = -toolDiam / 2; break; + } + if (offdist) { + let pnew = POLY.offset([poly], offdist); + if (pnew) { + poly = POLY.setZ(pnew, poly.getZ()); + } else { + continue; + } + } else { + poly = [ poly ]; + } + for (let pi of poly) + if (down) { + let zto = pi.getZ(); + if (zThru && similar(zto,0)) { + zto -= zThru; + } + for (let z of base.util.lerp(zTop, zto, down)) { + followZ(pi.clone().setZ(z)); + } + } else { + followZ(pi); + } + } + break; + case "clear": + let zmap = {}; + for (let poly of polys) { + let z = poly.getZ(); + (zmap[z] = zmap[z] || []).push(poly); + } + for (let [zv, polys] of Object.entries(zmap)) { + clearZ(polys, parseFloat(zv), down); + } } } - class OpTrace extends CamOp { - constructor(state, op) { - super(state, op); - } + prepare(ops, progress) { + let { op, state } = this; + let { settings } = state; + let { setTool, setSpindle } = ops; - slice(progress) { - let { op, state } = this; - let { tool, rate, down, plunge, offset } = op; - let { settings, widget, sliceAll, zMax, zTop, zThru, tabs } = state; - let { updateToolDiams, cutTabs, cutPolys, healPolys } = state; - let { process, stock } = settings; - let poly2polyEmit = BASE.util.poly2polyEmit; - // generate tracing offsets from chosen features - let sliceOut = this.sliceOut = []; - let areas = op.areas[widget.id] || []; - let toolDiam = new CAM.Tool(settings, tool).fluteDiameter(); - let toolOver = toolDiam * op.step; - let cutdir = process.camConventional; - let polys = []; - let stockRect = stock.center && stock.x && stock.y ? - newPolygon().centerRectangle(stock.center, stock.x, stock.y) : undefined; - updateToolDiams(toolDiam); - if (tabs) { - tabs.forEach(tab => { - tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); - }); - } - for (let arr of areas) { - let poly = newPolygon().fromArray(arr); - POLY.setWinding([ poly ], cutdir, false); - polys.push(poly); - } - function newSliceOut(z) { - let slice = newSlice(z); - sliceAll.push(slice); - sliceOut.push(slice); - return slice; - } - function followZ(poly) { - let z = poly.getZ(); - let slice = newSliceOut(z); - slice.camTrace = { tool, rate, plunge }; - if (tabs) { - slice.camLines = cutTabs(tabs, [poly], z); - } else { - slice.camLines = [ poly ]; - } - if (process.camStockClipTo && stockRect) { - slice.camLines = cutPolys([stockRect], slice.camLines, z, true); - } - slice.output() - .setLayer("follow", {line: 0xaa00aa}, false) - .addPolys(slice.camLines) - } - function clearZ(polys, z, down) { - let zs = down ? BASE.util.lerp(zTop, z, down) : [ z ]; - let nested = POLY.nest(polys); - for (let poly of nested) { - for (let z of zs) { - let slice = newSliceOut(z); - slice.camTrace = { tool, rate, plunge }; - POLY.offset([ poly ], -toolOver, { - count:999, outs: slice.camLines = [], flat:true, z - }); - if (tabs) { - slice.camLines = cutTabs(tabs, POLY.flatten(slice.camLines, null, true), z); - } else { - slice.camLines = POLY.flatten(slice.camLines, null, true); + setTool(op.tool, op.rate); + setSpindle(op.spindle); + for (let slice of this.sliceOut) { + ops.emitTrace(slice); + } + } +} + +class OpDrill extends CamOp { + constructor(state, op) { + super(state, op); + } + + slice(progress) { + let { op, state } = this; + let { settings, sliceAll, tslices, updateToolDiams } = state; + let { zBottom, zThru, thruHoles, color } = state; + + let drills = [], + drillTool = new CAM.Tool(settings, op.tool), + drillToolDiam = drillTool.fluteDiameter(), + centerDiff = drillToolDiam * 0.1, + area = (drillToolDiam/2) * (drillToolDiam/2) * Math.PI, + areaDelta = op.mark ? Infinity : area * 0.05, + sliceOut = this.sliceOut = []; + + updateToolDiams(drillToolDiam); + + // for each slice, look for polygons with 98.5% circularity whose + // area is within the tolerance of a circle matching the tool diameter + tslices.forEach(function(slice) { + if (slice.z < zBottom) return; + let inner = slice.topPolyInners([]); + inner.forEach(function(poly) { + if (poly.circularity() >= 0.985 && Math.abs(poly.area() - area) <= areaDelta) { + let center = poly.circleCenter(), + merged = false, + closest = Infinity, + dist; + // TODO reject if inside camShellPolys (means there is material above) + if (center.isInPolygon(slice.shadow)) return; + drills.forEach(function(drill) { + if (merged) return; + if ((dist = drill.last().distTo2D(center)) <= centerDiff) { + merged = true; + drill.push(center); } - POLY.setWinding(slice.camLines, cutdir, false); - slice.output() - .setLayer("clear", {line: 0xaa00aa}, false) - .addPolys(slice.camLines) - } - } - } - function similar(v1, v2, epsilon = 0.01) { - return Math.abs(v1-v2) <= epsilon; - } - function centerPoly(p1, p2) { - // follow poly with most points - if (p2.length > p1.length) { - let t = p1; - p1 = p2; - p2 = t; - } - let np = newPolygon().setOpen(true); - for (let p of p1.points) { - let q = p2.findClosestPointTo(p); - np.push(p.midPointTo3D(q.point)); - } - return np; - } - function centerPolys(polys) { - // select open polys and sort by length - let ptst = polys.filter(p => p.isOpen()).sort((a,b) => b.perimeter() - a.perimeter()); - if (ptst.length < 2) { - return polys; - } - let pt = newPoint(0,0,0); - // ensure polys are ordered with start point closest to 0,0 - ptst.forEach(p => { - if (p.last().distTo2D(pt) < p.first().distTo2D(pt)) { - p.reverse(); - } - }); - let pout = polys.filter(p => p.isClosed()); - outer: for (let i=0,l=ptst.length; i p)); - return pout; - } - // connect selected segments if open and touching - polys = healPolys(polys); - // find center line for open polys spaced by tool diameter - polys = centerPolys(polys); - switch (op.mode) { - case "follow": - let routed = []; - poly2polyEmit(polys, newPoint(0,0,0), (poly, index, count, spoint) => { - routed.push(poly); + closest = Math.min(closest,dist); }); - for (let poly of routed) { - let offdist = 0; - switch (offset) { - case "outside": offdist = toolDiam / 2; break; - case "inside": offdist = -toolDiam / 2; break; - } - if (offdist) { - let pnew = POLY.offset([poly], offdist); - if (pnew) { - poly = POLY.setZ(pnew, poly.getZ()); - } else { - continue; - } - } else { - poly = [ poly ]; - } - for (let pi of poly) - if (down) { - let zto = pi.getZ(); - if (zThru && similar(zto,0)) { - zto -= zThru; - } - for (let z of BASE.util.lerp(zTop, zto, down)) { - followZ(pi.clone().setZ(z)); - } - } else { - followZ(pi); - } - } - break; - case "clear": - let zmap = {}; - for (let poly of polys) { - let z = poly.getZ(); - (zmap[z] = zmap[z] || []).push(poly); - } - for (let [zv, polys] of Object.entries(zmap)) { - clearZ(polys, parseFloat(zv), down); + if (!merged) { + drills.push(newPolygon().append(center)); } + } + }); + }); + + // drill points to use center (average of all points) of the polygon + drills.forEach(function(drill) { + let center = drill.center(true), + slice = newSlice(0,null); + if (op.mark) { + // replace points with single mark + let points = drill.points; + points = [ + points[0], + points[0].clone().sub({x:0, y:0, z:op.down}) + ]; + drill.points = points; } + drill.points.forEach(function(point) { + point.x = center.x; + point.y = center.y; + }); + // for thru holes, follow z thru when set + if (zThru && center.isInPolygon(thruHoles)) { + drill.points.push(drill.points.last().sub({x:0,y:0,z:zThru})); + } + slice.camLines = [ drill ]; + slice.output() + .setLayer("drill", {face: color, line: color}) + .addPolys(drill); + sliceAll.push(slice); + sliceOut.push(slice); + }); + } + + prepare(ops, progress) { + let { op, state } = this; + let { settings, widget, sliceAll, updateToolDiams } = state; + let { setTool, setSpindle, setDrill, emitDrills } = ops; + + setTool(op.tool, undefined, op.rate); + setDrill(op.down, op.lift, op.dwell); + setSpindle(op.spindle); + emitDrills(this.sliceOut.map(slice => slice.camLines).flat()); + } +} + +class OpRegister extends CamOp { + constructor(state, op) { + super(state, op); + } + + slice(progress) { + let { op, state } = this; + let { settings, widget, bounds, sliceAll, zMax, zThru, color } = state; + let { updateToolDiams } = state; + + let tool = new CAM.Tool(settings, op.tool); + let sliceOut = this.sliceOut = []; + + updateToolDiams(tool.fluteDiameter()); + + let { stock } = settings, + tz = widget.track.pos.z, + lx = bounds.min.x, + hx = bounds.max.x, + ly = bounds.min.y, + hy = bounds.max.y, + o3 = tool.fluteDiameter() * 2, + mx = (lx + hx) / 2, + my = (ly + hy) / 2, + mz = zThru || 0, + dx = (stock.x - (hx - lx)) / 4, + dy = (stock.y - (hy - ly)) / 4, + dz = stock.z, + points = [], + wo = stock.z - bounds.max.z, + z1 = bounds.max.z + wo + tz, + z2 = tz - mz; + + if (!(stock.x && stock.y && stock.z)) { + return; } - prepare(ops, progress) { - let { op, state } = this; - let { settings } = state; - let { setTool, setSpindle } = ops; + switch (op.axis) { + case "X": + case "x": + if (op.points == 3) { + points.push(newPoint(lx - dx, my, 0)); + points.push(newPoint(hx + dx, my - o3, 0)); + points.push(newPoint(hx + dx, my + o3, 0)); + } else { + points.push(newPoint(lx - dx, my, 0)); + points.push(newPoint(hx + dx, my, 0)); + } + break; + case "Y": + case "y": + if (op.points == 3) { + points.push(newPoint(mx, ly - dy, 0)); + points.push(newPoint(mx - o3, hy + dy, 0)); + points.push(newPoint(mx + o3, hy + dy, 0)); + } else { + points.push(newPoint(mx, ly - dy, 0)); + points.push(newPoint(mx, hy + dy, 0)); + } + break; + case "-": + let o2 = o3 / 2, + x0 = lx - dx, + x1 = hx + dx, + y0 = ly - dy - o2, + y1 = hy + dy + o2, + x4 = (x1 - x0 - o2) / 4, + y4 = (y1 - y0 - o2 * 3) / 4, + poly, cp, cz; + function start(z) { + cz = z; + cp = {x:x0 + o2 * 0.5, y:y0 + o2 * 1.5}; + poly = newPolygon().add(cp.x, cp.y, z); + } + function move(dx, dy) { + cp.x += dx; + cp.y += dy; + poly.add(cp.x, cp.y, cz); + } + function rept(count, tv, fn) { + while (count-- > 0) { + fn(tv, count === 0); + tv = -tv; + } + } + for (let z of base.util.lerp(z1, z2, op.down)) { + let slice = newSlice(z); + sliceAll.push(slice); + sliceOut.push(slice); + start(z); + rept(4, o2, oy => { + move(0, -oy); + move(x4, 0); + }); + rept(4, o2, ox => { + move(ox, 0); + move(0, y4); + }); + rept(4, o2, oy => { + move(0, oy); + move(-x4, 0); + }); + rept(4, o2, ox => { + move(-ox, 0); + move(0, -y4); + }); + poly.points.pop(); + slice.camTrace = { tool: tool.getID(), rate: op.feed, plunge: op.rate }; + slice.camLines = [ poly ]; + slice.output() + .setLayer("register", {line: 0xaa00aa}, false) + .addPolys(slice.camLines) + } + break; + } - setTool(op.tool, op.rate); + if (points.length) { + let slice = newSlice(0,null), polys = []; + points.forEach(point => { + polys.push(newPolygon() + .append(point.clone().setZ(z1)) + .append(point.clone().setZ(z2))); + }); + slice.camLines = polys; + slice.output() + .setLayer("register", {face: color, line: color}) + .addPolys(polys); + sliceAll.push(slice); + sliceOut.push(slice); + } + } + + prepare(ops, progress) { + let { op, state } = this; + let { settings, widget, sliceAll, updateToolDiams } = state; + let { setTool, setSpindle, setDrill, emitDrills } = ops; + + if (op.axis === '-') { + setTool(op.tool, op.feed, op.rate); setSpindle(op.spindle); for (let slice of this.sliceOut) { ops.emitTrace(slice); } - } - } - - class OpDrill extends CamOp { - constructor(state, op) { - super(state, op); - } - - slice(progress) { - let { op, state } = this; - let { settings, sliceAll, tslices, updateToolDiams } = state; - let { zBottom, zThru, thruHoles, color } = state; - - let drills = [], - drillTool = new CAM.Tool(settings, op.tool), - drillToolDiam = drillTool.fluteDiameter(), - centerDiff = drillToolDiam * 0.1, - area = (drillToolDiam/2) * (drillToolDiam/2) * Math.PI, - areaDelta = op.mark ? Infinity : area * 0.05, - sliceOut = this.sliceOut = []; - - updateToolDiams(drillToolDiam); - - // for each slice, look for polygons with 98.5% circularity whose - // area is within the tolerance of a circle matching the tool diameter - tslices.forEach(function(slice) { - if (slice.z < zBottom) return; - let inner = slice.topPolyInners([]); - inner.forEach(function(poly) { - if (poly.circularity() >= 0.985 && Math.abs(poly.area() - area) <= areaDelta) { - let center = poly.circleCenter(), - merged = false, - closest = Infinity, - dist; - // TODO reject if inside camShellPolys (means there is material above) - if (center.isInPolygon(slice.shadow)) return; - drills.forEach(function(drill) { - if (merged) return; - if ((dist = drill.last().distTo2D(center)) <= centerDiff) { - merged = true; - drill.push(center); - } - closest = Math.min(closest,dist); - }); - if (!merged) { - drills.push(newPolygon().append(center)); - } - } - }); - }); - - // drill points to use center (average of all points) of the polygon - drills.forEach(function(drill) { - let center = drill.center(true), - slice = newSlice(0,null); - if (op.mark) { - // replace points with single mark - let points = drill.points; - points = [ - points[0], - points[0].clone().sub({x:0, y:0, z:op.down}) - ]; - drill.points = points; - } - drill.points.forEach(function(point) { - point.x = center.x; - point.y = center.y; - }); - // for thru holes, follow z thru when set - if (zThru && center.isInPolygon(thruHoles)) { - drill.points.push(drill.points.last().sub({x:0,y:0,z:zThru})); - } - slice.camLines = [ drill ]; - slice.output() - .setLayer("drill", {face: color, line: color}) - .addPolys(drill); - sliceAll.push(slice); - sliceOut.push(slice); - }); - } - - prepare(ops, progress) { - let { op, state } = this; - let { settings, widget, sliceAll, updateToolDiams } = state; - let { setTool, setSpindle, setDrill, emitDrills } = ops; - + } else { setTool(op.tool, undefined, op.rate); setDrill(op.down, op.lift, op.dwell); setSpindle(op.spindle); emitDrills(this.sliceOut.map(slice => slice.camLines).flat()); } } +} - class OpRegister extends CamOp { - constructor(state, op) { - super(state, op); +class OpXRay extends CamOp { + constructor(state, op) { + super(state, op); + } + + slice(progress) { + let { widget, sliceAll } = this.state; + let slicer = new kiri.cam_slicer(widget.getPoints(), { + zlist: true, + zline: true + }); + let xrayind = Object.keys(slicer.zLine) + .map(v => parseFloat(v).round(5)) + .sort((a,b) => a-b); + let xrayopt = { each: (data, index, total) => { + let slice = newSlice(data.z); + slice.addTops(data.tops); + // data.tops.forEach(top => slice.addTop(top)); + slice.lines = data.lines; + slice.xray(); + sliceAll.push(slice); + }, over: false, flatoff: 0, edges: true, openok: true }; + slicer.slice(xrayind, xrayopt); + // xrayopt.over = true; + // slicer.slice(xrayind, xrayopt); + } +} + +class OpShadow extends CamOp { + constructor(state, op) { + super(state, op); + } + + slice(progress) { + let state = this.state; + let { ops, slicer, widget, unsafe, sliceAll } = state; + + let real = ops.map(rec => rec.op).filter(op => op); + let drill = real.filter(op => op.type === 'drill').length > 0; + let rough = real.filter(op => op.type === 'rough').length > 0; + let outline = real.filter(op => op.type === 'outline').length > 0; + let outlineOut = real.filter(op => op.type === 'outline' && op.outside).length > 0; + let trueShadow = state.settings.process.camTrueShadow === true; + + let minStepDown = real + .map(op => (op.down || 3) / (trueShadow ? 1 : 3)) + .reduce((a,v) => Math.min(a, v, 1)); + + let tslices = []; + let tshadow = []; + let tzindex = slicer.interval(minStepDown, { fit: true, off: 0.01, down: true, flats: true }); + let complex = tzindex.length > 50 || widget.vertices.length > 1000000; + let skipTerrain = unsafe;// || (!rough && !outline && !drill && complex); + + if (skipTerrain) { + console.log("skipping terrain generation"); + tzindex = [ tzindex.pop() ]; } - slice(progress) { - let { op, state } = this; - let { settings, widget, bounds, sliceAll, zMax, zThru, color } = state; - let { updateToolDiams } = state; - - let tool = new CAM.Tool(settings, op.tool); - let sliceOut = this.sliceOut = []; - - updateToolDiams(tool.fluteDiameter()); - - let { stock } = settings, - tz = widget.track.pos.z, - lx = bounds.min.x, - hx = bounds.max.x, - ly = bounds.min.y, - hy = bounds.max.y, - o3 = tool.fluteDiameter() * 2, - mx = (lx + hx) / 2, - my = (ly + hy) / 2, - mz = zThru || 0, - dx = (stock.x - (hx - lx)) / 4, - dy = (stock.y - (hy - ly)) / 4, - dz = stock.z, - points = [], - wo = stock.z - bounds.max.z, - z1 = bounds.max.z + wo + tz, - z2 = tz - mz; - - if (!(stock.x && stock.y && stock.z)) { - return; - } - - switch (op.axis) { - case "X": - case "x": - if (op.points == 3) { - points.push(newPoint(lx - dx, my, 0)); - points.push(newPoint(hx + dx, my - o3, 0)); - points.push(newPoint(hx + dx, my + o3, 0)); - } else { - points.push(newPoint(lx - dx, my, 0)); - points.push(newPoint(hx + dx, my, 0)); - } - break; - case "Y": - case "y": - if (op.points == 3) { - points.push(newPoint(mx, ly - dy, 0)); - points.push(newPoint(mx - o3, hy + dy, 0)); - points.push(newPoint(mx + o3, hy + dy, 0)); - } else { - points.push(newPoint(mx, ly - dy, 0)); - points.push(newPoint(mx, hy + dy, 0)); - } - break; - case "-": - let o2 = o3 / 2, - x0 = lx - dx, - x1 = hx + dx, - y0 = ly - dy - o2, - y1 = hy + dy + o2, - x4 = (x1 - x0 - o2) / 4, - y4 = (y1 - y0 - o2 * 3) / 4, - poly, cp, cz; - function start(z) { - cz = z; - cp = {x:x0 + o2 * 0.5, y:y0 + o2 * 1.5}; - poly = newPolygon().add(cp.x, cp.y, z); - } - function move(dx, dy) { - cp.x += dx; - cp.y += dy; - poly.add(cp.x, cp.y, cz); - } - function rept(count, tv, fn) { - while (count-- > 0) { - fn(tv, count === 0); - tv = -tv; - } - } - for (let z of BASE.util.lerp(z1, z2, op.down)) { - let slice = newSlice(z); - sliceAll.push(slice); - sliceOut.push(slice); - start(z); - rept(4, o2, oy => { - move(0, -oy); - move(x4, 0); - }); - rept(4, o2, ox => { - move(ox, 0); - move(0, y4); - }); - rept(4, o2, oy => { - move(0, oy); - move(-x4, 0); - }); - rept(4, o2, ox => { - move(-ox, 0); - move(0, -y4); - }); - poly.points.pop(); - slice.camTrace = { tool: tool.getID(), rate: op.feed, plunge: op.rate }; - slice.camLines = [ poly ]; - slice.output() - .setLayer("register", {line: 0xaa00aa}, false) - .addPolys(slice.camLines) - } - break; - } - - if (points.length) { - let slice = newSlice(0,null), polys = []; - points.forEach(point => { - polys.push(newPolygon() - .append(point.clone().setZ(z1)) - .append(point.clone().setZ(z2))); - }); - slice.camLines = polys; + let lsz; // only shadow up to bottom of last shadow for progressive union + let terrain = slicer.slice(tzindex, { each: (data, index, total) => { + let shadowAt = trueShadow ? CAM.shadowAt(widget, data.z, lsz) : []; + tshadow = POLY.union(tshadow.slice().appendAll(data.tops).appendAll(shadowAt), 0.01, true); + tslices.push(data.slice); + data.slice.shadow = tshadow; + if (false) { + const slice = data.slice; + sliceAll.push(slice); slice.output() - .setLayer("register", {face: color, line: color}) - .addPolys(polys); - sliceAll.push(slice); - sliceOut.push(slice); + .setLayer("shadow", {line: 0x888800, thin: true }) + .addPolys(POLY.setZ(tshadow.clone(true), data.z), { thin: true }); + slice.output() + .setLayer("slice", {line: 0x886622, thin: true }) + .addPolys(POLY.setZ(data.tops.clone(true), data.z), { thin: true }); + // let p1 = [], p2 = [], cp = p1; + // for (let line of data.lines) { + // cp.push(line.p1); + // cp.push(line.p2); + // cp = (cp === p1 ? p2 : p1); + // } + // slice.output() + // .setLayer("lines1", {line: 0x884444, thin: true }) + // .addLines(p1, { thin: true }); + // slice.output() + // .setLayer("lines2", {line: 0x444488, thin: true }) + // .addLines(p2, { thin: true }); } - } + lsz = data.z; + progress(index / total); + }, genso: true }); - prepare(ops, progress) { - let { op, state } = this; - let { settings, widget, sliceAll, updateToolDiams } = state; - let { setTool, setSpindle, setDrill, emitDrills } = ops; + state.shadowTop = terrain[terrain.length - 1]; + state.center = tshadow[0].bounds.center(); + state.tshadow = tshadow; + state.terrain = terrain; + state.tslices = tslices; + state.skipTerrain = skipTerrain; - if (op.axis === '-') { - setTool(op.tool, op.feed, op.rate); - setSpindle(op.spindle); - for (let slice of this.sliceOut) { - ops.emitTrace(slice); - } - } else { - setTool(op.tool, undefined, op.rate); - setDrill(op.down, op.lift, op.dwell); - setSpindle(op.spindle); - emitDrills(this.sliceOut.map(slice => slice.camLines).flat()); - } - } + // identify through holes + state.thruHoles = tshadow.map(p => p.inner || []).flat(); } +} - class OpXRay extends CamOp { - constructor(state, op) { - super(state, op); +CAM.shadowAt = function(widget, z, ztop) { + let geo = widget.cache.geo || new THREE.BufferGeometry() + .setAttribute('position', new THREE.BufferAttribute(widget.vertices, 3)); + widget.cache.geo = geo; + let rad = (Math.PI / 180); + let deg = (180 / Math.PI); + let angle = rad * 1; + let thresh = -Math.sin(angle); + let found = []; + let { position } = geo.attributes; + let { itemSize, count, array } = position; + for (let i = 0; i ztop && b.z > ztop && c.z > ztop) { + // skip faces over top threshold + continue; } - - slice(progress) { - let { widget, sliceAll } = this.state; - let slicer = new KIRI.cam_slicer(widget.getPoints(), { - zlist: true, - zline: true - }); - let xrayind = Object.keys(slicer.zLine) - .map(v => parseFloat(v).round(5)) - .sort((a,b) => a-b); - let xrayopt = { each: (data, index, total) => { - let slice = newSlice(data.z); - slice.addTops(data.tops); - // data.tops.forEach(top => slice.addTop(top)); - slice.lines = data.lines; - slice.xray(); - sliceAll.push(slice); - }, over: false, flatoff: 0, edges: true, openok: true }; - slicer.slice(xrayind, xrayopt); - // xrayopt.over = true; - // slicer.slice(xrayind, xrayopt); - } - } - - class OpShadow extends CamOp { - constructor(state, op) { - super(state, op); - } - - slice(progress) { - let state = this.state; - let { ops, slicer, widget, unsafe, sliceAll } = state; - - let real = ops.map(rec => rec.op).filter(op => op); - let drill = real.filter(op => op.type === 'drill').length > 0; - let rough = real.filter(op => op.type === 'rough').length > 0; - let outline = real.filter(op => op.type === 'outline').length > 0; - let outlineOut = real.filter(op => op.type === 'outline' && op.outside).length > 0; - let trueShadow = state.settings.process.camTrueShadow === true; - - let minStepDown = real - .map(op => (op.down || 3) / (trueShadow ? 1 : 3)) - .reduce((a,v) => Math.min(a, v, 1)); - - let tslices = []; - let tshadow = []; - let tzindex = slicer.interval(minStepDown, { fit: true, off: 0.01, down: true, flats: true }); - let complex = tzindex.length > 50 || widget.vertices.length > 1000000; - let skipTerrain = unsafe;// || (!rough && !outline && !drill && complex); - - if (skipTerrain) { - console.log("skipping terrain generation"); - tzindex = [ tzindex.pop() ]; - } - - let lsz; // only shadow up to bottom of last shadow for progressive union - let terrain = slicer.slice(tzindex, { each: (data, index, total) => { - let shadowAt = trueShadow ? CAM.shadowAt(widget, data.z, lsz) : []; - tshadow = POLY.union(tshadow.slice().appendAll(data.tops).appendAll(shadowAt), 0.01, true); - tslices.push(data.slice); - data.slice.shadow = tshadow; - if (false) { - const slice = data.slice; - sliceAll.push(slice); - slice.output() - .setLayer("shadow", {line: 0x888800, thin: true }) - .addPolys(POLY.setZ(tshadow.clone(true), data.z), { thin: true }); - slice.output() - .setLayer("slice", {line: 0x886622, thin: true }) - .addPolys(POLY.setZ(data.tops.clone(true), data.z), { thin: true }); - // let p1 = [], p2 = [], cp = p1; - // for (let line of data.lines) { - // cp.push(line.p1); - // cp.push(line.p2); - // cp = (cp === p1 ? p2 : p1); - // } - // slice.output() - // .setLayer("lines1", {line: 0x884444, thin: true }) - // .addLines(p1, { thin: true }); - // slice.output() - // .setLayer("lines2", {line: 0x444488, thin: true }) - // .addLines(p2, { thin: true }); - } - lsz = data.z; - progress(index / total); - }, genso: true }); - - state.shadowTop = terrain[terrain.length - 1]; - state.center = tshadow[0].bounds.center(); - state.tshadow = tshadow; - state.terrain = terrain; - state.tslices = tslices; - state.skipTerrain = skipTerrain; - - // identify through holes - state.thruHoles = tshadow.map(p => p.inner || []).flat(); - } - } - - CAM.shadowAt = function(widget, z, ztop) { - let geo = widget.cache.geo || new THREE.BufferGeometry() - .setAttribute('position', new THREE.BufferAttribute(widget.vertices, 3)); - widget.cache.geo = geo; - let rad = (Math.PI / 180); - let deg = (180 / Math.PI); - let angle = rad * 1; - let thresh = -Math.sin(angle); - let found = []; - let { position } = geo.attributes; - let { itemSize, count, array } = position; - for (let i = 0; i 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) { - // limit to selected faces over threshold - let norm = THREE.computeFaceNormal(a,b,c); - if (norm.z < thresh) { - continue; - } - found.push([a,b,c]); - continue; - } else { - // check faces straddling threshold - where = {under: [], over: [], on: []}; - } - if (where) { - let { checkOverUnderOn, intersectPoints } = self.kiri.cam_slicer; - 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[0], line[0], line[1]]); - found.push([where.over[1], line[0], line[1]]); - found.push([where.over[0], where.over[1], 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}); - } - } - continue; - } else { + 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) { + // limit to selected faces over threshold + let norm = THREE.computeFaceNormal(a,b,c); + if (norm.z < thresh) { continue; } found.push([a,b,c]); + continue; + } else { + // check faces straddling threshold + where = {under: [], over: [], on: []}; } - 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); - }); - return POLY.union(polys, 0.0001, true); - }; + if (where) { + let { checkOverUnderOn, intersectPoints } = self.kiri.cam_slicer; + 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[0], line[0], line[1]]); + found.push([where.over[1], line[0], line[1]]); + found.push([where.over[0], where.over[1], 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}); + } + } + continue; + } else { + continue; + } + found.push([a,b,c]); + } + 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); + }); + return POLY.union(polys, 0.0001, true); +}; - CAM.OPS = CamOp.MAP = { - "xray": OpXRay, - "shadow": OpShadow, - "level": OpLevel, - "rough": OpRough, - "outline": OpOutline, - "contour": OpContour, - "trace": OpTrace, - "drill": OpDrill, - "register": OpRegister, - "flip": CamOp - }; +CAM.OPS = CamOp.MAP = { + "xray": OpXRay, + "shadow": OpShadow, + "level": OpLevel, + "rough": OpRough, + "outline": OpOutline, + "contour": OpContour, + "trace": OpTrace, + "drill": OpDrill, + "register": OpRegister, + "flip": CamOp +}; -})(); +}); diff --git a/src/kiri-mode/cam/prepare.js b/src/kiri-mode/cam/prepare.js index 4285dd87..8e442e45 100644 --- a/src/kiri-mode/cam/prepare.js +++ b/src/kiri-mode/cam/prepare.js @@ -2,553 +2,556 @@ "use strict"; -(function() { +// dep: geo.paths +// dep: geo.point +// dep: geo.polygons +gapp.register("kiri-mode.cam.prepare", [], (root, exports) => { - let KIRI = self.kiri, - BASE = self.base, - POLY = BASE.polygons, - PATH = BASE.paths, - CAM = KIRI.driver.CAM, - { newPoint } = BASE, - { tip2tipEmit, poly2polyEmit } = PATH; +const { base, kiri } = root; +const { paths, polygons, newPoint } = base; +const { tip2tipEmit, poly2polyEmit } = paths; +const { driver } = kiri; +const { CAM } = driver; - /** - * DRIVER PRINT CONTRACT - * - * @param {Object} print state object - * @param {Function} update incremental callback - * @param {Number} [index] into widget array - * @param {Object} [firstPoint] starting point - */ - CAM.prepare = function(widgets, settings, update) { - widgets = widgets.filter(w => !w.track.ignore && !w.meta.disabled); +const POLY = polygons; - const count = widgets.length; - const weight = 1/count; - const print = self.worker.print = KIRI.newPrint(settings, widgets); - print.output = []; +/** + * DRIVER PRINT CONTRACT + * + * @param {Object} print state object + * @param {Function} update incremental callback + * @param {Number} [index] into widget array + * @param {Object} [firstPoint] starting point + */ +CAM.prepare = function(widgets, settings, update) { + widgets = widgets.filter(w => !w.track.ignore && !w.meta.disabled); - let point; - widgets.forEach((widget, index) => { - point = prepEach(widget, settings, print, point, (progress, msg) => { - update((index * weight + progress * weight) * 0.75, msg || "prepare"); - }); + const count = widgets.length; + const weight = 1/count; + const print = self.worker.print = kiri.newPrint(settings, widgets); + print.output = []; + + let point; + widgets.forEach((widget, index) => { + point = prepEach(widget, settings, print, point, (progress, msg) => { + update((index * weight + progress * weight) * 0.75, msg || "prepare"); }); + }); - print.render = KIRI.driver.FDM.prepareRender(print.output, (progress, layer) => { - update(0.75 + progress * 0.25, "render", layer); - }, { - thin: true, - print: 0, - move: 0x557799, - speed: false, - moves: true, - other: "moving", - action: "milling", - maxspeed: settings.process.camFastFeed || 6000 - }); + print.render = kiri.driver.FDM.prepareRender(print.output, (progress, layer) => { + update(0.75 + progress * 0.25, "render", layer); + }, { + thin: true, + print: 0, + move: 0x557799, + speed: false, + moves: true, + other: "moving", + action: "milling", + maxspeed: settings.process.camFastFeed || 6000 + }); - return print.render; - }; + return print.render; +}; - function prepEach(widget, settings, print, firstPoint, update) { +function prepEach(widget, settings, print, firstPoint, update) { - if (widget.camops.length === 0 || widget.meta.disabled) return; + if (widget.camops.length === 0 || widget.meta.disabled) return; - let device = settings.device, - process = settings.process, - stock = settings.stock || {}, - outer = settings.bounds || widget.getPositionBox(), - outerz = outer.max.z, - slices = widget.slices, - hasStock = stock.x && stock.y && stock.z && process.camStockOn, - startCenter = process.outputOriginCenter, - alignTop = settings.controller.alignTop, - zclear = (process.camZClearance || 1), - zmax_outer = hasStock ? stock.z + zclear : outerz + zclear, - wztop = widget.track.top, - ztOff = hasStock ? stock.z - wztop : 0, - bounds = widget.getBoundingBox(), - boundsz = bounds.max.z + ztOff, - zadd = hasStock ? stock.z - boundsz : alignTop ? outerz - boundsz : 0, - zmax = outerz + zclear, - wmpos = widget.track.pos, - wmx = wmpos.x, - wmy = wmpos.y, - originx = startCenter ? 0 : hasStock ? -stock.x / 2 : bounds.min.x, - originy = startCenter ? 0 : hasStock ? -stock.y / 2 : bounds.min.y, - origin = newPoint(originx + wmx, originy + wmy, zmax), - output = print.output, - easeDown = process.camEaseDown, - depthFirst = process.camDepthFirst, - tolerance = 0, - drillDown = 0, - drillLift = 0, - drillDwell = 0, - newOutput = print.output || [], - layerOut = [], - printPoint, - isNewMode, - tool, - toolType, - toolDiam, - toolDiamMove, - plungeRate = process.camFastFeedZ, - feedRate, - lastTool, - lastMode, - lastPoint, - nextIsMove = true, - synthPlunge = false, - spindle = 0, - spindleMax = device.spindleMax, - maxToolDiam = widget.maxToolDiam, - terrain = widget.terrain ? widget.terrain.map(data => { - return { - z: data.z, - tops: data.tops, - }; - }) : zmax; + let device = settings.device, + process = settings.process, + stock = settings.stock || {}, + outer = settings.bounds || widget.getPositionBox(), + outerz = outer.max.z, + slices = widget.slices, + hasStock = stock.x && stock.y && stock.z && process.camStockOn, + startCenter = process.outputOriginCenter, + alignTop = settings.controller.alignTop, + zclear = (process.camZClearance || 1), + zmax_outer = hasStock ? stock.z + zclear : outerz + zclear, + wztop = widget.track.top, + ztOff = hasStock ? stock.z - wztop : 0, + bounds = widget.getBoundingBox(), + boundsz = bounds.max.z + ztOff, + zadd = hasStock ? stock.z - boundsz : alignTop ? outerz - boundsz : 0, + zmax = outerz + zclear, + wmpos = widget.track.pos, + wmx = wmpos.x, + wmy = wmpos.y, + originx = startCenter ? 0 : hasStock ? -stock.x / 2 : bounds.min.x, + originy = startCenter ? 0 : hasStock ? -stock.y / 2 : bounds.min.y, + origin = newPoint(originx + wmx, originy + wmy, zmax), + output = print.output, + easeDown = process.camEaseDown, + depthFirst = process.camDepthFirst, + tolerance = 0, + drillDown = 0, + drillLift = 0, + drillDwell = 0, + newOutput = print.output || [], + layerOut = [], + printPoint, + isNewMode, + tool, + toolType, + toolDiam, + toolDiamMove, + plungeRate = process.camFastFeedZ, + feedRate, + lastTool, + lastMode, + lastPoint, + nextIsMove = true, + synthPlunge = false, + spindle = 0, + spindleMax = device.spindleMax, + maxToolDiam = widget.maxToolDiam, + terrain = widget.terrain ? widget.terrain.map(data => { + return { + z: data.z, + tops: data.tops, + }; + }) : zmax; - function newLayer() { - if (layerOut.length < 2) { - return; - } - newOutput.push(layerOut); - layerOut = []; - layerOut.mode = lastMode; - layerOut.spindle = spindle; + function newLayer() { + if (layerOut.length < 2) { + return; } + newOutput.push(layerOut); + layerOut = []; + layerOut.mode = lastMode; + layerOut.spindle = spindle; + } - // non-zero means contouring - function setTolerance(dist) { - tolerance = dist; + // non-zero means contouring + function setTolerance(dist) { + tolerance = dist; + } + + function setPrintPoint(point) { + printPoint = point; + } + + function setSpindle(speed) { + spindle = Math.min(speed, spindleMax); + } + + function setTool(toolID, feed, plunge) { + if (toolID !== lastTool) { + tool = new CAM.Tool(settings, toolID); + toolType = tool.getType(); + toolDiam = tool.fluteDiameter(); + toolDiamMove = toolType === 'endmill' ? toolDiam : tolerance * 2; + lastTool = toolID; } + feedRate = feed || feedRate; + plungeRate = plunge || plungeRate || feedRate; + } - function setPrintPoint(point) { - printPoint = point; - } + function setDrill(down, lift, dwell) { + drillDown = down; + drillLift = lift; + drillDwell = dwell; + } - function setSpindle(speed) { - spindle = Math.min(speed, spindleMax); - } + function emitDrills(polys) { + polys = polys.slice(); + for (;;) { + let closestDist = Infinity, + closestI, + closest = null, + dist; - function setTool(toolID, feed, plunge) { - if (toolID !== lastTool) { - tool = new CAM.Tool(settings, toolID); - toolType = tool.getType(); - toolDiam = tool.fluteDiameter(); - toolDiamMove = toolType === 'endmill' ? toolDiam : tolerance * 2; - lastTool = toolID; - } - feedRate = feed || feedRate; - plungeRate = plunge || plungeRate || feedRate; - } - - function setDrill(down, lift, dwell) { - drillDown = down; - drillLift = lift; - drillDwell = dwell; - } - - function emitDrills(polys) { - polys = polys.slice(); - for (;;) { - let closestDist = Infinity, - closestI, - closest = null, - dist; - - for (let i=0; i down * 2) { - points.push(point.clone()); - point.z -= down; - remain -= down; - } else if (remain < down) { - points.push(point.clone()); - point.z -= remain; - points.push(point.clone()); - break; - } else { - points.push(point.clone()); - point.z -= remain / 2; - points.push(point.clone()); - point.z -= remain / 2; - points.push(point.clone()); - break; - } - } - camOut(point.clone().setZ(zmax)); - points.forEach(function(point, index) { - camOut(point, 1); - if (index > 0 && index < points.length - 1) { - if (dwell) camDwell(dwell); - if (lift) camOut(point.clone().setZ(point.z + lift), 0); - } - }) - camOut(point.clone().setZ(zmax)); - newLayer(); - } - - /** - * @param {Point} point - * @param {number} emit (0=move, !0=filament emit/laser on/cut mode) - * @param {number} [speed] speed - * @param {number} [tool] tool - */ - function layerPush(point, emit, speed, tool) { - layerOut.mode = lastMode; - print.addOutput(layerOut, point, emit, speed, tool); - return point; - } - - function camDwell(time) { - layerPush( - null, - 0, - time, - tool.getNumber() - ); - } - - function camOut(point, cut, moveLen = toolDiamMove) { - point = point.clone(); - point.x += wmx; - point.y += wmy; - point.z += zadd; - - if (nextIsMove) { - cut = 0; - nextIsMove = false; - } - - let rate = feedRate; - - // before first point, move cutting head to point above it - // then set that new point as the lastPoint - if (!lastPoint) { - let above = point.clone().setZ(zmax + zadd + ztOff); - lastPoint = layerPush(above, 0, 0, tool.getNumber()); - } - - // measure deltas to last point in XY and Z - let deltaXY = lastPoint.distTo2D(point), - deltaZ = point.z - lastPoint.z, - absDeltaZ = Math.abs(deltaZ), - isMove = !cut; - - // drop points too close together - if (deltaXY < 0.001 && point.z === lastPoint.z) { - // console.trace(["drop dup",lastPoint,point]); - return; - } - - // convert short planar moves to cuts in some cases - if (isMove && deltaXY <= moveLen) { - let iscontour = tolerance > 0; - let isflat = absDeltaZ < 0.001; - // restrict this to contouring - if (isflat || (iscontour && absDeltaZ <= tolerance)) { - cut = 1; - isMove = false; - } else if (deltaZ <= -tolerance) { - // move over before descending - layerPush(point.clone().setZ(lastPoint.z), 0, 0, tool.getNumber()); - // new pos for plunge calc - deltaXY = 0; - } - } else if (isMove) { - // for longer moves, check the terrain to see if we need to go up and over - const bigXY = (deltaXY > moveLen); - const bigZ = (deltaZ > toolDiam/2 && deltaXY > tolerance); - const midZ = (absDeltaZ >= tolerance); - if ((bigXY || bigZ) && midZ) { - let maxz = getZClearPath( - terrain, - lastPoint.x - wmx, - lastPoint.y - wmy, - point.x - wmx, - point.y - wmy, - Math.max(point.z, lastPoint.z), - zadd, - maxToolDiam/2, - zclear - ), - mustGoUp = Math.max(maxz - point.z, maxz - lastPoint.z) >= tolerance, - clearz = maxz; - // up if any point between higher than start/outline, go up first - if (mustGoUp) { - layerPush(lastPoint.clone().setZ(clearz), 0, 0, tool.getNumber()); - } - // move to point above target point - if (mustGoUp || point.z < maxz) { - layerPush(point.clone().setZ(clearz), 0, 0, tool.getNumber()); - // new pos for plunge calc - deltaXY = 0; - } + for (let i=0; i tolerance) { - // use modifier to speed up long XY move plunge rates - rate = Math.round(plungeRate + ((feedRate - plungeRate) * modifier)); - } else { - rate = plungeRate; - } - } - - // todo synthesize move speed from feed / plunge accordingly - layerOut.mode = lastMode; - layerOut.spindle = spindle; - lastPoint = layerPush( - point, - cut ? 1 : 0, - rate, - tool.getNumber() - ); + if (!closest) return; + polys[closestI] = null; + printPoint = closest.first(); + emitDrill(closest, drillDown, drillLift, drillDwell); } + } - // coming from a previous widget, use previous last point - lastPoint = firstPoint; - - // make top start offset configurable - printPoint = firstPoint || origin; - - // accumulated data for depth-first optimiztions - let depthData = { - rough: [], - outline: [], - roughDiam: 0, - outlineDiam: 0, - contourx: [], - contoury: [], - trace: [], - drill: [], - layer: 0, - }; - - let ops = { - setTool, - setDrill, - setSpindle, - setTolerance, - setPrintPoint, - printPoint, - newLayer, - camOut, - polyEmit, - poly2polyEmit, - tip2tipEmit, - depthRoughPath, - depthOutlinePath, - emitDrills, - emitTrace, - bounds, - zmax, - lastPoint: () => { return lastPoint } - }; - - let opSum = 0; - let opTot = widget.camops.map(op => op.weight()).reduce((a,v) => a + v); - - for (let op of widget.camops) { - setTolerance(0); - nextIsMove = true; - lastMode = op.op.type; - let weight = op.weight(); - op.prepare(ops, (progress, message) => { - update((opSum + (progress * weight)) / opTot, message || op.type(), message); - }); - opSum += weight; + function emitDrill(poly, down, lift, dwell) { + let remain = poly.first().z - poly.last().z, + points = [], + point = poly.first(); + if (down <= 0) { + down = remain; } - - function emitTrace(slice) { - let { tool, rate, plunge } = slice.camTrace; - setTool(tool, rate, plunge); - let traceTool = new CAM.Tool(settings, tool); - let traceToolDiam = traceTool.fluteDiameter(); - printPoint = poly2polyEmit(slice.camLines, printPoint, polyEmit, { swapdir: false }); - newLayer(); - } - - function polyEmit(poly, index, count, fromPoint) { - let last = null; - if (easeDown && poly.isClosed()) { - last = poly.forEachPointEaseDown(function(point, offset) { - camOut(point.clone(), offset > 0); - }, fromPoint); + for (;;) { + if (remain > down * 2) { + points.push(point.clone()); + point.z -= down; + remain -= down; + } else if (remain < down) { + points.push(point.clone()); + point.z -= remain; + points.push(point.clone()); + break; } else { - poly.forEachPoint(function(point, pidx, points, offset) { - last = point; - camOut(point.clone(), offset !== 0); - }, poly.isClosed(), index); - } - newLayer(); - return last; - } - - function depthRoughPath(start, depth, levels, tops, emitter, fit, ease) { - let level = levels[depth]; - if (!level) { - return start; - } - let ltops = tops[depth]; - let fitted = fit ? ltops.filter(poly => poly.isInside(fit, 0.01)) : ltops; - let ftops = fitted.filter(top => !top.level_emit); - if (ftops.length > 1) { - ftops = POLY.route(ftops, start); - } - - function roughTopEmit(top, index, count, start) { - top.level_emit = true; - let inside = level.filter(poly => poly.isInside(top)); - if (ease) { - start.z += ease; - } - start = poly2polyEmit(inside, start, emitter, { mark: "emark", perm: true, swapdir: false }); - if (ease) { - start.z += ease; - } - start = depthRoughPath(start, depth + 1, levels, tops, emitter, top, ease); - return start; - } - - // output fragments (due to tabs) last - let frag = ftops.filter(p => p.open); - let full = ftops.filter(p => !p.open); - - poly2polyEmit(full, start, roughTopEmit, { mark: "emark", swapdir: false }); - poly2polyEmit(frag, start, roughTopEmit, { mark: "emark", swapdir: false }); - - return start; - } - - function depthOutlinePath(start, depth, levels, radius, emitter, dir, ease) { - let bottm = depth < levels.length - 1 ? levels[levels.length - 1] : null; - let above = levels[depth-1]; - let level = levels[depth]; - if (!level) { - return start; - } - if (above) { - level = level.filter(lp => { - const conf = above.filter(ap => !ap.level_emit && lp.isNear(ap, radius, true)); - return conf.length === 0; - }); - } - // const thru = []; // match thru polys - level = level.filter(lp => { - if (lp.level_emit) { - return false; - } - // if (bottm && !clr) { - // const tm = bottm.filter(bp => lp.isEquivalent(bp)); - // thru.appendAll(tm); - // return tm.length === 0; - // } - return true; - }); - // limit level search to polys matching winding (inside vs outside) - level = level.filter(p => p.isClockwise() === dir); - // omit polys that match bottom level polys unless level above is cleared - start = poly2polyEmit(level, start, (poly, index, count, fromPoint) => { - poly.level_emit = true; - if (ease) { - fromPoint.z += ease; - } - fromPoint = polyEmit(poly, index, count, fromPoint); - if (ease) { - fromPoint.z += ease; - } - fromPoint = depthOutlinePath(fromPoint, depth + 1, levels, radius, emitter, dir, ease); - fromPoint = depthOutlinePath(fromPoint, depth + 1, levels, radius, emitter, !dir, ease); - return fromPoint; - }, { weight: false, swapdir: false }); - return start; - } - - // last layer/move is to zmax - // injected into the last layer generated - if (lastPoint && newOutput.length) - print.addOutput(newOutput[newOutput.length-1], printPoint = lastPoint.clone().setZ(zmax_outer), 0, 0, tool.getNumber()); - - // replace output single flattened layer with all points - print.output = newOutput; - return printPoint; - }; - - /** - * return tool Z clearance height for a line segment movement path - */ - function getZClearPath(terrain, x1, y1, x2, y2, z, zadd, off, over) { - // when terrain skipped, top + pass used - if (terrain > 0) { - return terrain; - } - let maxz = z; - let check = []; - for (let i=0; i p.intersections(p1, p2, true)).flat(); - if (int.length) { - maxz = Math.max(maxz, data.z + zadd + over); - continue; + camOut(point.clone().setZ(zmax)); + points.forEach(function(point, index) { + camOut(point, 1); + if (index > 0 && index < points.length - 1) { + if (dwell) camDwell(dwell); + if (lift) camOut(point.clone().setZ(point.z + lift), 0); } - let s1 = p1.slopeTo(p2).toUnit().normal(); - let s2 = p2.slopeTo(p1).toUnit().normal(); - let pa = p1.projectOnSlope(s1, off); - let pb = p2.projectOnSlope(s1, off); - int = data.tops.map(p => p.intersections(pa, pb, true)).flat(); - if (int.length) { - maxz = Math.max(maxz, data.z + zadd + over); - continue; - } - pa = p1.projectOnSlope(s2, off); - pb = p2.projectOnSlope(s2, off); - int = data.tops.map(p => p.intersections(pa, pb, true)).flat(); - if (int.length) { - maxz = Math.max(maxz, data.z + zadd + over); - continue; - } - } - return maxz; + }) + camOut(point.clone().setZ(zmax)); + newLayer(); } -})(); + /** + * @param {Point} point + * @param {number} emit (0=move, !0=filament emit/laser on/cut mode) + * @param {number} [speed] speed + * @param {number} [tool] tool + */ + function layerPush(point, emit, speed, tool) { + layerOut.mode = lastMode; + print.addOutput(layerOut, point, emit, speed, tool); + return point; + } + + function camDwell(time) { + layerPush( + null, + 0, + time, + tool.getNumber() + ); + } + + function camOut(point, cut, moveLen = toolDiamMove) { + point = point.clone(); + point.x += wmx; + point.y += wmy; + point.z += zadd; + + if (nextIsMove) { + cut = 0; + nextIsMove = false; + } + + let rate = feedRate; + + // before first point, move cutting head to point above it + // then set that new point as the lastPoint + if (!lastPoint) { + let above = point.clone().setZ(zmax + zadd + ztOff); + lastPoint = layerPush(above, 0, 0, tool.getNumber()); + } + + // measure deltas to last point in XY and Z + let deltaXY = lastPoint.distTo2D(point), + deltaZ = point.z - lastPoint.z, + absDeltaZ = Math.abs(deltaZ), + isMove = !cut; + + // drop points too close together + if (deltaXY < 0.001 && point.z === lastPoint.z) { + // console.trace(["drop dup",lastPoint,point]); + return; + } + + // convert short planar moves to cuts in some cases + if (isMove && deltaXY <= moveLen) { + let iscontour = tolerance > 0; + let isflat = absDeltaZ < 0.001; + // restrict this to contouring + if (isflat || (iscontour && absDeltaZ <= tolerance)) { + cut = 1; + isMove = false; + } else if (deltaZ <= -tolerance) { + // move over before descending + layerPush(point.clone().setZ(lastPoint.z), 0, 0, tool.getNumber()); + // new pos for plunge calc + deltaXY = 0; + } + } else if (isMove) { + // for longer moves, check the terrain to see if we need to go up and over + const bigXY = (deltaXY > moveLen); + const bigZ = (deltaZ > toolDiam/2 && deltaXY > tolerance); + const midZ = (absDeltaZ >= tolerance); + if ((bigXY || bigZ) && midZ) { + let maxz = getZClearPath( + terrain, + lastPoint.x - wmx, + lastPoint.y - wmy, + point.x - wmx, + point.y - wmy, + Math.max(point.z, lastPoint.z), + zadd, + maxToolDiam/2, + zclear + ), + mustGoUp = Math.max(maxz - point.z, maxz - lastPoint.z) >= tolerance, + clearz = maxz; + // up if any point between higher than start/outline, go up first + if (mustGoUp) { + layerPush(lastPoint.clone().setZ(clearz), 0, 0, tool.getNumber()); + } + // move to point above target point + if (mustGoUp || point.z < maxz) { + layerPush(point.clone().setZ(clearz), 0, 0, tool.getNumber()); + // new pos for plunge calc + deltaXY = 0; + } + } + } + + // set new plunge rate + if (deltaZ < -tolerance) { + let threshold = Math.min(deltaXY / 2, absDeltaZ), + modifier = threshold / absDeltaZ; + if (synthPlunge && threshold && modifier && deltaXY > tolerance) { + // use modifier to speed up long XY move plunge rates + rate = Math.round(plungeRate + ((feedRate - plungeRate) * modifier)); + } else { + rate = plungeRate; + } + } + + // todo synthesize move speed from feed / plunge accordingly + layerOut.mode = lastMode; + layerOut.spindle = spindle; + lastPoint = layerPush( + point, + cut ? 1 : 0, + rate, + tool.getNumber() + ); + } + + // coming from a previous widget, use previous last point + lastPoint = firstPoint; + + // make top start offset configurable + printPoint = firstPoint || origin; + + // accumulated data for depth-first optimiztions + let depthData = { + rough: [], + outline: [], + roughDiam: 0, + outlineDiam: 0, + contourx: [], + contoury: [], + trace: [], + drill: [], + layer: 0, + }; + + let ops = { + setTool, + setDrill, + setSpindle, + setTolerance, + setPrintPoint, + printPoint, + newLayer, + camOut, + polyEmit, + poly2polyEmit, + tip2tipEmit, + depthRoughPath, + depthOutlinePath, + emitDrills, + emitTrace, + bounds, + zmax, + lastPoint: () => { return lastPoint } + }; + + let opSum = 0; + let opTot = widget.camops.map(op => op.weight()).reduce((a,v) => a + v); + + for (let op of widget.camops) { + setTolerance(0); + nextIsMove = true; + lastMode = op.op.type; + let weight = op.weight(); + op.prepare(ops, (progress, message) => { + update((opSum + (progress * weight)) / opTot, message || op.type(), message); + }); + opSum += weight; + } + + function emitTrace(slice) { + let { tool, rate, plunge } = slice.camTrace; + setTool(tool, rate, plunge); + let traceTool = new CAM.Tool(settings, tool); + let traceToolDiam = traceTool.fluteDiameter(); + printPoint = poly2polyEmit(slice.camLines, printPoint, polyEmit, { swapdir: false }); + newLayer(); + } + + function polyEmit(poly, index, count, fromPoint) { + let last = null; + if (easeDown && poly.isClosed()) { + last = poly.forEachPointEaseDown(function(point, offset) { + camOut(point.clone(), offset > 0); + }, fromPoint); + } else { + poly.forEachPoint(function(point, pidx, points, offset) { + last = point; + camOut(point.clone(), offset !== 0); + }, poly.isClosed(), index); + } + newLayer(); + return last; + } + + function depthRoughPath(start, depth, levels, tops, emitter, fit, ease) { + let level = levels[depth]; + if (!level) { + return start; + } + let ltops = tops[depth]; + let fitted = fit ? ltops.filter(poly => poly.isInside(fit, 0.01)) : ltops; + let ftops = fitted.filter(top => !top.level_emit); + if (ftops.length > 1) { + ftops = POLY.route(ftops, start); + } + + function roughTopEmit(top, index, count, start) { + top.level_emit = true; + let inside = level.filter(poly => poly.isInside(top)); + if (ease) { + start.z += ease; + } + start = poly2polyEmit(inside, start, emitter, { mark: "emark", perm: true, swapdir: false }); + if (ease) { + start.z += ease; + } + start = depthRoughPath(start, depth + 1, levels, tops, emitter, top, ease); + return start; + } + + // output fragments (due to tabs) last + let frag = ftops.filter(p => p.open); + let full = ftops.filter(p => !p.open); + + poly2polyEmit(full, start, roughTopEmit, { mark: "emark", swapdir: false }); + poly2polyEmit(frag, start, roughTopEmit, { mark: "emark", swapdir: false }); + + return start; + } + + function depthOutlinePath(start, depth, levels, radius, emitter, dir, ease) { + let bottm = depth < levels.length - 1 ? levels[levels.length - 1] : null; + let above = levels[depth-1]; + let level = levels[depth]; + if (!level) { + return start; + } + if (above) { + level = level.filter(lp => { + const conf = above.filter(ap => !ap.level_emit && lp.isNear(ap, radius, true)); + return conf.length === 0; + }); + } + // const thru = []; // match thru polys + level = level.filter(lp => { + if (lp.level_emit) { + return false; + } + // if (bottm && !clr) { + // const tm = bottm.filter(bp => lp.isEquivalent(bp)); + // thru.appendAll(tm); + // return tm.length === 0; + // } + return true; + }); + // limit level search to polys matching winding (inside vs outside) + level = level.filter(p => p.isClockwise() === dir); + // omit polys that match bottom level polys unless level above is cleared + start = poly2polyEmit(level, start, (poly, index, count, fromPoint) => { + poly.level_emit = true; + if (ease) { + fromPoint.z += ease; + } + fromPoint = polyEmit(poly, index, count, fromPoint); + if (ease) { + fromPoint.z += ease; + } + fromPoint = depthOutlinePath(fromPoint, depth + 1, levels, radius, emitter, dir, ease); + fromPoint = depthOutlinePath(fromPoint, depth + 1, levels, radius, emitter, !dir, ease); + return fromPoint; + }, { weight: false, swapdir: false }); + return start; + } + + // last layer/move is to zmax + // injected into the last layer generated + if (lastPoint && newOutput.length) + print.addOutput(newOutput[newOutput.length-1], printPoint = lastPoint.clone().setZ(zmax_outer), 0, 0, tool.getNumber()); + + // replace output single flattened layer with all points + print.output = newOutput; + return printPoint; +}; + +/** + * return tool Z clearance height for a line segment movement path + */ +function getZClearPath(terrain, x1, y1, x2, y2, z, zadd, off, over) { + // when terrain skipped, top + pass used + if (terrain > 0) { + return terrain; + } + let maxz = z; + let check = []; + for (let i=0; i p.intersections(p1, p2, true)).flat(); + if (int.length) { + maxz = Math.max(maxz, data.z + zadd + over); + continue; + } + let s1 = p1.slopeTo(p2).toUnit().normal(); + let s2 = p2.slopeTo(p1).toUnit().normal(); + let pa = p1.projectOnSlope(s1, off); + let pb = p2.projectOnSlope(s1, off); + int = data.tops.map(p => p.intersections(pa, pb, true)).flat(); + if (int.length) { + maxz = Math.max(maxz, data.z + zadd + over); + continue; + } + pa = p1.projectOnSlope(s2, off); + pb = p2.projectOnSlope(s2, off); + int = data.tops.map(p => p.intersections(pa, pb, true)).flat(); + if (int.length) { + maxz = Math.max(maxz, data.z + zadd + over); + continue; + } + } + return maxz; +} + +}); diff --git a/src/kiri-mode/cam/slice.js b/src/kiri-mode/cam/slice.js index d6460e54..39f7807a 100644 --- a/src/kiri-mode/cam/slice.js +++ b/src/kiri-mode/cam/slice.js @@ -2,354 +2,355 @@ "use strict"; -(function() { +// dep: geo.point +// dep: geo.polygons +// dep: kiri.slice +gapp.register("kiri-mode.cam.slice", [], (root, exports) => { - let KIRI = self.kiri, - BASE = self.base, - UTIL = BASE.util, - CAM = KIRI.driver.CAM, - OPS = CAM.OPS, - PRO = CAM.process, - POLY = BASE.polygons, - newSlice = KIRI.newSlice, - newPoint = BASE.newPoint, - newPolygon = BASE.newPolygon; +const { base, kiri } = root; +const { driver, newSlice } = kiri; +const { polygons, newPoint, newPolygon } = base; +const { CAM } = driver; +const { OPS } = CAM; - /** - * DRIVER SLICE CONTRACT - * - * @param {Object} settings - * @param {Widget} widget - * @param {Function} output - */ - CAM.slice = function(settings, widget, onupdate, ondone) { - let mesh = widget.mesh, - proc = settings.process, - stock = settings.stock || {}, - hasStock = stock.x && stock.y && stock.z && proc.camStockOn, - camOps = widget.camops = [], - sliceAll = widget.slices = [], - bounds = widget.getBoundingBox(), - zBottom = proc.camZBottom, - zMin = Math.max(bounds.min.z, zBottom), - zMax = bounds.max.z, - zThru = zBottom === 0 ? (proc.camZThru || 0) : 0, - wztop = widget.track.top, - ztOff = hasStock ? stock.z - wztop : 0, - zTop = zMax + ztOff, - minToolDiam = Infinity, - maxToolDiam = -Infinity, - thruHoles, - dark = settings.controller.dark ? true : false, - color = dark ? 0xbbbbbb : 0, - tabs = widget.anno.tab, - unsafe = proc.camExpertFast, - units = settings.controller.units === 'in' ? 25.4 : 1; +const PRO = CAM.process; +const POLY = polygons; - if (tabs) { - // make tab polygons +/** + * DRIVER SLICE CONTRACT + * + * @param {Object} settings + * @param {Widget} widget + * @param {Function} output + */ +CAM.slice = function(settings, widget, onupdate, ondone) { + let mesh = widget.mesh, + proc = settings.process, + stock = settings.stock || {}, + hasStock = stock.x && stock.y && stock.z && proc.camStockOn, + camOps = widget.camops = [], + sliceAll = widget.slices = [], + bounds = widget.getBoundingBox(), + zBottom = proc.camZBottom, + zMin = Math.max(bounds.min.z, zBottom), + zMax = bounds.max.z, + zThru = zBottom === 0 ? (proc.camZThru || 0) : 0, + wztop = widget.track.top, + ztOff = hasStock ? stock.z - wztop : 0, + zTop = zMax + ztOff, + minToolDiam = Infinity, + maxToolDiam = -Infinity, + thruHoles, + dark = settings.controller.dark ? true : false, + color = dark ? 0xbbbbbb : 0, + tabs = widget.anno.tab, + unsafe = proc.camExpertFast, + units = settings.controller.units === 'in' ? 25.4 : 1; + + if (tabs) { + // make tab polygons + tabs.forEach(tab => { + let zero = newPoint(0,0,0); + let point = newPoint(tab.pos.x, tab.pos.y, tab.pos.z); + let poly = newPolygon().centerRectangle(zero, tab.dim.x, tab.dim.y); + let tslice = newSlice(0); + let m4 = new THREE.Matrix4().makeRotationFromQuaternion( + new THREE.Quaternion(tab.rot._x, tab.rot._y, tab.rot._z, tab.rot._w) + ); + poly.points = poly.points + .map(p => new THREE.Vector3(p.x,p.y,p.z).applyMatrix4(m4)) + .map(v => newPoint(v.x, v.y, v.z)); + poly.move(point); + tab.poly = poly; + // tslice.output().setLayer("tabs", 0xff0000).addPoly(poly); + // sliceAll.push(tslice); + }); + } + + if (unsafe) { + console.log("disabling overhang safeties"); + } + + if (!proc.ops || proc.ops.length === 0) { + throw 'no processes specified'; + } + + if (stock.x && stock.y && stock.z) { + if (stock.x + 0.00001 < bounds.max.x - bounds.min.x) { + throw 'stock X too small for part. disable stock or use offset stock'; + } + + if (stock.y + 0.00001 < bounds.max.y - bounds.min.y) { + throw 'stock Y too small for part. disable stock or use offset stock'; + } + + if (stock.z + 0.00001 < bounds.max.z - bounds.min.z) { + throw 'stock Z too small for part. disable stock or use offset stock'; + } + } + + if (zMin >= bounds.max.z) { + throw `invalid z bottom ${(zMin/units).round(3)} >= bounds z max ${(zMax/units).round(3)}`; + } + + let mark = Date.now(); + let slicer = new kiri.cam_slicer(widget.getPoints(), { + zlist: true, + zline: true + }); + + function updateToolDiams(toolDiam) { + minToolDiam = Math.min(minToolDiam, toolDiam); + maxToolDiam = Math.max(maxToolDiam, toolDiam); + } + + let state = { + settings, + widget, + bounds, + tabs, + cutTabs, + cutPolys, + healPolys, + slicer, + sliceAll, + updateToolDiams, + zBottom, + zThru, + ztOff, + zMax, + zTop, + unsafe, + color, + dark + }; + + let opList = [ + // silently preface op list with OpShadow + new CAM.OPS.shadow(state, { type: "shadow" }) + ]; + + if (false) { + opList.push(new CAM.OPS.xray(state, { type: "xray" })); + } + + let opSum = 0; + let opTot = opList.map(op => op.weight()).reduce((a,v) => a + v); + + for (let op of proc.ops.filter(op => !op.disabled)) { + let opfn = OPS[op.type]; + if (opfn) { + let opin = new opfn(state, op); + opList.push(opin); + opTot += opin.weight(); + } + } + + // give ops access to entire sequence + state.ops = opList; + + for (let op of opList) { + let weight = op.weight(); + op.slice((progress, message) => { + onupdate((opSum + (progress * weight)) / opTot, message || op.type()); + }); + camOps.push(op); + opSum += weight; + } + + // reindex + sliceAll.forEach((slice, index) => slice.index = index); + + // used in printSetup() + // used in CAM.prepare.getZClearPath() + // add tabs to terrain tops so moves avoid them + if (tabs) { + state.terrain.forEach(slab => { tabs.forEach(tab => { - let zero = newPoint(0,0,0); - let point = newPoint(tab.pos.x, tab.pos.y, tab.pos.z); - let poly = newPolygon().centerRectangle(zero, tab.dim.x, tab.dim.y); - let tslice = newSlice(0); - let m4 = new THREE.Matrix4().makeRotationFromQuaternion( - new THREE.Quaternion(tab.rot._x, tab.rot._y, tab.rot._z, tab.rot._w) - ); - poly.points = poly.points - .map(p => new THREE.Vector3(p.x,p.y,p.z).applyMatrix4(m4)) - .map(v => newPoint(v.x, v.y, v.z)); - poly.move(point); - tab.poly = poly; - // tslice.output().setLayer("tabs", 0xff0000).addPoly(poly); - // sliceAll.push(tslice); - }); - } - - if (unsafe) { - console.log("disabling overhang safeties"); - } - - if (!proc.ops || proc.ops.length === 0) { - throw 'no processes specified'; - } - - if (stock.x && stock.y && stock.z) { - if (stock.x + 0.00001 < bounds.max.x - bounds.min.x) { - throw 'stock X too small for part. disable stock or use offset stock'; - } - - if (stock.y + 0.00001 < bounds.max.y - bounds.min.y) { - throw 'stock Y too small for part. disable stock or use offset stock'; - } - - if (stock.z + 0.00001 < bounds.max.z - bounds.min.z) { - throw 'stock Z too small for part. disable stock or use offset stock'; - } - } - - if (zMin >= bounds.max.z) { - throw `invalid z bottom ${(zMin/units).round(3)} >= bounds z max ${(zMax/units).round(3)}`; - } - - let mark = Date.now(); - let slicer = new KIRI.cam_slicer(widget.getPoints(), { - zlist: true, - zline: true - }); - - function updateToolDiams(toolDiam) { - minToolDiam = Math.min(minToolDiam, toolDiam); - maxToolDiam = Math.max(maxToolDiam, toolDiam); - } - - let state = { - settings, - widget, - bounds, - tabs, - cutTabs, - cutPolys, - healPolys, - slicer, - sliceAll, - updateToolDiams, - zBottom, - zThru, - ztOff, - zMax, - zTop, - unsafe, - color, - dark - }; - - let opList = [ - // silently preface op list with OpShadow - new CAM.OPS.shadow(state, { type: "shadow" }) - ]; - - if (false) { - opList.push(new CAM.OPS.xray(state, { type: "xray" })); - } - - let opSum = 0; - let opTot = opList.map(op => op.weight()).reduce((a,v) => a + v); - - for (let op of proc.ops.filter(op => !op.disabled)) { - let opfn = OPS[op.type]; - if (opfn) { - let opin = new opfn(state, op); - opList.push(opin); - opTot += opin.weight(); - } - } - - // give ops access to entire sequence - state.ops = opList; - - for (let op of opList) { - let weight = op.weight(); - op.slice((progress, message) => { - onupdate((opSum + (progress * weight)) / opTot, message || op.type()); - }); - camOps.push(op); - opSum += weight; - } - - // reindex - sliceAll.forEach((slice, index) => slice.index = index); - - // used in printSetup() - // used in CAM.prepare.getZClearPath() - // add tabs to terrain tops so moves avoid them - if (tabs) { - state.terrain.forEach(slab => { - tabs.forEach(tab => { - if (tab.pos.z + tab.dim.z/2 >= slab.z) { - let all = [...slab.tops, tab.poly]; - slab.tops = POLY.union(all, 0, true); - // slab.slice.output() - // .setLayer("debug-tabs", {line: 0x880088, thin: true }) - // .addPolys(POLY.setZ(slab.tops.clone(true), slab.z), { thin: true }); - } - }); - }); - } - - // add shadow perimeter to terrain to catch outside moves off part - let tabpoly = tabs ? tabs.map(tab => tab.poly) : []; - let allpoly = POLY.union([...state.shadowTop.tops, ...tabpoly, ...state.shadowTop.slice.shadow], 0, true); - let shadowOff = maxToolDiam < 0 ? allpoly : - POLY.offset(allpoly, [minToolDiam/2,maxToolDiam/2], { count: 2, flat: true }); - state.terrain.forEach(level => level.tops.appendAll(shadowOff)); - - widget.terrain = state.skipTerrain ? null : state.terrain; - widget.minToolDiam = minToolDiam; - widget.maxToolDiam = maxToolDiam; - - ondone(); - }; - - CAM.addDogbones = function(poly, dist, reverse) { - if (Array.isArray(poly)) { - return poly.forEach(p => CAM.addDogbones(p, dist)); - } - if (poly.open) return; - let isCW = poly.isClockwise(); - if (reverse || poly.parent) isCW = !isCW; - let oldpts = poly.points.slice(); - let lastpt = oldpts[oldpts.length - 1]; - let lastsl = lastpt.slopeTo(oldpts[0]).toUnit(); - let newpts = [ ]; - for (let i=0; i 45) { - let newa = BASE.newSlopeFromAngle(lastsl.angle + bdiff); - newpts.push(lastpt.projectOnSlope(newa, dist)); - newpts.push(lastpt.clone()); - } else if (!isCW && adiff < -45) { - let newa = BASE.newSlopeFromAngle(lastsl.angle - bdiff); - newpts.push(lastpt.projectOnSlope(newa, dist)); - newpts.push(lastpt.clone()); - } - lastsl = nextsl; - lastpt = nextpt; - if (i < oldpts.length) { - newpts.push(nextpt); - } - } - poly.points = newpts; - if (poly.inner) { - CAM.addDogbones(poly.inner, dist, true); - } - }; - - CAM.traces = function(settings, widget, single) { - if (widget.traces && widget.trace_single === single) { - // do no work if cached - return false; - } - let slicer = new KIRI.cam_slicer(widget.getPoints(), { - zlist: true, - zline: true - }); - let indices = [...new Set(Object.keys(slicer.zFlat) - .map(kv => parseFloat(kv).round(5)) - .appendAll(Object.entries(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, index, total) => { - 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()); - } + if (tab.pos.z + tab.dim.z/2 >= slab.z) { + let all = [...slab.tops, tab.poly]; + slab.tops = POLY.union(all, 0, true); + // slab.slice.output() + // .setLayer("debug-tabs", {line: 0x880088, thin: true }) + // .addPolys(POLY.setZ(slab.tops.clone(true), slab.z), { thin: true }); } - } else - BASE.polygons.flatten(data.tops,null,true).forEach(poly => { - poly.inner = null; - poly.parent = null; - let z = poly.getZ(); - for (let i=0, il=traces.length; i 0.01) { + }); + }); + } + + // add shadow perimeter to terrain to catch outside moves off part + let tabpoly = tabs ? tabs.map(tab => tab.poly) : []; + let allpoly = POLY.union([...state.shadowTop.tops, ...tabpoly, ...state.shadowTop.slice.shadow], 0, true); + let shadowOff = maxToolDiam < 0 ? allpoly : + POLY.offset(allpoly, [minToolDiam/2,maxToolDiam/2], { count: 2, flat: true }); + state.terrain.forEach(level => level.tops.appendAll(shadowOff)); + + widget.terrain = state.skipTerrain ? null : state.terrain; + widget.minToolDiam = minToolDiam; + widget.maxToolDiam = maxToolDiam; + + ondone(); +}; + +CAM.addDogbones = function(poly, dist, reverse) { + if (Array.isArray(poly)) { + return poly.forEach(p => CAM.addDogbones(p, dist)); + } + if (poly.open) return; + let isCW = poly.isClockwise(); + if (reverse || poly.parent) isCW = !isCW; + let oldpts = poly.points.slice(); + let lastpt = oldpts[oldpts.length - 1]; + let lastsl = lastpt.slopeTo(oldpts[0]).toUnit(); + let newpts = [ ]; + for (let i=0; i 45) { + let newa = base.newSlopeFromAngle(lastsl.angle + bdiff); + newpts.push(lastpt.projectOnSlope(newa, dist)); + newpts.push(lastpt.clone()); + } else if (!isCW && adiff < -45) { + let newa = base.newSlopeFromAngle(lastsl.angle - bdiff); + newpts.push(lastpt.projectOnSlope(newa, dist)); + newpts.push(lastpt.clone()); + } + lastsl = nextsl; + lastpt = nextpt; + if (i < oldpts.length) { + newpts.push(nextpt); + } + } + poly.points = newpts; + if (poly.inner) { + CAM.addDogbones(poly.inner, dist, true); + } +}; + +CAM.traces = function(settings, widget, single) { + if (widget.traces && widget.trace_single === single) { + // do no work if cached + return false; + } + let slicer = new kiri.cam_slicer(widget.getPoints(), { + zlist: true, + zline: true + }); + let indices = [...new Set(Object.keys(slicer.zFlat) + .map(kv => parseFloat(kv).round(5)) + .appendAll(Object.entries(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, index, total) => { + 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,null,true).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)) { + return; + } + } + traces.push(poly); + }); + }; + let opts = { each: oneach, over: false, flatoff: 0, edges: true, openok: true }; + slicer.slice(indices, opts); + opts.over = true; + slicer.slice(indices, opts); + widget.traces = traces; + widget.trace_single = single; + return true; +}; + +function cutTabs(tabs, offset, z, inter) { + tabs = tabs.filter(tab => z < tab.pos.z + tab.dim.z/2).map(tab => tab.off).flat(); + return cutPolys(tabs, offset, z, false); +} + +function cutPolys(polys, offset, z, inter) { + let noff = []; + offset.forEach(op => noff.appendAll( op.cut(POLY.union(polys, 0, true), inter) )); + return healPolys(noff); +} + +function healPolys(noff) { + if (noff.length > 1) { + let heal = 0; + // heal/rejoin open segments that share endpoints + outer: for(;; heal++) { + let ntmp = noff, tlen = ntmp.length; + for (let i=0; i 0.01) { continue; } - // do not add duplicates - if (traces[i].isEquivalent(poly)) { - return; + if (!(s1.open && s2.open)) continue; + if (s1.last().isMergable2D(s2.first())) { + s1.addPoints(s2.points.slice(1)); + ntmp[j] = null; + continue outer; + } + if (s2.last().isMergable2D(s1.first())) { + s2.addPoints(s1.points.slice(1)); + ntmp[i] = null; + continue outer; + } + if (s1.first().isMergable2D(s2.first())) { + s1.reverse(); + s1.addPoints(s2.points.slice(1)); + ntmp[j] = null; + continue outer; + } + if (s1.last().isMergable2D(s2.last())) { + s2.reverse(); + s1.addPoints(s2.points.slice(1)); + ntmp[j] = null; + continue outer; } } - traces.push(poly); - }); - }; - let opts = { each: oneach, over: false, flatoff: 0, edges: true, openok: true }; - slicer.slice(indices, opts); - opts.over = true; - slicer.slice(indices, opts); - widget.traces = traces; - widget.trace_single = single; - return true; - }; - - function cutTabs(tabs, offset, z, inter) { - tabs = tabs.filter(tab => z < tab.pos.z + tab.dim.z/2).map(tab => tab.off).flat(); - return cutPolys(tabs, offset, z, false); - } - - function cutPolys(polys, offset, z, inter) { - let noff = []; - offset.forEach(op => noff.appendAll( op.cut(POLY.union(polys, 0, true), inter) )); - return healPolys(noff); - } - - function healPolys(noff) { - if (noff.length > 1) { - let heal = 0; - // heal/rejoin open segments that share endpoints - outer: for(;; heal++) { - let ntmp = noff, tlen = ntmp.length; - for (let i=0; i 0.01) { - continue; - } - if (!(s1.open && s2.open)) continue; - if (s1.last().isMergable2D(s2.first())) { - s1.addPoints(s2.points.slice(1)); - ntmp[j] = null; - continue outer; - } - if (s2.last().isMergable2D(s1.first())) { - s2.addPoints(s1.points.slice(1)); - ntmp[i] = null; - continue outer; - } - if (s1.first().isMergable2D(s2.first())) { - s1.reverse(); - s1.addPoints(s2.points.slice(1)); - ntmp[j] = null; - continue outer; - } - if (s1.last().isMergable2D(s2.last())) { - s2.reverse(); - s1.addPoints(s2.points.slice(1)); - ntmp[j] = null; - continue outer; - } - } - } - break; } - if (heal > 0) { - // cull nulls - noff = noff.filter(o => o); - } - // close poly if head meets tail - for (let poly of noff) { - if (poly.open && poly.first().isMergable2D(poly.last())) { - poly.points.pop(); - poly.open = false; - } + break; + } + if (heal > 0) { + // cull nulls + noff = noff.filter(o => o); + } + // close poly if head meets tail + for (let poly of noff) { + if (poly.open && poly.first().isMergable2D(poly.last())) { + poly.points.pop(); + poly.open = false; } } - return noff; } + return noff; +} -})(); +}); diff --git a/src/kiri-mode/cam/slicer.js b/src/kiri-mode/cam/slicer.js index ee901f57..859d325f 100644 --- a/src/kiri-mode/cam/slicer.js +++ b/src/kiri-mode/cam/slicer.js @@ -5,846 +5,844 @@ /** * Slicing engine used by CAM */ -(function() { +gapp.register("kiri-mode.cam.slicer", [], (root, exports) => { - let KIRI = self.kiri, - BASE = self.base, - CONF = BASE.config, - UTIL = BASE.util, - POLY = BASE.polygons, - newSlice = KIRI.newSlice, - newOrderedLine = BASE.newOrderedLine; +const { base, kiri } = root; +const { config, util, polygons, newOrderedLine } = base; +const { newSlice } = kiri; - class Slicer { - constructor(points, options) { - this.options = {}; - if (points) { - this.setPoints(points, options); +const POLY = polygons; + +class Slicer { + constructor(points, options) { + this.options = {}; + if (points) { + this.setPoints(points, options); + } + } + + // notopok = when genso set, allow empty top array + // emptyok = allow empty slices + // openok = allow open tops + // swapX = swap X/Z + // swapY = sawp Y/Z + // zList = generate list of z vertices + // zline = generate list of z vertices with coplanar lines + // trace = find z coplanar trace lines + // flatoff = amount to offset z when slicing on detected flats + // genso = generate a slice object with tops + // each = call for each slice generated from an interval + setOptions(options) { + Object.assign(this.options, options || {}); + return this.options; + } + + setPoints(points, options) { + this.bounds = null; + this.points = this.swap(points, options); + this.zFlat = {}; // accumulated flat area at z height + this.zLine = {}; // count of z coplanar lines + this.zList = {}; // count of z values for auto slicing + this.zSum = 0; // used in bucketing calculations + return this + .computeBounds() + .computeFeatures() + .computeBuckets(); + } + + computeBounds() { + if (!this.bounds) { + this.bounds = new THREE.Box3(); + this.bounds.setFromPoints(this.points); + } + return this; + } + + // gather z-index stats + // these are used for auto-slicing in laser + // and to flats detection in CAM mode + computeFeatures(options) { + const opt = this.setOptions(options); + const points = this.points; + const bounds = this.bounds; + const zFlat = this.zFlat; + const zLine = this.zLine; + const zList = this.zList; + + function countZ(z) { + z = z.round(5); + zList[z] = (zList[z] || 0) + 1; + } + + for (let i = 0, il = points.length; i < il; ) { + let p1 = points[i++]; + let p2 = points[i++]; + let p3 = points[i++]; + // used in bucket calculations + this.zSum += (Math.abs(p1.z - p2.z) + Math.abs(p2.z - p3.z) + Math.abs(p3.z - p1.z)); + // count occurrences of z values for auto slicing + if (opt.zlist) { + countZ(p1.z); + countZ(p2.z); + countZ(p3.z); + } + // use co-flat and co-line detection to adjust slice Z + if (p1.z === p2.z && p2.z === p3.z) { + // detect zFlat faces to avoid slicing directly on them + let zkey = p1.z.toFixed(5), + area = Math.abs(util.area2(p1,p2,p3)) / 2; + if (!zFlat[zkey]) { + zFlat[zkey] = area; + } else { + zFlat[zkey] += area; + } + } else if (opt.zline) { + // detect zLine (curved region tops/bottoms) + // in cam used for ball and v mill tracing + if (p1.z === p2.z) { + let zkey = p1.z.toFixed(5); + let zval = zLine[zkey]; + zLine[zkey] = (zval || 0) + 1; + } + if (p2.z === p3.z) { + let zkey = p2.z.toFixed(5); + let zval = zLine[zkey]; + zLine[zkey] = (zval || 0) + 1; + } + if (p3.z === p1.z) { + let zkey = p3.z.toFixed(5); + let zval = zLine[zkey]; + zLine[zkey] = (zval || 0) + 1; + } } } - // notopok = when genso set, allow empty top array - // emptyok = allow empty slices - // openok = allow open tops - // swapX = swap X/Z - // swapY = sawp Y/Z - // zList = generate list of z vertices - // zline = generate list of z vertices with coplanar lines - // trace = find z coplanar trace lines - // flatoff = amount to offset z when slicing on detected flats - // genso = generate a slice object with tops - // each = call for each slice generated from an interval - setOptions(options) { - Object.assign(this.options, options || {}); - return this.options; - } + return this; + } - setPoints(points, options) { - this.bounds = null; - this.points = this.swap(points, options); - this.zFlat = {}; // accumulated flat area at z height - this.zLine = {}; // count of z coplanar lines - this.zList = {}; // count of z values for auto slicing - this.zSum = 0; // used in bucketing calculations - return this - .computeBounds() - .computeFeatures() - .computeBuckets(); - } + /** + * bucket polygons into z-bounded groups (inside or crossing) + * to reduce the search space in complex models + */ + computeBuckets() { + let zSum = this.zSum; + let zMax = this.bounds.max.z; + let points = this.points; + let bucketCount = Math.max(1, Math.ceil(zMax / (zSum / points.length)) - 1); + let zScale = this.zScale = 1 / (zMax / bucketCount); + let buckets = this.buckets = []; - computeBounds() { - if (!this.bounds) { - this.bounds = new THREE.Box3(); - this.bounds.setFromPoints(this.points); - } - return this; - } - - // gather z-index stats - // these are used for auto-slicing in laser - // and to flats detection in CAM mode - computeFeatures(options) { - const opt = this.setOptions(options); - const points = this.points; - const bounds = this.bounds; - const zFlat = this.zFlat; - const zLine = this.zLine; - const zList = this.zList; - - function countZ(z) { - z = z.round(5); - zList[z] = (zList[z] || 0) + 1; + if (bucketCount > 1) { + // create empty buckets + for (let i = 0; i <= bucketCount + 1; i++) { + buckets.push([]); } + // copy triples into all matching z-buckets for (let i = 0, il = points.length; i < il; ) { - let p1 = points[i++]; - let p2 = points[i++]; - let p3 = points[i++]; - // used in bucket calculations - this.zSum += (Math.abs(p1.z - p2.z) + Math.abs(p2.z - p3.z) + Math.abs(p3.z - p1.z)); - // count occurrences of z values for auto slicing - if (opt.zlist) { - countZ(p1.z); - countZ(p2.z); - countZ(p3.z); - } - // use co-flat and co-line detection to adjust slice Z - if (p1.z === p2.z && p2.z === p3.z) { - // detect zFlat faces to avoid slicing directly on them - let zkey = p1.z.toFixed(5), - area = Math.abs(UTIL.area2(p1,p2,p3)) / 2; - if (!zFlat[zkey]) { - zFlat[zkey] = area; - } else { - zFlat[zkey] += area; - } - } else if (opt.zline) { - // detect zLine (curved region tops/bottoms) - // in cam used for ball and v mill tracing - if (p1.z === p2.z) { - let zkey = p1.z.toFixed(5); - let zval = zLine[zkey]; - zLine[zkey] = (zval || 0) + 1; - } - if (p2.z === p3.z) { - let zkey = p2.z.toFixed(5); - let zval = zLine[zkey]; - zLine[zkey] = (zval || 0) + 1; - } - if (p3.z === p1.z) { - let zkey = p3.z.toFixed(5); - let zval = zLine[zkey]; - zLine[zkey] = (zval || 0) + 1; - } + let p1 = points[i++], + p2 = points[i++], + p3 = points[i++], + zm = Math.min(p1.z, p2.z, p3.z), + zM = Math.max(p1.z, p2.z, p3.z), + bm = Math.floor(zm * zScale), + bM = Math.ceil(zM * zScale); + for (let j = bm; j <= bM; j++) { + buckets[j].push(p1); + buckets[j].push(p2); + buckets[j].push(p3); } } + } + return this; + } - return this; + // slice through points at given Z and return polygons + slice(z, options, index, total, mark) { + const opt = this.setOptions(options); + + // if Z is supplied as an array, iterate and collect + if (Array.isArray(z)) { + const mark = util.time(); + const rarr = []; + z.forEach((zv,zi) => { + const data = this.slice(zv, opt, zi, z.length, mark); + if (data) { + rarr.push(data); + } + }); + return rarr; } - /** - * bucket polygons into z-bounded groups (inside or crossing) - * to reduce the search space in complex models - */ - computeBuckets() { - let zSum = this.zSum; - let zMax = this.bounds.max.z; - let points = this.points; - let bucketCount = Math.max(1, Math.ceil(zMax / (zSum / points.length)) - 1); - let zScale = this.zScale = 1 / (zMax / bucketCount); - let buckets = this.buckets = []; + let znorm = z.toFixed(5), + flatoff = util.numOrDefault(opt.flatoff, 0.01), + onflat = this.zFlat[znorm], + edges = opt.edges || false, + over = opt.over || false, + phash = {}, + lines = [], + zScale = this.zScale, + buckets = this.buckets, + bucket = buckets.length ? buckets[Math.floor(z * zScale)] : this.points; - if (bucketCount > 1) { - // create empty buckets - for (let i = 0; i <= bucketCount + 1; i++) { - buckets.push([]); - } - - // copy triples into all matching z-buckets - for (let i = 0, il = points.length; i < il; ) { - let p1 = points[i++], - p2 = points[i++], - p3 = points[i++], - zm = Math.min(p1.z, p2.z, p3.z), - zM = Math.max(p1.z, p2.z, p3.z), - bm = Math.floor(zm * zScale), - bM = Math.ceil(zM * zScale); - for (let j = bm; j <= bM; j++) { - buckets[j].push(p1); - buckets[j].push(p2); - buckets[j].push(p3); - } - } - } - return this; + // compensate by moving z by "flatoff" on flats + if (onflat) { + z += flatoff; } - // slice through points at given Z and return polygons - slice(z, options, index, total, mark) { - const opt = this.setOptions(options); - - // if Z is supplied as an array, iterate and collect - if (Array.isArray(z)) { - const mark = UTIL.time(); - const rarr = []; - z.forEach((zv,zi) => { - const data = this.slice(zv, opt, zi, z.length, mark); - if (data) { - rarr.push(data); - } - }); - return rarr; - } - - let znorm = z.toFixed(5), - flatoff = UTIL.numOrDefault(opt.flatoff, 0.01), - onflat = this.zFlat[znorm], - edges = opt.edges || false, - over = opt.over || false, - phash = {}, - lines = [], - zScale = this.zScale, - buckets = this.buckets, - bucket = buckets.length ? buckets[Math.floor(z * zScale)] : this.points; - - // compensate by moving z by "flatoff" on flats - if (onflat) { - z += flatoff; - } - - if (!bucket) { - console.log({no_bucket_for_z: z}); - return; - } - - // iterate over matching buckets for this z offset - for (let i = 0, il = bucket.length; i < il; ) { - let p1 = bucket[i++]; - let p2 = bucket[i++]; - let p3 = bucket[i++]; - let where = {under: [], over: [], on: []}; - checkOverUnderOn(p1, z, where); - checkOverUnderOn(p2, z, where); - checkOverUnderOn(p3, z, where); - if (where.under.length === 3 || where.over.length === 3) { - // does not intersect (all 3 above or below) - } else if (where.on.length === 2) { - // one side of triangle is on the Z plane and 3rd is below - // drop lines with 3rd above because that leads to ambiguities - // with complex nested polygons on flat surface - if ((over && where.over.length === 1) || (!over && where.under.length === 1)) { - lines.push(makeZLine(phash, where.on[0], where.on[1], false, true)); - } - } else if (where.on.length === 3) { - // triangle is coplanar with Z - // we drop these because this face is attached to 3 others - // that will satisfy the if above (line) with 2 points - } else if (where.under.length === 0 || where.over.length === 0) { - // does not intersect but one point is on the slice Z plane - } else if (!edges) { - // compute two point intersections and construct line - let line = intersectPoints(where.over, where.under, z); - if (line.length < 2 && where.on.length === 1) { - line.push(where.on[0]); - } - if (line.length === 2) { - lines.push(makeZLine(phash, line[0], line[1])); - } else { - console.log({msg: "invalid ips", line: line, where: where}); - } - } - } - - let retn = { z }; - - if (lines.length) { - const debug = false; - retn.lines = removeDuplicateLines(lines, debug); - let polys = connectLines(retn.lines, opt, debug); - retn.tops = POLY.nest(polys); - - if (opt.swapX || opt.swapY) { - this.unswap(opt.swapX, opt.swapY, retn.lines, retn.tops); - } - - if (opt.genso) { - retn.slice = newSlice(z).addTops(retn.tops); - retn.slice.lines = retn.lines; - retn.slice.groups = retn.tops; - } - } - - const haslines = lines.length || opt.emptyok; - const hastops = !opt.genso || opt.notopok || (retn.tops && retn.tops.length) || edges; - - if (opt.each && haslines && hastops) { - opt.each(retn, index, total, UTIL.time() - mark); - } - - return haslines && hastops ? retn : null; + if (!bucket) { + console.log({no_bucket_for_z: z}); + return; } - swap(points, options) { - const opt = this.setOptions(options); - - if (!(opt && (opt.swapX || opt.swapY))) { - return points; - } - - let btmp = new THREE.Box3(), - pref = {}, - cached; - - points = points.slice(); - btmp.setFromPoints(points); - if (opt.swapX) this.ox = -btmp.max.x; - if (opt.swapY) this.oy = -btmp.max.y; - - // array re-uses points so we need - // to be careful not to alter a point - // more than once - for (let p, index=0; index { - poly.swap(swapX, swapY); - poly.move(move); + polys.forEach(poly => { + poly.swap(swapX, swapY); + poly.move(move); + }); + } + + interval(step, options) { + let opt = options || {}, + bounds = this.bounds, + boff = opt.boff || opt.off || 0, // bottom offset + toff = opt.toff || opt.off || 0, // top offset + zmin = (opt.min || this.bounds.min.z) + boff, + zmax = (opt.max || this.bounds.max.z) - toff, + steps = (zmax - zmin) / step, + rem = steps % 1 != 0 ? 0 : 1, + count = Math.floor(steps) + rem, + array = []; + + if (opt.fit) { + count++; + step = (zmax - zmin) / count; + } + if (opt.down) { + for (let i=0; i { + z = parseFloat(z); + add.push(z + opt.off); + if (z > zmin) { + add.push(z - opt.off); + } + }); + // add over and under all flats by 'off' + array.appendAll(add).sort((a,b) => { + return opt.down ? b-a : a-b; }); } + // filter duplicate values + array = array.map(v => v.round(5)).filter((e,i,a) => i < 1 || a[i-1] !== a[i]); + return array.map(v => Math.abs(parseFloat(v.toFixed(5)))); + } +} - interval(step, options) { - let opt = options || {}, - bounds = this.bounds, - boff = opt.boff || opt.off || 0, // bottom offset - toff = opt.toff || opt.off || 0, // top offset - zmin = (opt.min || this.bounds.min.z) + boff, - zmax = (opt.max || this.bounds.max.z) - toff, - steps = (zmax - zmin) / step, - rem = steps % 1 != 0 ? 0 : 1, - count = Math.floor(steps) + rem, - array = []; +/** + * given a point, append to the correct + * 'where' objec tarray (on, over or under) + * + * @param {Point} p + * @param {number} z offset + * @param {Obejct} where + */ +function checkOverUnderOn(p, z, where) { + let delta = p.z - z; + if (Math.abs(delta) < config.precision_slice_z) { // on + where.on.push(p); + } else if (delta < 0) { // under + where.under.push(p); + } else { // over + where.over.push(p); + } +} - if (opt.fit) { - count++; - step = (zmax - zmin) / count; - } - if (opt.down) { - for (let i=0; i { - z = parseFloat(z); - add.push(z + opt.off); - if (z > zmin) { - add.push(z - opt.off); - } - }); - // add over and under all flats by 'off' - array.appendAll(add).sort((a,b) => { - return opt.down ? b-a : a-b; - }); - } - // filter duplicate values - array = array.map(v => v.round(5)).filter((e,i,a) => i < 1 || a[i-1] !== a[i]); - return array.map(v => Math.abs(parseFloat(v.toFixed(5)))); +/** + * Given a point over and under a z offset, calculate + * and return the intersection point on that z plane + * + * @param {Point} over + * @param {Point} under + * @param {number} z offset + * @returns {Point} intersection point + */ +function intersectPoints(over, under, z) { + let ip = []; + for (let i = 0; i < over.length; i++) { + for (let j = 0; j < under.length; j++) { + ip.push(over[i].intersectZ(under[j], z)); } } + return ip; +} + +/** + * Ensure points are unique with a cache/key algorithm + */ +function getCachedPoint(phash, p) { + let cached = phash[p.key]; + if (!cached) { + phash[p.key] = p; + return p; + } + return cached; +} + +/** + * Given two points and hints about their edges, + * return a new Line object with points sorted + * lexicographically by key. This allows for future + * line de-duplication and joins. + * + * @param {Object} phash + * @param {Point} p1 + * @param {Point} p2 + * @param {boolean} [coplanar] + * @param {boolean} [edge] + * @returns {Line} + */ +function makeZLine(phash, p1, p2, coplanar, edge) { + p1 = getCachedPoint(phash, p1); + p2 = getCachedPoint(phash, p2); + let line = newOrderedLine(p1,p2); + line.coplanar = coplanar || false; + line.edge = edge || false; + return line; +} + +/** + * Given an array of input lines (line soup), find the path through + * joining line ends that encompasses the greatest area without self + * interesection. Eliminate used points and repeat. Unjoined lines + * are permitted and handled after all other cases are handled. + * + * @param {Line[]} input + * @param {number} [index] + * @returns {Array} + */ +function connectLines(input, opt = {}, debug) { + // map points to all other points they're connected to + let config = base.config, + pmap = {}, + points = [], + output = [], + connect = [], + search = 1, + nextMod = 1, + bridge = config.bridgeLineGapDistance, + minPoly = opt.openok ? 2 : 3, + p1, p2; + + function cachedPoint(p) { + let cp = pmap[p.key]; + if (cp) return cp; + points.push(p); + pmap[p.key] = p; + p.pos = 0; + p.mod = nextMod++; // unique seq ID for points + p.toString = function() { return this.mod }; // point array concat + return p; + } + + function addConnected(p1, p2) { + if (!p1.group) p1.group = [ p2 ]; + else p1.group.push(p2); + } + + function sliceAtTerm(path, term) { + let idx, len = path.length; + for (idx = 0; idx < len-1; idx++) { + if (path[idx] === term) { + return path.slice(idx); + } + } + return path; + } /** - * given a point, append to the correct - * 'where' objec tarray (on, over or under) - * - * @param {Point} p - * @param {number} z offset - * @param {Obejct} where + * using minimal recursion, follow points through connected lines + * to form candidate output paths. */ - function checkOverUnderOn(p, z, where) { - let delta = p.z - z; - if (Math.abs(delta) < CONF.precision_slice_z) { // on - where.on.push(p); - } else if (delta < 0) { // under - where.under.push(p); - } else { // over - where.over.push(p); + function findPathsMinRecurse(point, path, paths, from) { + let stack = [ ]; + if (paths.length > 100000) { + console.log("excessive path options @ "+paths.length+" #"+input.length); + return; } - } + for (;;) { + stack.push(point); - /** - * Given a point over and under a z offset, calculate - * and return the intersection point on that z plane - * - * @param {Point} over - * @param {Point} under - * @param {number} z offset - * @returns {Point} intersection point - */ - function intersectPoints(over, under, z) { - let ip = []; - for (let i = 0; i < over.length; i++) { - for (let j = 0; j < under.length; j++) { - ip.push(over[i].intersectZ(under[j], z)); - } - } - return ip; - } + let last = point, + links = point.group; - /** - * Ensure points are unique with a cache/key algorithm - */ - function getCachedPoint(phash, p) { - let cached = phash[p.key]; - if (!cached) { - phash[p.key] = p; - return p; - } - return cached; - } - - /** - * Given two points and hints about their edges, - * return a new Line object with points sorted - * lexicographically by key. This allows for future - * line de-duplication and joins. - * - * @param {Object} phash - * @param {Point} p1 - * @param {Point} p2 - * @param {boolean} [coplanar] - * @param {boolean} [edge] - * @returns {Line} - */ - function makeZLine(phash, p1, p2, coplanar, edge) { - p1 = getCachedPoint(phash, p1); - p2 = getCachedPoint(phash, p2); - let line = newOrderedLine(p1,p2); - line.coplanar = coplanar || false; - line.edge = edge || false; - return line; - } - - /** - * Given an array of input lines (line soup), find the path through - * joining line ends that encompasses the greatest area without self - * interesection. Eliminate used points and repeat. Unjoined lines - * are permitted and handled after all other cases are handled. - * - * @param {Line[]} input - * @param {number} [index] - * @returns {Array} - */ - function connectLines(input, opt = {}, debug) { - // map points to all other points they're connected to - let CONF = BASE.config, - pmap = {}, - points = [], - output = [], - connect = [], - search = 1, - nextMod = 1, - bridge = CONF.bridgeLineGapDistance, - minPoly = opt.openok ? 2 : 3, - p1, p2; - - function cachedPoint(p) { - let cp = pmap[p.key]; - if (cp) return cp; - points.push(p); - pmap[p.key] = p; - p.pos = 0; - p.mod = nextMod++; // unique seq ID for points - p.toString = function() { return this.mod }; // point array concat - return p; - } - - function addConnected(p1, p2) { - if (!p1.group) p1.group = [ p2 ]; - else p1.group.push(p2); - } - - function sliceAtTerm(path, term) { - let idx, len = path.length; - for (idx = 0; idx < len-1; idx++) { - if (path[idx] === term) { - return path.slice(idx); - } - } - return path; - } - - /** - * using minimal recursion, follow points through connected lines - * to form candidate output paths. - */ - function findPathsMinRecurse(point, path, paths, from) { - let stack = [ ]; - if (paths.length > 100000) { - console.log("excessive path options @ "+paths.length+" #"+input.length); - return; - } - for (;;) { - stack.push(point); - - let last = point, - links = point.group; - - path.push(point); - // use del to mark traversed path - point.del = true; - // set so point isn't used in another polygon search - point.pos = search++; - // seed path with two points to prevent redundant opposing seeks - if (path.length === 1) { - from = point; - point = links[0]; - continue; - } - - if (links.length > 2) { - // TODO optimize when > 2 and limit to left-most and right-most branches - // for now, pursue all possible branches - links.forEach(function(nextp) { - // do not backtrack - if (nextp === from) { - return; - } - if (nextp.del) { - paths.push(sliceAtTerm(path,nextp)); - } else { - findPathsMinRecurse(nextp, path.slice(), paths, point); - } - }); - break; - } else { - point = links[0] === from ? links[1] : links[0]; - from = last; - // hit an open end - if (!point) { - path.open = true; - paths.push(path); - break; - } - // hit a point previously in the path (or start) - if (point.del) { - paths.push(sliceAtTerm(path,point)); - break; - } - } - } - - for (let i=0; i= minPoly) output.push(poly); - } - - // given an array of paths, emit longest to shortest - // eliminating points from the paths as they are emitted - // shorter paths any point eliminated are eliminated as candidates. - function emitLongestAsPolygon(paths) { - let longest = null, - emitted = 0, - closed = 0, - open = 0; - - paths.forEach(function(path) { - // use longest perimeter vs longest path? - if (!longest || path.length > longest.length) longest = path; - if (!path.open) closed++; else open++; - }); - - // it gets more complicated with multiple possible output paths - if (closed > 1 && open === 0) { - // add polygon to path (for area sorting) - paths.forEach(function(path) { path.poly = BASE.newPolygon().addPoints(path) }); - - // sort descending by area VS (length below -- better in most cases) - // paths.sort(function(a,b) { return b.poly.area() - a.poly.area() }); - - // sort descending by length - paths.sort(function(a,b) { return b.poly.length - a.poly.length }); - - // emit polygons largest to smallest - // omit polygon if it intersects previously emitted (has del points) - paths.forEach(function(path) { - if (path.length < minPoly) return; - let len = path.length, i; - for (i = 0; i < len; i++) if (path[i].del) return; - for (i = 0; i < len; i++) path[i].del = true; - emit(path.poly); - emitted++; - }); - } else { - if (longest.open) { - connect.push(longest); - } else { - emit(BASE.newPolygon().addPoints(longest)); - } - } - } - - if (debug) console.log('map', input); - - // create point map, unique point list and point group arrays - input.forEach(function(line) { - p1 = cachedPoint(line.p1.round(5)); - p2 = cachedPoint(line.p2.round(5)); - addConnected(p1,p2); - addConnected(p2,p1); - }); - - // first trace paths starting at dangling endpoinds (bad polygon soup) - points.forEach(function(point) { - // must not have been used and be a dangling end - if (point.pos === 0 && point.group.length === 1) { - let path = [], - paths = []; - findPathsMinRecurse(point, path, paths); - if (debug) console.log('dangle', {point, path, paths}); - if (paths.length > 0) emitLongestAsPolygon(paths); - } - }); - - // for each point, find longest path back to self - points.forEach(function(point) { - // must not have been used or be at a split - if (point.pos === 0 && point.group.length === 2) { - let path = [], - paths = []; - findPathsMinRecurse(point, path, paths); - if (paths.length > 0) emitLongestAsPolygon(paths); - } - }); - - // return true if points are deemed "close enough" close a polygon - function close(p1,p2) { - return p1.distToSq2D(p2) <= 0.01; - } - - // reconnect dangling/open polygons to closest endpoint - for (let i=0; i 2) { + // TODO optimize when > 2 and limit to left-most and right-most branches + // for now, pursue all possible branches + links.forEach(function(nextp) { + // do not backtrack + if (nextp === from) { + return; } - dist = last.distToSq2D(tmp[tmp.length-1]); - if (dist < closest.dist && dist <= bridge) { - closest = { - dist: dist, - array: tmp, - reverse: true - } + if (nextp.del) { + paths.push(sliceAtTerm(path,nextp)); + } else { + findPathsMinRecurse(nextp, path.slice(), paths, point); + } + }); + break; + } else { + point = links[0] === from ? links[1] : links[0]; + from = last; + // hit an open end + if (!point) { + path.open = true; + paths.push(path); + break; + } + // hit a point previously in the path (or start) + if (point.del) { + paths.push(sliceAtTerm(path,point)); + break; + } + } + } + + for (let i=0; i= minPoly) output.push(poly); + } + + // given an array of paths, emit longest to shortest + // eliminating points from the paths as they are emitted + // shorter paths any point eliminated are eliminated as candidates. + function emitLongestAsPolygon(paths) { + let longest = null, + emitted = 0, + closed = 0, + open = 0; + + paths.forEach(function(path) { + // use longest perimeter vs longest path? + if (!longest || path.length > longest.length) longest = path; + if (!path.open) closed++; else open++; + }); + + // it gets more complicated with multiple possible output paths + if (closed > 1 && open === 0) { + // add polygon to path (for area sorting) + paths.forEach(function(path) { path.poly = base.newPolygon().addPoints(path) }); + + // sort descending by area VS (length below -- better in most cases) + // paths.sort(function(a,b) { return b.poly.area() - a.poly.area() }); + + // sort descending by length + paths.sort(function(a,b) { return b.poly.length - a.poly.length }); + + // emit polygons largest to smallest + // omit polygon if it intersects previously emitted (has del points) + paths.forEach(function(path) { + if (path.length < minPoly) return; + let len = path.length, i; + for (i = 0; i < len; i++) if (path[i].del) return; + for (i = 0; i < len; i++) path[i].del = true; + emit(path.poly); + emitted++; + }); + } else { + if (longest.open) { + connect.push(longest); + } else { + emit(base.newPolygon().addPoints(longest)); + } + } + } + + if (debug) console.log('map', input); + + // create point map, unique point list and point group arrays + input.forEach(function(line) { + p1 = cachedPoint(line.p1.round(5)); + p2 = cachedPoint(line.p2.round(5)); + addConnected(p1,p2); + addConnected(p2,p1); + }); + + // first trace paths starting at dangling endpoinds (bad polygon soup) + points.forEach(function(point) { + // must not have been used and be a dangling end + if (point.pos === 0 && point.group.length === 1) { + let path = [], + paths = []; + findPathsMinRecurse(point, path, paths); + if (debug) console.log('dangle', {point, path, paths}); + if (paths.length > 0) emitLongestAsPolygon(paths); + } + }); + + // for each point, find longest path back to self + points.forEach(function(point) { + // must not have been used or be at a split + if (point.pos === 0 && point.group.length === 2) { + let path = [], + paths = []; + findPathsMinRecurse(point, path, paths); + if (paths.length > 0) emitLongestAsPolygon(paths); + } + }); + + // return true if points are deemed "close enough" close a polygon + function close(p1,p2) { + return p1.distToSq2D(p2) <= 0.01; + } + + // reconnect dangling/open polygons to closest endpoint + for (let i=0; i { - let KIRI = self.kiri, - BASE = self.base, - UTIL = BASE.util, - HPI = Math.PI/2, - CAM = KIRI.driver.CAM; +const { kiri } = root; +const { driver } = kiri; +const { CAM } = driver; - class Tool { - constructor(settings, id, number) { - if (number >= 0) { - this.tool = settings.tools.filter(tool => tool.number == number)[0]; - } else { - this.tool = settings.tools.filter(tool => tool.id == id)[0]; - } - if (!this.tool) { - this.tool = Object.assign({}, settings.tools[0]); - this.tool.number = number >= 0 ? number : this.tool.number; - this.tool.id = id >= 0 ? id : this.tool.id; - } +const HPI = Math.PI/2; + +class Tool { + constructor(settings, id, number) { + if (number >= 0) { + this.tool = settings.tools.filter(tool => tool.number == number)[0]; + } else { + this.tool = settings.tools.filter(tool => tool.id == id)[0]; } - - getID() { - return this.tool.id; - } - - getName() { - return this.tool.name; - } - - getType() { - return this.tool.type; - } - - getNumber() { - return this.tool.number; - } - - isMetric() { - return this.tool.metric; - } - - unitScale() { - return this.isMetric() ? 1 : 25.4; - } - - fluteLength() { - return this.unitScale() * this.tool.flute_len; - } - - fluteDiameter() { - return this.unitScale() * this.tool.flute_diam; - } - - tipDiameter() { - return this.unitScale() * this.tool.taper_tip; - } - - shaftLength() { - return this.unitScale() * this.tool.shaft_len; - } - - shaftDiameter() { - return this.unitScale() * this.tool.shaft_diam; - } - - generateProfile(resolution) { - // generate tool profile - let type = this.getType(), - ball = type === "ballmill", - taper = type === "tapermill", - tip_diameter = this.tipDiameter(), - shaft_offset = this.fluteLength(), - flute_diameter = this.fluteDiameter(), - shaft_diameter = this.shaftDiameter(), - shaft_radius = shaft_diameter / 2, - shaft_pix_float = shaft_diameter / resolution, - shaft_pix_int = Math.round(shaft_pix_float), - shaft_radius_pix_float = shaft_pix_float / 2, - flute_radius = flute_diameter / 2, - flute_pix_float = flute_diameter / resolution, - flute_radius_pix_float = flute_pix_float / 2, - tip_pix_float = tip_diameter / resolution, - tip_radius_pix_float = tip_pix_float / 2, - tip_max_radius_offset = flute_radius_pix_float - tip_radius_pix_float, - profile_pix_iter = shaft_pix_int + (1 - shaft_pix_int % 2), - toolCenter = (shaft_pix_int - (shaft_pix_int % 2)) / 2, - toolOffset = [], - larger_shaft = shaft_diameter - flute_diameter > 0.001, - rpixsq = flute_radius_pix_float * flute_radius_pix_float; - - // for each point in tool profile, check inside radius - for (let x = 0; x < profile_pix_iter; x++) { - for (let y = 0; y < profile_pix_iter; y++) { - let dx = x - toolCenter, - dy = y - toolCenter, - dist_from_center = Math.sqrt(dx * dx + dy * dy); - if (dist_from_center <= flute_radius_pix_float) { - // console.log({x,y,dx,dy,dist:dist_from_center,ln:dbl.length}) - // flute offset points - let z_offset = 0; - if (ball) { - let rd = dist_from_center * dist_from_center; - z_offset = Math.sqrt(rpixsq - rd) * resolution - flute_radius; - // z_offset = (1 - Math.cos((dist_from_center / flute_radius_pix_float) * HPI)) * -flute_radius; - } else if (taper && dist_from_center >= tip_radius_pix_float) { - z_offset = ((dist_from_center - tip_radius_pix_float) / tip_max_radius_offset) * -shaft_offset; - } - toolOffset.push(dx, dy, z_offset); - } else if (shaft_offset && larger_shaft && dist_from_center <= shaft_radius_pix_float) { - // shaft offset points - toolOffset.push(dx, dy, -shaft_offset); - } - } - } - - this.profile = toolOffset; - this.profileDim = { - size: shaft_diameter, - pix: profile_pix_iter + 2 - }; - return this; + if (!this.tool) { + this.tool = Object.assign({}, settings.tools[0]); + this.tool.number = number >= 0 ? number : this.tool.number; + this.tool.id = id >= 0 ? id : this.tool.id; } } - CAM.Tool = Tool; - CAM.getToolDiameter = function(settings, id) { - return new CAM.Tool(settings, id).fluteDiameter(); - }; + getID() { + return this.tool.id; + } -})(); + getName() { + return this.tool.name; + } + + getType() { + return this.tool.type; + } + + getNumber() { + return this.tool.number; + } + + isMetric() { + return this.tool.metric; + } + + unitScale() { + return this.isMetric() ? 1 : 25.4; + } + + fluteLength() { + return this.unitScale() * this.tool.flute_len; + } + + fluteDiameter() { + return this.unitScale() * this.tool.flute_diam; + } + + tipDiameter() { + return this.unitScale() * this.tool.taper_tip; + } + + shaftLength() { + return this.unitScale() * this.tool.shaft_len; + } + + shaftDiameter() { + return this.unitScale() * this.tool.shaft_diam; + } + + generateProfile(resolution) { + // generate tool profile + let type = this.getType(), + ball = type === "ballmill", + taper = type === "tapermill", + tip_diameter = this.tipDiameter(), + shaft_offset = this.fluteLength(), + flute_diameter = this.fluteDiameter(), + shaft_diameter = this.shaftDiameter(), + shaft_radius = shaft_diameter / 2, + shaft_pix_float = shaft_diameter / resolution, + shaft_pix_int = Math.round(shaft_pix_float), + shaft_radius_pix_float = shaft_pix_float / 2, + flute_radius = flute_diameter / 2, + flute_pix_float = flute_diameter / resolution, + flute_radius_pix_float = flute_pix_float / 2, + tip_pix_float = tip_diameter / resolution, + tip_radius_pix_float = tip_pix_float / 2, + tip_max_radius_offset = flute_radius_pix_float - tip_radius_pix_float, + profile_pix_iter = shaft_pix_int + (1 - shaft_pix_int % 2), + toolCenter = (shaft_pix_int - (shaft_pix_int % 2)) / 2, + toolOffset = [], + larger_shaft = shaft_diameter - flute_diameter > 0.001, + rpixsq = flute_radius_pix_float * flute_radius_pix_float; + + // for each point in tool profile, check inside radius + for (let x = 0; x < profile_pix_iter; x++) { + for (let y = 0; y < profile_pix_iter; y++) { + let dx = x - toolCenter, + dy = y - toolCenter, + dist_from_center = Math.sqrt(dx * dx + dy * dy); + if (dist_from_center <= flute_radius_pix_float) { + // console.log({x,y,dx,dy,dist:dist_from_center,ln:dbl.length}) + // flute offset points + let z_offset = 0; + if (ball) { + let rd = dist_from_center * dist_from_center; + z_offset = Math.sqrt(rpixsq - rd) * resolution - flute_radius; + // z_offset = (1 - Math.cos((dist_from_center / flute_radius_pix_float) * HPI)) * -flute_radius; + } else if (taper && dist_from_center >= tip_radius_pix_float) { + z_offset = ((dist_from_center - tip_radius_pix_float) / tip_max_radius_offset) * -shaft_offset; + } + toolOffset.push(dx, dy, z_offset); + } else if (shaft_offset && larger_shaft && dist_from_center <= shaft_radius_pix_float) { + // shaft offset points + toolOffset.push(dx, dy, -shaft_offset); + } + } + } + + this.profile = toolOffset; + this.profileDim = { + size: shaft_diameter, + pix: profile_pix_iter + 2 + }; + return this; + } +} + +CAM.Tool = Tool; + +CAM.getToolDiameter = function(settings, id) { + return new CAM.Tool(settings, id).fluteDiameter(); +}; + +}); diff --git a/src/kiri-mode/cam/topo.js b/src/kiri-mode/cam/topo.js index 019c22f1..8a1b3504 100644 --- a/src/kiri-mode/cam/topo.js +++ b/src/kiri-mode/cam/topo.js @@ -2,404 +2,401 @@ "use strict"; -(function() { +gapp.register("kiri-mode.cam.tools", [], (root, exports) => { - let KIRI = self.kiri, - BASE = self.base, - CAM = KIRI.driver.CAM, - PRO = CAM.process, - POLY = BASE.polygons, - RAD2DEG = 180 / Math.PI, - newSlice = KIRI.newSlice, - newLine = BASE.newLine, - newSlope = BASE.newSlope, - newPoint = BASE.newPoint, - newPolygon = BASE.newPolygon; +const { base, kiri } = root; +const { driver, newSlice } = kiri; +const { CAM } = driver; +const { polygons, newLine, newSlope, newPoint, newPolygon } = base; - class Topo { - constructor(opt = {}) { - let { state, contour, onupdate, ondone } = opt; - let { widget, settings, tshadow, center, tabs, color } = opt.state; +const PRO = CAM.process; +const POLY = polygons; +const RAD2DEG = 180 / Math.PI; - let density = parseInt(settings.controller.animesh || 100) * 2500, - axis = contour.axis.toLowerCase(), - contourX = axis === "x", - contourY = axis === "y", - bounds = widget.getBoundingBox().clone(), - tolerance = contour.tolerance, - proc = settings.process, - shadow = tshadow, - minX = bounds.min.x, - maxX = bounds.max.x, - minY = bounds.min.y, - maxY = bounds.max.y, - zBottom = proc.camZBottom, - zMin = Math.max(bounds.min.z, zBottom) + 0.0001, - boundsX = maxX - minX, - boundsY = maxY - minY, - inside = contour.inside, - resolution = tolerance ? tolerance : 1/Math.sqrt(density/(boundsX * boundsY)), - tool = new CAM.Tool(settings, contour.tool), - toolOffset = tool.generateProfile(resolution).profile, - toolDiameter = tool.fluteDiameter(), - toolStep = toolDiameter * contour.step, - traceJoin = toolDiameter / 2, - maxangle = contour.angle, - curvesOnly = contour.curves, - R2A = 180 / Math.PI, - stepsx = Math.ceil(boundsX / resolution), - stepsy = Math.ceil(boundsY / resolution), - topo = widget.topo = { - data: new Float32Array(stepsx * stepsy), - stepsx: stepsx, - stepsy: stepsy, - bounds: bounds, - diameter: toolDiameter, - resolution: resolution, - profile: toolOffset, - widget: widget, - raster: true, - slices: null - }, - data = topo.data, - newslices = [], - tabsMax = tabs ? Math.max(...tabs.map(tab => tab.dim.z/2 + tab.pos.z)) : 0, - tabsOn = tabs, - tabHeight = Math.max(proc.camTabsHeight + zBottom, tabsMax), - clipTab = tabsOn ? [] : null, - clipTo = inside ? shadow : POLY.expand(shadow, toolDiameter/2 + resolution * 3), - partOff = inside ? 0 : toolDiameter / 2 + resolution, - gridDelta = Math.floor(partOff / resolution), - debug = false, - debug_clips = debug && true, - debug_topo = debug && true, - debug_topo_lines = debug && true, - debug_topo_shells = debug && true, - time = Date.now; +class Topo { + constructor(opt = {}) { + let { state, contour, onupdate, ondone } = opt; + let { widget, settings, tshadow, center, tabs, color } = opt.state; - if (tolerance == 0) { - console.log(`contour auto tolerance`,resolution.round(4)); - } + let density = parseInt(settings.controller.animesh || 100) * 2500, + axis = contour.axis.toLowerCase(), + contourX = axis === "x", + contourY = axis === "y", + bounds = widget.getBoundingBox().clone(), + tolerance = contour.tolerance, + proc = settings.process, + shadow = tshadow, + minX = bounds.min.x, + maxX = bounds.max.x, + minY = bounds.min.y, + maxY = bounds.max.y, + zBottom = proc.camZBottom, + zMin = Math.max(bounds.min.z, zBottom) + 0.0001, + boundsX = maxX - minX, + boundsY = maxY - minY, + inside = contour.inside, + resolution = tolerance ? tolerance : 1/Math.sqrt(density/(boundsX * boundsY)), + tool = new CAM.Tool(settings, contour.tool), + toolOffset = tool.generateProfile(resolution).profile, + toolDiameter = tool.fluteDiameter(), + toolStep = toolDiameter * contour.step, + traceJoin = toolDiameter / 2, + maxangle = contour.angle, + curvesOnly = contour.curves, + R2A = 180 / Math.PI, + stepsx = Math.ceil(boundsX / resolution), + stepsy = Math.ceil(boundsY / resolution), + topo = widget.topo = { + data: new Float32Array(stepsx * stepsy), + stepsx: stepsx, + stepsy: stepsy, + bounds: bounds, + diameter: toolDiameter, + resolution: resolution, + profile: toolOffset, + widget: widget, + raster: true, + slices: null + }, + data = topo.data, + newslices = [], + tabsMax = tabs ? Math.max(...tabs.map(tab => tab.dim.z/2 + tab.pos.z)) : 0, + tabsOn = tabs, + tabHeight = Math.max(proc.camTabsHeight + zBottom, tabsMax), + clipTab = tabsOn ? [] : null, + clipTo = inside ? shadow : POLY.expand(shadow, toolDiameter/2 + resolution * 3), + partOff = inside ? 0 : toolDiameter / 2 + resolution, + gridDelta = Math.floor(partOff / resolution), + debug = false, + debug_clips = debug && true, + debug_topo = debug && true, + debug_topo_lines = debug && true, + debug_topo_shells = debug && true, + time = Date.now; - this.tolerance = resolution; + if (tolerance == 0) { + console.log(`contour auto tolerance`,resolution.round(4)); + } - if (tabs) { - clipTab.appendAll(tabs.map(tab => { - let ctab = POLY.expand([tab.poly], toolDiameter/2); - ctab.forEach(ct => ct.z = tab.dim.z/2 + tab.pos.z); - return ctab; - }).flat()); - } + this.tolerance = resolution; - // debug clipTab and clipTo - if (debug_clips) { - const debug = newSlice(-1); - const output = debug.output(); - // if (clipTab) output.setLayer("clip.tab", { line: 0xff0000 }).addPolys(clipTab); - if (clipTo) output.setLayer("clip.to", { line: 0x00dd00 }).addPolys(clipTo); - newslices.push(debug); - } + if (tabs) { + clipTab.appendAll(tabs.map(tab => { + let ctab = POLY.expand([tab.poly], toolDiameter/2); + ctab.forEach(ct => ct.z = tab.dim.z/2 + tab.pos.z); + return ctab; + }).flat()); + } - let newtrace, - sliceout, - latent, - lastP, - slice, lx, ly, - startTime = time(), - stepsTaken = 0, - stepsTotal = 0; + // debug clipTab and clipTo + if (debug_clips) { + const debug = newSlice(-1); + const output = debug.output(); + // if (clipTab) output.setLayer("clip.tab", { line: 0xff0000 }).addPolys(clipTab); + if (clipTo) output.setLayer("clip.to", { line: 0x00dd00 }).addPolys(clipTo); + newslices.push(debug); + } - // return the touching z given topo x,y and a tool profile - function toolAtZ(x,y) { - const profile = toolOffset, - sx = stepsx, - sy = stepsy, - xl = sx - 1, - yl = sy - 1; + let newtrace, + sliceout, + latent, + lastP, + slice, lx, ly, + startTime = time(), + stepsTaken = 0, + stepsTotal = 0; - let tx, ty, tz, gv, i = 0, mz = -1; + // return the touching z given topo x,y and a tool profile + function toolAtZ(x,y) { + const profile = toolOffset, + sx = stepsx, + sy = stepsy, + xl = sx - 1, + yl = sy - 1; - while (i < profile.length) { - // tool profile point x, y, and z offsets - const tx = profile[i++] + x; - const ty = profile[i++] + y; - const tz = profile[i++]; - if (tx < 0 || tx > xl || ty < 0 || ty > yl) { - // if outside max topo steps, use zMin - gv = zMin; - } else { - // lookup grid value @ tx, ty - gv = topo.data[tx * sy + ty] || zMin; - } - // update the rest - mz = Math.max(tz + gv, mz); - } + let tx, ty, tz, gv, i = 0, mz = -1; - return Math.max(mz, 0); - } - - function push_point(x,y,z) { - const newP = newPoint(x,y,z); - // todo: merge co-linear, not just co-planar - if (lastP && Math.abs(lastP.z - z) < 0.005) { - if (curvesOnly) { - end_poly(); - } else { - latent = newP; - } + while (i < profile.length) { + // tool profile point x, y, and z offsets + const tx = profile[i++] + x; + const ty = profile[i++] + y; + const tz = profile[i++]; + if (tx < 0 || tx > xl || ty < 0 || ty > yl) { + // if outside max topo steps, use zMin + gv = zMin; } else { - if (latent) { - newtrace.push(latent); - latent = null; - } - if (curvesOnly && lastP) { - // maxangle - const dz = Math.abs(lastP.z - z); - const dv = contourX ? Math.abs(lastP.x - x) : Math.abs(lastP.y - y); - const angle = Math.atan2(dz, dv) * RAD2DEG; - // if (lastP.z < 0.1) console.log('pp', {dz, dxy, angle}); - if (angle > maxangle) { - lastP = newP; - return; - } - } - newtrace.push(newP); + // lookup grid value @ tx, ty + gv = topo.data[tx * sy + ty] || zMin; } - lastP = newP; + // update the rest + mz = Math.max(tz + gv, mz); } - function end_poly() { + return Math.max(mz, 0); + } + + function push_point(x,y,z) { + const newP = newPoint(x,y,z); + // todo: merge co-linear, not just co-planar + if (lastP && Math.abs(lastP.z - z) < 0.005) { + if (curvesOnly) { + end_poly(); + } else { + latent = newP; + } + } else { if (latent) { newtrace.push(latent); + latent = null; } - if (newtrace.length > 0) { - // add additional constraint on min perimeter() - if (newtrace.length > 1) { - sliceout.push(newtrace); + if (curvesOnly && lastP) { + // maxangle + const dz = Math.abs(lastP.z - z); + const dv = contourX ? Math.abs(lastP.x - x) : Math.abs(lastP.y - y); + const angle = Math.atan2(dz, dv) * RAD2DEG; + // if (lastP.z < 0.1) console.log('pp', {dz, dxy, angle}); + if (angle > maxangle) { + lastP = newP; + return; } - newtrace = newPolygon().setOpen(); } - latent = undefined; - lastP = undefined; + newtrace.push(newP); } + lastP = newP; + } - function raster(slices) { - if (!topo.raster) { - // console.log({skipping_raster: widget.id}); - return; + function end_poly() { + if (latent) { + newtrace.push(latent); + } + if (newtrace.length > 0) { + // add additional constraint on min perimeter() + if (newtrace.length > 1) { + sliceout.push(newtrace); } - topo.raster = false; + newtrace = newPolygon().setOpen(); + } + latent = undefined; + lastP = undefined; + } - let gridx = 0, - gridy, - gridi, // index - gridv, // value - i, il, j, jl, x, y, tv, ltv; + function raster(slices) { + if (!topo.raster) { + // console.log({skipping_raster: widget.id}); + return; + } + topo.raster = false; - // filter lines pairs to only surface "up-facing", "uncovered" lines - slices.map(slice => { - newslices.push(slice); - const points = []; - // emit an array of valid line-pairs - const lines = slice.lines; - const len = lines.length; - outer: for (let i=0; i p2.y) { const tp = p1; p1 = p2; p2 = tp }; - // eliminate if points "under" other lines - for (let j=0; j= i-1 && j <= i+1) continue; - let l2 = lines[j], p3 = l2.p1, p4 = l2.p2; - // sort p3,p4 by y for comparison - if (p3.y > p4.y) { const tp = p3; p3 = p4; p4 = tp }; - // it's under the other line - if (Math.max(p1.z,p2.z) < Math.min(p3.z,p4.z)) { - // it's inside the other line, too, so skip - if (p1.y >= p3.y && p2.y <= p4.y) continue outer; - } + let gridx = 0, + gridy, + gridi, // index + gridv, // value + i, il, j, jl, x, y, tv, ltv; + + // filter lines pairs to only surface "up-facing", "uncovered" lines + slices.map(slice => { + newslices.push(slice); + const points = []; + // emit an array of valid line-pairs + const lines = slice.lines; + const len = lines.length; + outer: for (let i=0; i p2.y) { const tp = p1; p1 = p2; p2 = tp }; + // eliminate if points "under" other lines + for (let j=0; j= i-1 && j <= i+1) continue; + let l2 = lines[j], p3 = l2.p1, p4 = l2.p2; + // sort p3,p4 by y for comparison + if (p3.y > p4.y) { const tp = p3; p3 = p4; p4 = tp }; + // it's under the other line + if (Math.max(p1.z,p2.z) < Math.min(p3.z,p4.z)) { + // it's inside the other line, too, so skip + if (p1.y >= p3.y && p2.y <= p4.y) continue outer; } - points.push(p1, p2); } - slice.points = points; - if (debug_topo_lines) slice.output() - .setLayer("topo lines", {face: 0xff00ff, line: 0x880088}) - .addLines(points); - if (debug_topo_shells) slice.output() - .setLayer("topo shells", {face: 0xff00ff, line: 0x008888}) - .addPolys(slice.topPolys()); - }); + points.push(p1, p2); + } + slice.points = points; + if (debug_topo_lines) slice.output() + .setLayer("topo lines", {face: 0xff00ff, line: 0x880088}) + .addLines(points); + if (debug_topo_shells) slice.output() + .setLayer("topo shells", {face: 0xff00ff, line: 0x008888}) + .addPolys(slice.topPolys()); + }); - // raster grid: for each Y slice, find z grid value (x/z swapped) - for (j=0, jl=slices.length; j gridv || p2.z > gridv); - // segment crosses grid y - const spansy = (p1.y <= y && p2.y >= y); - if (crossz && spansy) { - // compute intersection of z ray up - // and segment at this grid point - const dy = p1.y - p2.y, - dz = p1.z - p2.z, - pct = (p1.y - y) / dy, - nz = p1.z - (dz * pct); - // save if point is greater than existing grid point - if (nz > gridv) { - gridv = data[gridi] = Math.max(nz, zMin); - if (debug_topo) slice.output() - .setLayer("topo", {face: color, line: color}) - .addLine( - newPoint(p1.x, y, 0), - newPoint(p1.x, y, gridv) - ); - } + // raster grid: for each Y slice, find z grid value (x/z swapped) + for (j=0, jl=slices.length; j gridv || p2.z > gridv); + // segment crosses grid y + const spansy = (p1.y <= y && p2.y >= y); + if (crossz && spansy) { + // compute intersection of z ray up + // and segment at this grid point + const dy = p1.y - p2.y, + dz = p1.z - p2.z, + pct = (p1.y - y) / dy, + nz = p1.z - (dz * pct); + // save if point is greater than existing grid point + if (nz > gridv) { + gridv = data[gridi] = Math.max(nz, zMin); + if (debug_topo) slice.output() + .setLayer("topo", {face: color, line: color}) + .addLine( + newPoint(p1.x, y, 0), + newPoint(p1.x, y, gridv) + ); } } + } + gridy++; + } + gridx++; + onupdate(++stepsTaken, stepsTotal, "raster surface"); + } + } + + function processSlices(slices) { + let gridx = 0, + gridy, + gridi, // index + gridv, // value + i, il, j, jl, x, y, tv, ltv; + + raster(slices); + + const checkr = newPoint(0,0); + const inClip = function(polys, checkZ) { + checkr.x = x; + checkr.y = y; + for (let i=0; i 0) { + newslices.push(slice); + slice.camLines = sliceout; + slice.output() + .setLayer("contour y", {face: color, line: color}) + .addPolys(sliceout); + } + onupdate(++stepsTaken, stepsTotal, "contour y"); } } - function processSlices(slices) { - let gridx = 0, - gridy, - gridi, // index - gridv, // value - i, il, j, jl, x, y, tv, ltv; - - raster(slices); - - const checkr = newPoint(0,0); - const inClip = function(polys, checkZ) { - checkr.x = x; - checkr.y = y; - for (let i=0; i 0) { - newslices.push(slice); - slice.camLines = sliceout; - slice.output() - .setLayer("contour y", {face: color, line: color}) - .addPolys(sliceout); - } - onupdate(++stepsTaken, stepsTotal, "contour y"); - } - } - - if (contourX) { - startTime = time(); - // emit slice per Y - for (y = minY - partOff; y <= maxY + partOff; y += toolStep) { - gridy = Math.round(((y - minY) / boundsY) * stepsy); - lx = gridx = -gridDelta; - slice = newSlice(gridy); - newtrace = newPolygon().setOpen(); - sliceout = []; - for (x = minX - partOff; x <= maxX + partOff; x += resolution) { - tv = toolAtZ(gridx, gridy); - if (tabsOn && tv < tabHeight && inClip(clipTab, tv)) { - tv = tabZ; - } - if (clipTo && !inClip(clipTo)) { - end_poly(); - gridx++; - lx = -gridDelta; - continue; - } - push_point(x,y,tv); - lx = x; - ltv = tv; + if (clipTo && !inClip(clipTo)) { + end_poly(); gridx++; + lx = -gridDelta; + continue; } - end_poly(); - if (sliceout.length > 0) { - newslices.push(slice); - slice.camLines = sliceout; - slice.output() - .setLayer("contour x", {face: color, line: color}) - .addPolys(sliceout); - } - onupdate(++stepsTaken, stepsTotal, "contour x"); + push_point(x,y,tv); + lx = x; + ltv = tv; + gridx++; } + end_poly(); + if (sliceout.length > 0) { + newslices.push(slice); + slice.camLines = sliceout; + slice.output() + .setLayer("contour x", {face: color, line: color}) + .addPolys(sliceout); + } + onupdate(++stepsTaken, stepsTotal, "contour x"); } - - ondone(newslices); } - let slicer = new KIRI.cam_slicer(widget.getPoints(), { - swapX: true, emptyok: true, notopok: true - }); - let sindex = slicer.interval(resolution); - if (!topo.slices) stepsTotal += sindex.length * 2; - if (contourX) stepsTotal += Math.round((maxY-minY) / toolStep); - if (contourY) stepsTotal += Math.round((maxX-minX) / toolStep); - - let slices = topo.slices = topo.slices || slicer.slice(sindex, { each: (data, index, total) => { - onupdate(++stepsTaken, stepsTotal, "topo slice"); - }, genso: true }); - - processSlices(slices.filter(s => s.lines).map(data => data.slice)); + ondone(newslices); } + + let slicer = new kiri.cam_slicer(widget.getPoints(), { + swapX: true, emptyok: true, notopok: true + }); + let sindex = slicer.interval(resolution); + if (!topo.slices) stepsTotal += sindex.length * 2; + if (contourX) stepsTotal += Math.round((maxY-minY) / toolStep); + if (contourY) stepsTotal += Math.round((maxX-minX) / toolStep); + + let slices = topo.slices = topo.slices || slicer.slice(sindex, { each: (data, index, total) => { + onupdate(++stepsTaken, stepsTotal, "topo slice"); + }, genso: true }); + + processSlices(slices.filter(s => s.lines).map(data => data.slice)); } +} - CAM.Topo = Topo; +CAM.Topo = Topo; -})(); +});