diff --git a/src2/kiri-mode/cam/op-contour.js b/src2/kiri-mode/cam/op-contour.js new file mode 100644 index 00000000..1801b95c --- /dev/null +++ b/src2/kiri-mode/cam/op-contour.js @@ -0,0 +1,125 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; +import { Tool } from './tool.js'; +import { newPoint } from '../../geo/point.js'; +import { tip2tipEmit } from '../../geo/paths.js'; + +function createFilter(op) { + let filter = slices => slices; + if (op.filter) { + try { + const obj = eval(`( ${op.filter.join('\n')} )`); + let idx = 0; + if (obj && obj.slices) { + const nadd = []; + filter = function(slices) { + for (let slice of slices) { + if (obj.slices(slice, idx++)) { + nadd.push(slice); + } + } + return nadd; + }; + } + } catch (e) { + console.log('filter parse error', e, op.filter); + } + } + return filter; +} + +class OpContour extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let { op, state } = this; + let { addSlices } = state; + let filter = createFilter(op); + // 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 = await Topo({ + // onupdate: (update, msg) => { + onupdate: (index, total, msg) => { + progress(index / total, msg); + }, + ondone: (slices) => { + slices = filter(slices); + this.sliceOut = slices; + addSlices(slices); + }, + contour: op, + state: state + }); + // computed if set to 0 + this.tolerance = topo.tolerance; + } + + prepare(ops, progress) { + let { op, state, sliceOut } = this; + let { settings, widget } = state; + let { process } = settings; + + let { setTolerance, setTool, setSpindle, setPrintPoint } = ops; + let { camOut, polyEmit, newLayer, printPoint, lastPoint } = ops; + let { bounds, zmax } = ops; + + let toolDiam = this.toolDiam = new Tool(settings, op.tool).fluteDiameter(); + let stepover = toolDiam * op.step * 2; + let depthFirst = process.camDepthFirst; + let depthData = []; + + setTool(op.tool, op.rate, process.camFastFeedZ); + setSpindle(op.spindle); + setTolerance(this.tolerance); + + printPoint = newPoint(bounds.min.x,bounds.min.y,zmax); + + for (let slice of sliceOut) { + // ignore debug slices + 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(); + } + } + + 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); + } +} + +export { OpContour }; \ No newline at end of file diff --git a/src2/kiri-mode/cam/op-drill.js b/src2/kiri-mode/cam/op-drill.js new file mode 100644 index 00000000..aea41352 --- /dev/null +++ b/src2/kiri-mode/cam/op-drill.js @@ -0,0 +1,68 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; +import { Tool } from './tool.js'; +import { newPolygon } from '../../geo/polygon.js'; +import { newSlice } from '../../kiri/slice.js'; +import { newPoint } from '../../geo/point.js'; + +class OpDrill extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let { op, state } = this; + let { settings, addSlices, widget, updateToolDiams } = state; + let { zBottom, zThru, thruHoles, color } = state; + let { drills, drillThrough} = op + + let drillTool = new Tool(settings, op.tool), + drillToolDiam = drillTool.fluteDiameter(), + sliceOut = this.sliceOut = []; + + updateToolDiams(drillToolDiam); + + const allDrills = drills[widget.id] ?? [] + // drill points to use center (average of all points) of the polygon + allDrills.forEach((drill)=> { + if(!drill.selected){ + return + } + let slice = newSlice(0); + if (op.mark) { + // replace depth with single down peck + drill.depth = op.down + } + drill.zBottom = drill.z - drill.depth; + // for thru holes, follow z thru when set + if ((op.thru>0) ) { + drill.zBottom -= op.thru; + } + const poly = newPolygon() + poly.points.push(newPoint(drill.x, drill.y, drill.z)) + poly.points.push(newPoint(drill.x, drill.y, drill.zBottom)) + // poly.points.pop(); + slice.camTrace = { tool: op.tool, rate: op.feed, plunge: op.rate }; + slice.camLines = [poly]; + + slice.output() + .setLayer("drill", {face: color, line: color}) + .addPolys(slice.camLines); + addSlices(slice); + sliceOut.push(slice); + }); + } + + prepare(ops, progress) { + let { op, state } = this; + let { settings, widget, addSlices, updateToolDiams } = state; + let { setTool, setSpindle, setDrill, emitDrills } = ops; + setTool(op.tool, undefined, op.rate); + setDrill(op.down, op.lift, op.dwell,op.thru); + setSpindle(op.spindle); + emitDrills(this.sliceOut.map(slice => slice.camLines).flat()); + } +} + +export { OpDrill }; diff --git a/src2/kiri-mode/cam/op-gcode.js b/src2/kiri-mode/cam/op-gcode.js new file mode 100644 index 00000000..a3ed9bfb --- /dev/null +++ b/src2/kiri-mode/cam/op-gcode.js @@ -0,0 +1,13 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; + +export class OpGCode extends CamOp { + constructor(state, op) { + super(state, op); + } + + prepare(ops, progress) { + ops.addGCode(this.op.gcode); + } +} diff --git a/src2/kiri-mode/cam/op-index.js b/src2/kiri-mode/cam/op-index.js new file mode 100644 index 00000000..becd0441 --- /dev/null +++ b/src2/kiri-mode/cam/op-index.js @@ -0,0 +1,36 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; + +export class OpIndex extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice() { + let { op, state } = this; + if (!state.isIndexed) { + throw 'index op requires indexed stock'; + } + let { widget, updateSlicer, computeShadows, setAxisIndex } = state; + this.degrees = setAxisIndex(op.degrees, op.absolute); + // force recompute of topo + widget.topo = undefined; + updateSlicer(); + await computeShadows(); + } + + prepare(ops, progress) { + const { lastPoint, zmax, zclear, camOut, stock } = ops; + let last = lastPoint(); + if (last) { + // max point of stock corner radius when rotating (safe z when indexing) + const rzmax = (Math.max(stock.y, stock.z) * Math.sqrt(2)) / 2 + zclear; + const zmove = Math.max(rzmax, zmax); + // move above rotating stock + camOut(last = last.clone().setZ(zmove), 0); + // issue rotation command + camOut(last = last.clone().setY(0).setA(this.degrees), 0); + } + } +} diff --git a/src2/kiri-mode/cam/op-laser-off.js b/src2/kiri-mode/cam/op-laser-off.js new file mode 100644 index 00000000..a474e933 --- /dev/null +++ b/src2/kiri-mode/cam/op-laser-off.js @@ -0,0 +1,17 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; + +export class OpLaserOff extends CamOp { + constructor(state, op) { + super(state, op); + } + + prepare(ops, progress) { + const { printPoint, zmax, camOut } = ops; + this.op.silent = true; + ops.addGCode(this.op.disable); + ops.setLasering(false); + camOut(printPoint.clone().setZ(zmax), 0); + } +} diff --git a/src2/kiri-mode/cam/op-laser-on.js b/src2/kiri-mode/cam/op-laser-on.js new file mode 100644 index 00000000..076009c9 --- /dev/null +++ b/src2/kiri-mode/cam/op-laser-on.js @@ -0,0 +1,17 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; + +export class OpLaserOn extends CamOp { + constructor(state, op) { + super(state, op); + } + + prepare(ops, progress) { + const { printPoint, setPrintPoint, setTool, zmax, camOut } = ops; + this.op.silent = true; + setTool(0); + ops.addGCode(this.op.enable); + ops.setLasering(true, this.op.power); + } +} diff --git a/src2/kiri-mode/cam/op-lathe.js b/src2/kiri-mode/cam/op-lathe.js new file mode 100644 index 00000000..61184306 --- /dev/null +++ b/src2/kiri-mode/cam/op-lathe.js @@ -0,0 +1,118 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; +import { Tool } from './tool.js'; +import { newPoint } from '../../geo/point.js'; + +function createFilter(op) { + let filter = slices => slices; + if (op.filter) { + try { + const obj = eval(`( ${op.filter.join('\n')} )`); + let idx = 0; + if (obj && obj.slices) { + const nadd = []; + filter = function(slices) { + for (let slice of slices) { + if (obj.slices(slice, idx++)) { + nadd.push(slice); + } + } + return nadd; + }; + } + } catch (e) { + console.log('filter parse error', e, op.filter); + } + } + return filter; +} + +class OpLathe extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let { op, state } = this; + let { addSlices } = state; + let filter = createFilter(op); + + this.topo = await Topo4({ + op, + state, + onupdate: (pct, msg) => { + progress(pct, msg); + }, + ondone: (slices) => { + slices = filter(slices); + this.slices = slices; + addSlices(slices, false); + } + }); + } + + prepare(ops, progress) { + let { op, state, slices, topo } = this; + let { settings } = state; + + let { setTool, setSpindle, setPrintPoint } = ops; + let { camOut, newLayer, printPoint } = ops; + let { zmax } = ops; + + let toolDiam = new Tool(settings, op.tool).fluteDiameter(); + let stepover = toolDiam * op.step * 2; + let rez = topo.resolution; + + setTool(op.tool, op.rate, op.plunge); + setSpindle(op.spindle); + + // start top center, X = 0, Y = 0 closest to 4th axis chuck + printPoint = newPoint(0, 0, zmax); + + for (let slice of slices) { + // ignore debug slices + if (!slice.camLines) { + continue; + } + + let last; + for (let path of slice.camLines) { + let latent; + path.forEachPoint((point, pidx) => { + if (last) { + const dz = Math.abs(last.z - point.z); + if (dz < rez) { + // latent point should still be included in + // preview b/c arcs would look like straight lines + latent = point.clone(); + return; + } + if (latent) { + camOut(latent, true, stepover); + latent = undefined; + } + } + camOut(last = point.clone(), pidx > 0, stepover); + }, false); + if (latent) { + camOut(latent, true, stepover); + } + } + + newLayer(); + } + + // move to safe height and reset A axis + let last = ops.lastPoint(); + let amax = (Math.round(last.a / 360) * 360).round(2); + // camOut(last = last.clone().setZ(zmax), 0); + // camOut(last = last.clone().setA(amax), 0); + newLayer(); + ops.addGCode([`G0 Z${zmax.round(2)}`,`G0 A${amax}`,"G92 A0"]); + + setPrintPoint(last); + } +} + +export { OpLathe }; \ No newline at end of file diff --git a/src2/kiri-mode/cam/op-level.js b/src2/kiri-mode/cam/op-level.js new file mode 100644 index 00000000..ee18e664 --- /dev/null +++ b/src2/kiri-mode/cam/op-level.js @@ -0,0 +1,68 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; +import { Tool } from './tool.js'; +import { newPolygon } from '../../geo/polygon.js'; +import { newSlice } from '../../kiri/slice.js'; +import { polygons as POLY } from '../../geo/polygons.js'; + +class OpLevel extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let { op, state } = this; + let { settings, widget, addSlices } = state; + let { updateToolDiams, tabs, cutTabs } = state; + let { bounds, zMax, ztOff, color, tshadow } = state; + let { stock } = settings; + + let toolDiam = new Tool(settings, op.tool).fluteDiameter(); + let stepOver = this.stepOver = toolDiam * op.step; + let z = zMax + ztOff - op.down; + + updateToolDiams(toolDiam); + + let points = []; + let clear = op.stock ? + [ newPolygon().centerRectangle({x:0,y:0,z:0}, stock.x, stock.y) ] : + POLY.outer(POLY.offset(tshadow, toolDiam * (op.over || 0))); + + POLY.fillArea(clear, 90, stepOver, points); + + let lines = this.lines = []; + for (let i=0; i { return { first: p.first(), last: p.last(), poly: p } }); + printPoint = tip2tipEmit(lines, printPoint, (el, point, count) => { + let poly = el.poly; + if (poly.last() === point) { + poly.reverse(); + } + poly.forEachPoint((point, pidx) => { + camOut(point.clone(), true, stepOver); + }, false); + }); + setPrintPoint(printPoint); + + newLayer(); + } +} + +export { OpLevel }; diff --git a/src2/kiri-mode/cam/op-outline.js b/src2/kiri-mode/cam/op-outline.js new file mode 100644 index 00000000..7c59e46d --- /dev/null +++ b/src2/kiri-mode/cam/op-outline.js @@ -0,0 +1,298 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; +import { Tool } from './tool.js'; +import { newPolygon } from '../../geo/polygon.js'; +import { newSlice } from '../../kiri/slice.js'; +import { polygons as POLY } from '../../geo/polygons.js'; +import { util as base_util } from '../../geo/base.js'; +import { poly2polyEmit } from '../../geo/paths.js'; +import { newPoint } from '../../geo/point.js'; + +class OpOutline extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let { op, state } = this; + let { settings, widget, slicer, addSlices, tshadow, thruHoles, unsafe, color } = state; + let { updateToolDiams, tabs, cutTabs, cutPolys, workarea, zMax } = state; + let { process, stock } = settings; + + if (op.down <= 0) { + throw `invalid step down "${op.down}"`; + } + let toolDiam = this.toolDiam = new Tool(settings, op.tool).fluteDiameter(); + updateToolDiams(toolDiam); + + let shadow = []; + let slices = []; + let intopt = { + off: 0.01, + fit: true, + down: true, + min: Math.max(0, workarea.bottom_z), + max: workarea.top_z + }; + let indices = slicer.interval(op.down, intopt); + 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); + + let cnt = 0; + let tot = 0; + 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 + await slicer.slice(indices, { each: data => { + 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 ? 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(0.5 + 0.5 * (++cnt / tot)); + }, progress: (index, total) => { + tot = total; + progress((index / total) * 0.5); + } }); + shadow = POLY.union(shadow.appendAll(state.shadow.base), 0.01, true); + + // start slices at top of stock when `clear top` enabled + if (op.top) { + let first = slices[0]; + let zlist = slices.map(s => s.z); + for (let z of indices.filter(v => v >= zMax)) { + if (zlist.contains(z)) { + continue; + } + let add = first.clone(true); + add.tops.forEach(top => top.poly.setZ(add.z)); + add.shadow = first.shadow.clone(true); + add.z = z; + slices.splice(0,0,add); + } + } + + // extend cut thru (only when z bottom is 0) + if (workarea.bottom_z < 0) { + let last = slices[slices.length-1]; + for (let zneg of base_util.lerp(0, -workarea.bottom_cut, op.down)) { + if (!last) continue; + 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, false, 0.1); + }); + } + } + } + + if (op.omitvoid) { + for (let top of tops) { + delete top.inner; + } + } + + let offset = POLY.expand(tops, toolDiam / 2, slice.z); + if (!(offset && offset.length)) { + return; + } + + // 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 0; c--){ + offset.slice().forEach(op => { + // clone removes inners but the real solution is + // to limit expanded shells to through holes + let wideCut = POLY.expand([op.clone(true)], stepover*c, slice.z, [], 1); + wideCut.forEach(cut =>{ //set order of cuts when wide + cut.order = c + if(cut.inner) cut.inner.forEach(inn =>{ inn.order = c }) + }); + wideCuts.push(...wideCut) + }); + } + offset.appendAll(wideCuts); + } + } + + if (op.dogbones && !op.wide) { + addDogbones(offset, toolDiam / 5); + } + + if (tabs) { + tabs.forEach(tab => { + tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); + }); + offset = cutTabs(tabs, offset, slice.z); + } + + if (process.camStockClipTo && stock.x && stock.y && stock.center) { + let rect = newPolygon().centerRectangle({x:0,y:0}, stock.x, stock.y); + offset = cutPolys([rect], offset, slice.z, true); + } + + // offset.xout(`slice ${slice.z}`); + slice.camLines = offset; + }); + + // when top expand fails above, it creates an empty slice + slices = slices.filter(s => s.camLines); + + // 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); + } + + addSlices(slices); + this.sliceOut = slices; + } + + 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 easeDown = process.camEaseDown; + let toolDiam = this.toolDiam; + let cutdir = op.ov_conv;//process.camConventional; + let depthFirst = process.camDepthFirst; + let depthData = []; + + setTool(op.tool, op.rate, op.plunge); + setSpindle(op.spindle); + + // printpoint becomes NaN in engine mode. not sure why but this fixes it + if(Object.values(printPoint).some(v=>Number.isNaN(v))){ + printPoint = newPoint(0,0,0); + } + + for (let slice of sliceOut) { + let polys = [], t = [], c = []; + let lines =POLY.flatten(slice.camLines) + // console.log(lines); + lines.forEach((poly)=> { + poly.order = poly.order ?? 0; + 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 { + let orderSplit = {} + polys.forEach(poly => { + if(poly.order in orderSplit) orderSplit[poly.order].push(poly); + else orderSplit[poly.order] = [poly]; + }) + Object.entries(orderSplit) //split the polys by order + .sort((a,b) => -(a[0] - b[0] )) //sort by order (highest first) + .forEach(([order, orderPolys]) => { // emit based on closest for each order + let polyLast; + // console.log({order, orderPolys}); + printPoint = poly2polyEmit(orderPolys, printPoint, function(poly, index, count) { + + polyLast = polyEmit(poly, index, count, polyLast); + }, { 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 = op.down && easeDown ? 0.001 : 0; + printPoint = depthOutlinePath(printPoint, 0, flatLevels, toolDiam, polyEmit, false, ease); + printPoint = depthOutlinePath(printPoint, 0, flatLevels, toolDiam, polyEmit, true, ease); + } + } + + setPrintPoint(printPoint); + } +} + +export { OpOutline }; diff --git a/src2/kiri-mode/cam/op-pocket.js b/src2/kiri-mode/cam/op-pocket.js new file mode 100644 index 00000000..3b734e20 --- /dev/null +++ b/src2/kiri-mode/cam/op-pocket.js @@ -0,0 +1,283 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; +import { Tool } from './tool.js'; +import { newPolygon } from '../../geo/polygon.js'; +import { newSlice } from '../../kiri/slice.js'; +import { polygons as POLY } from '../../geo/polygons.js'; +import { util as base_util } from '../../geo/base.js'; +import { CAM } from './driver-be.js'; + +const DEG2RAG = Math.PI / 180; + +class OpPocket extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + const pocket = this; + const debug = false; + let { op, state } = this; + let { tool, rate, down, plunge, expand, contour, smooth, tolerance } = op; + let { ov_botz, ov_conv } = op; + let { settings, widget, addSlices, zBottom, zThru, tabs, color } = state; + let { updateToolDiams, cutTabs, healPolys, shadowAt, workarea } = state; + let { process } = settings; + zBottom = ov_botz ? workarea.bottom_stock + ov_botz : zBottom; + // generate tracing offsets from chosen features + let sliceOut; + let pockets = this.pockets = []; + let camTool = new Tool(settings, tool); + let toolDiam = camTool.fluteDiameter(); + let toolOver = toolDiam * op.step; + let cutdir = ov_conv; + let engrave = contour && op.engrave; + let zTop = workarea.top_z; + if (contour) { + down = 0; + this.topo = await Topo({ + // onupdate: (update, msg) => { + onupdate: (index, total, msg) => { + progress((index / total) * 0.9, msg); + }, + ondone: (slices) => { + // console.log({ contour: slices }); + }, + contour: { + tool, + tolerance, + inside: true, + axis: "-" + }, + state: state + }); + } + updateToolDiams(toolDiam); + if (tabs) { + tabs.forEach(tab => { + tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); + }); + } + function newPocket() { + pockets.push(sliceOut = []); + } + function newSliceOut(z) { + let slice = newSlice(z); + sliceOut.push(slice); + return slice; + } + function clearZ(polys, z, down) { + if (down) { + // adjust step down to a value <= down that + // ends on the lowest z specified + let diff = zTop - z; + down = diff / Math.ceil(diff / down); + } + let zs = down ? base_util.lerp(zTop, z, down) : [ z ]; + if (engrave) { + toolDiam = toolOver; + } + if (contour) { + expand = engrave ? 0 : expand; + } else if (expand) { + polys = POLY.offset(polys, expand); + } + let zpro = 0, zinc = 1 / (polys.length * zs.length); + for (let poly of polys) { + newPocket(); + for (let z of zs) { + let clip = [], shadow; + if (contour) { + if (smooth) { + clip = POLY.offset(POLY.offset([ poly ], smooth), -smooth); + } else { + clip = [ poly ]; + } + } else { + shadow = shadowAt(z); + if (smooth) { + shadow = POLY.setZ(POLY.offset(POLY.offset(shadow, smooth), -smooth), z); + } + POLY.subtract([ poly ], shadow, clip, undefined, undefined, 0); + if (op.outline) { + POLY.clearInner(clip); + } + } + if (clip.length === 0) { + continue; + } + let slice = newSliceOut(z); + let count = engrave ? 1 : 999; + slice.camTrace = { tool, rate, plunge }; + if (toolDiam) { + const offs = contour ? + [ expand || (-0.02), -toolOver ] : + [ -toolDiam / 2, -toolOver ]; + POLY.offset(clip, offs, { + count, outs: slice.camLines = [], flat:true, z, minArea: 0 + }); + } else { + // when engraving with a 0 width tip + slice.camLines = clip; + } + if (tabs) { + slice.camLines = cutTabs(tabs, POLY.flatten(slice.camLines, null, true), z); + } else { + slice.camLines = POLY.flatten(slice.camLines, null, true); + } + POLY.setWinding(slice.camLines, cutdir, false); + if (contour) { + slice.camLines = pocket.conform(slice.camLines, op.refine, engrave, pct => { + progress(0.9 + (zpro + zinc * pct) * 0.1, "conform"); + }); + } + slice.output() + .setLayer("pocket", {line: color}, false) + .addPolys(slice.camLines) + if (debug && shadow) slice.output() + .setLayer("pocket shadow", {line: 0xff8811}, false) + .addPolys(shadow) + if (!contour) { + progress(zpro, "pocket"); + } + zpro += zinc; + addSlices(slice); + } + } + } + let surfaces = op.surfaces[widget.id] || []; + let vert = widget.getGeoVertices({ unroll: true, translate: true }).map(v => v.round(4)); + // let vert = widget.getVertices().array.map(v => v.round(4)); + let outline = []; + let faces = CAM.surface_find(widget, surfaces, (op.follow || 5) * DEG2RAG); + let zmin = Infinity; + let j=0, k=faces.length; + for (let face of faces) { + let i = face * 9; + outline.push(newPolygon() + .add(vert[i++], vert[i++], zmin = Math.min(zmin, vert[i++])) + .add(vert[i++], vert[i++], zmin = Math.min(zmin, vert[i++])) + .add(vert[i++], vert[i++], zmin = Math.min(zmin, vert[i++])) + ); + } + zmin = Math.max(zBottom, zmin); + outline = POLY.union(outline, 0.0001, true); + outline = POLY.setWinding(outline, cutdir, false); + outline = healPolys(outline); + if (smooth) { + outline = POLY.offset(POLY.offset(outline, smooth), -smooth); + } + if (outline.length) { + // option to skip interior features (holes, pillars) + if (op.outline) { + POLY.clearInner(outline); + } + if (debug) newSliceOut(zmin).output() + .setLayer("pocket area", {line: 0x1188ff}, false) + .addPolys(outline) + clearZ(outline, zmin + 0.0001, down); + progress(1, "pocket"); + } + } + + // mold cam output lines to the surface of the topo offset by tool geometry + conform(camLines, refine, engrave, progress) { + const topo = this.topo; + // re-segment polygon to a higher resolution + const hirez = camLines.map(p => p.segment(topo.tolerance * 2)); + // walk points and offset from surface taking into account tool geometry + let steps = hirez.length; + let iter = 0; + for (let poly of hirez) { + for (let point of poly.points) { + point.z = engrave ? topo.zAtXY(point.x, point.y) : topo.toolAtXY(point.x, point.y); + } + progress((iter++ / steps) * 0.8); + } + steps = steps * refine; + iter = 0; + // walk points noting z deltas and smoothing z sawtooth patterns + for (let j=0; j p.midpoints(topo.tolerance * 8)); + return hirez; + } + + prepare(ops, progress) { + let { op, state, pockets } = this; + let { setTool, setSpindle, setTolerance, sliceOutput, getPrintPoint } = ops; + let { process } = state.settings; + + setTool(op.tool, op.rate); + setSpindle(op.spindle); + + if (this.topo) { + setTolerance(this.topo.tolerance); + } + + // eliminate empty pockets + pockets = pockets.filter(p => p.length); + + // pockets is an [ array of an [ array of slices ] ] + // each top level array is a pocket containing a [ z layer array of slices ] + // follow each pocket to the next closest one from previous exit + for (;;) { + let printPoint = getPrintPoint(); + let min = { + dist: Infinity, + pocket: undefined + }; + for (let pocket of pockets.filter(p => !p.used)) { + let poly = pocket[0].camLines.slice().sort((a,b) => b.area() - a.area())[0]; + if (!poly) continue; + let find = poly.findClosestPointTo(printPoint); + if (find.distance < min.dist) { + min.pocket = pocket; + min.dist = find.distance; + } + } + if (min.pocket) { + min.pocket.used = true; + sliceOutput(min.pocket, { + cutdir: op.ov_conv, + depthFirst: process.camDepthFirst && !state.isIndexed, + easeDown: op.down && process.easeDown ? op.down : 0, + progress: (n,m) => progress(n/m, "pocket") + }); + } else { + break; + } + } + } +} + +export { OpPocket }; diff --git a/src2/kiri-mode/cam/op-register.js b/src2/kiri-mode/cam/op-register.js new file mode 100644 index 00000000..f08b3046 --- /dev/null +++ b/src2/kiri-mode/cam/op-register.js @@ -0,0 +1,162 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; +import { Tool } from './tool.js'; +import { newPolygon } from '../../geo/polygon.js'; +import { newSlice } from '../../kiri/slice.js'; +import { newPoint } from '../../geo/point.js'; +import { util as base_util } from '../../geo/base.js'; + +class OpRegister extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let { op, state } = this; + let { settings, widget, bounds, addSlices, zMax, zThru, color } = state; + let { updateToolDiams } = state; + + let tool = new 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 = op.thru || 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); + addSlices(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: color}, 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; + slice.output() + .setLayer("register", {face: color, line: color}) + .addPolys(polys); + addSlices(slice); + sliceOut.push(slice); + } + } + + prepare(ops, progress) { + let { op, state } = this; + let { settings, widget, addSlices, 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); + } + } 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()); + } + } +} + +export { OpRegister }; diff --git a/src2/kiri-mode/cam/op-rough.js b/src2/kiri-mode/cam/op-rough.js new file mode 100644 index 00000000..a373144c --- /dev/null +++ b/src2/kiri-mode/cam/op-rough.js @@ -0,0 +1,318 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; +import { Tool } from './tool.js'; +import { newPolygon } from '../../geo/polygon.js'; +import { newSlice } from '../../kiri/slice.js'; +import { polygons as POLY } from '../../geo/polygons.js'; +import { util as base_util } from '../../geo/base.js'; +import { poly2polyEmit } from '../../geo/paths.js'; + +class OpRough extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let { op, state } = this; + let { settings, slicer, addSlices, unsafe, color } = state; + let { updateToolDiams, thruHoles, tabs, cutTabs, cutPolys } = state; + let { ztOff, zMax, shadowAt, isIndexed} = state; + let { workarea } = state; + let { process, stock } = settings; + + if (op.down <= 0) { + throw `invalid step down "${op.down}"`; + } + + let roughIn = op.inside; + let roughDown = op.down; + let roughLeave = op.leave || 0; + let roughLeaveZ = op.leavez || 0; + let roughStock = op.all && isIndexed; + let toolDiam = new Tool(settings, op.tool).fluteDiameter(); + let trueShadow = process.camTrueShadow === true; + + updateToolDiams(toolDiam); + + // clear the stock above the area to be roughed out + if (workarea.top_z > workarea.top_part) { + let shadow = state.shadow.base.clone(); + let step = toolDiam * op.step; + let inset = roughStock ? + POLY.offset([ newPolygon().centerRectangle(stock.center, stock.x, stock.y) ], step) : + 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 = newSlice(); + slice.z = z; + slice.camLines = POLY.setZ(facing.clone(true), slice.z + roughLeaveZ); + slice.output() + .setLayer("face", {face: color, line: color}) + .addPolys(slice.camLines); + addSlices(slice); + camFaces.push(slice); + z -= zstep; + } + } + + // create roughing slices + let flats = []; + let shadow = []; + let slices = []; + let indices = slicer.interval(roughDown, { + down: true, min: 0, fit: true, off: 0.01 + }); + + // shift out first (top-most) slice + indices.shift(); + + // find flats and add to indices for slicing + 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 >= workarea.bottom_z); + 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); + } + + indices = indices.filter(v => v >= workarea.bottom_z); + // console.log('indices', ...indices, {zBottom}); + + let cnt = 0; + let tot = 0; + await slicer.slice(indices, { each: data => { + 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; + } + if (data.z > workarea.top_z) { + return; + } + data.shadow = trueShadow ? shadowAt(data.z) : shadow.clone(true); + data.slice.shadow = data.shadow; + slices.push(data.slice); + progress(0.25 + 0.25 * (++cnt / tot)); + }, progress: (index, total) => { + tot = total; + progress((index / total) * 0.25); + } }); + + if (trueShadow) { + shadow = state.shadow.base.clone(true); + } else { + shadow = POLY.union(shadow.appendAll(state.shadow.base), 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)) { + let po = POLY.offset([p], -(toolDiam / 2 + roughLeave + 0.05)); + return po ? po[0] : undefined; + } else { + return p; + } + }).filter(p => p); + }); + shadow = POLY.nest(shadow); + if (op.voids) { + // eliminate voids from shadow when "clear voids" enables + for (let s of shadow) s.inner = undefined; + } + + // shell = shadow expanded by half tool diameter + leave stock + const sadd = roughIn ? toolDiam / 2 : toolDiam / 2; + const shell = roughStock ? + POLY.offset([ newPolygon().centerRectangle(stock.center, stock.x, stock.y) ], sadd) : + 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: Math.min(0.01, toolDiam * op.step / 4), + 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 && !roughStock) { + 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 (roughLeaveZ) { + // offset roughing in Z as well to minimize + // tool marks on curved surfaces + // const roughLeaveZ = 1 * Math.min(roughDown, roughLeave / 2); + slice.camLines.forEach(p => { + p.setZ(p.getZ() + roughLeaveZ); + }); + } + 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); + progress(0.5 + 0.5 * (index / slices.length)); + }); + + let last = slices[slices.length-1]; + + if (workarea.bottom_z < 0) + for (let zneg of base_util.lerp(0, -workarea.bottom_cut, op.down)) { + if (!last) continue; + let add = last.clone(true); + add.z -= zneg; + add.camLines = last.camLines.clone(true); + add.camLines.forEach(p => p.setZ(add.z + roughLeaveZ)); + // add.tops.forEach(top => top.poly.setZ(add.z)); + // add.shadow = last.shadow.clone(true); + slices.push(add); + } + + slices.forEach(slice => { + slice.output() + .setLayer("roughing", {face: color, line: color}) + .addPolys(slice.camLines); + }); + this.sliceOut = slices.filter(slice => slice.camLines); + + addSlices(this.sliceOut); + } + + prepare(ops, progress) { + let { op, state, sliceOut, camFaces } = this; + let { setTool, setSpindle, setPrintPoint, sliceOutput, polyEmit } = ops; + let { camOut, newLayer, printPoint } = ops; + let { settings } = state; + let { process } = settings; + + let easeDown = process.camEaseDown; + let cutdir = op.ov_conv; + let depthFirst = process.camDepthFirst && !state.isIndexed; + let depthData = []; + + setTool(op.tool, op.rate, op.plunge); + setSpindle(op.spindle); + + // output the clearing of stock above roughing + 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); + poly2polyEmit(level, printPoint, (poly, index, count) => { + printPoint = polyEmit(poly, index, count, printPoint); + }); + newLayer(); + } + + // output the roughing passes + setPrintPoint(printPoint); + sliceOutput(sliceOut, { + cutdir, + depthFirst, + easeDown: op.down && easeDown ? 0.001 : 0, + progress: (n,m) => progress(n/m, "routing") + }); + } +} + +export { OpRough }; diff --git a/src2/kiri-mode/cam/op-shadow.js b/src2/kiri-mode/cam/op-shadow.js new file mode 100644 index 00000000..dc35e3ea --- /dev/null +++ b/src2/kiri-mode/cam/op-shadow.js @@ -0,0 +1,112 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; +import { polygons as POLY } from '../../geo/polygons.js'; + +/** + * Computes the Part "shadow" and attaches relevant data to the "state" object + * + * The part shadow consists of top-cown layers at which the polygon shadow changes + * shape. For curved or sloped surfaces, this is approximated and paths that clip + * to it should use the next lower layer from current Z to ensure no part collisions. + * + * The shadow at each layer is computed by top-down unioning the part outline with + * the shadow from the layer above. + * + * This operation is injected at the start of the operation chain before processing. + */ +class OpShadow extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let state = this.state; + let { ops, slicer, widget, unsafe, addSlices, shadowAt } = state; + + let realOps = ops.map(rec => rec.op).filter(op => op); + let trueShadow = state.settings.process.camTrueShadow === true; + + let minStepDown = realOps + .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 skipTerrain = unsafe; + + if (skipTerrain) { + console.log("skipping terrain generation"); + tzindex = [ tzindex.pop() ]; + } + + let lsz; // only shadow up to bottom of last shadow for progressive union + let cnt = 0; + let tot = 0; + + // terrain is the "shadow stack" where index 0 = top of part + // thus array.length -1 = bottom of part + let terrain = await slicer.slice(tzindex, { each: data => { + let shadow = trueShadow ? shadowAt(data.z, lsz) : []; + tshadow = POLY.union(tshadow.slice().appendAll(data.tops).appendAll(shadow), 0.01, true); + tslices.push(data.slice); + // capture current shadow for this slice + data.slice.shadow = tshadow; + if (false) { + const slice = data.slice; + addSlices(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(0.5 + 0.5 * (++cnt / tot)); + }, progress: (index, total) => { + tot = total; + progress((index / total) * 0.5); + } }); + + if (terrain.length === 0) { + throw `invalid widget shadow`; + } + + // TODO: deprecate use of separate shadow vars in state + state.center = tshadow[0].bounds.center(); + state.tshadow = tshadow; // true shadow (base of part) + state.terrain = terrain; // stack of shadow slices stored in tops + state.tslices = tslices; // raw slicer 'data' layer outputs + state.skipTerrain = skipTerrain; + + // TODO: refactor ops to use a unified shadow object + state.shadow = { + base: tshadow, // computed shadow union at base of part + stack: terrain, // stack of shadow slices + slices: tslices, // raw slicer 'data' objects + skip: skipTerrain + }; + + // identify through holes which are inner/child polygons + // on the bottom-most layer of the shadow stack (tshadow, index == 0) + state.thruHoles = tshadow.map(p => p.inner || []).flat(); + } +} + +export { OpShadow }; diff --git a/src2/kiri-mode/cam/op-trace.js b/src2/kiri-mode/cam/op-trace.js new file mode 100644 index 00000000..fcd807f8 --- /dev/null +++ b/src2/kiri-mode/cam/op-trace.js @@ -0,0 +1,290 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; +import { Tool } from './tool.js'; +import { newPolygon } from '../../geo/polygon.js'; +import { newSlice } from '../../kiri/slice.js'; +import { polygons as POLY } from '../../geo/polygons.js'; +import { util as base_util } from '../../geo/base.js'; +import { poly2polyEmit } from '../../geo/paths.js'; +import { newPoint } from '../../geo/point.js'; + +class OpTrace extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + const debug = false; + let { op, state } = this; + let { tool, rate, down, plunge, offset, offover, thru } = op; + let { ov_conv } = op; + let { settings, widget, addSlices, zThru, tabs, workarea } = state; + let { updateToolDiams, cutTabs, cutPolys, healPolys, color, shadowAt } = state; + let { process, stock } = settings; + let { camStockClipTo } = process; + if (state.isIndexed) { + throw 'trace op not supported with indexed stock'; + } + // generate tracing offsets from chosen features + let zTop = workarea.top_z; + let zBottom = workarea.bottom_z; + let sliceOut = this.sliceOut = []; + let areas = op.areas[widget.id] || []; + let camTool = new Tool(settings, tool); + let toolDiam = camTool.fluteDiameter(); + let toolOver = toolDiam * op.step; + let traceOffset = camTool.traceOffset() + let cutdir = ov_conv; + let polys = []; + let reContour = false; + let canRecontour = offset !== 'none' && down === 0; + let stockRect = stock.center && stock.x && stock.y ? + newPolygon().centerRectangle({x:0,y:0}, 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); + let zs = poly.points.map(p => p.z); + let min = Math.min(...zs); + let max = Math.max(...zs); + if (max - min > 0.0001 && canRecontour) { + reContour = true; + } + } + if (false) newSliceOut(0).output() + .setLayer("polys", {line: 0xaaaa00}, false) + .addPolys(polys); + function newSliceOut(z) { + let slice = newSlice(z); + addSlices(slice); + sliceOut.push(slice); + return slice; + } + function minZ(z) { + return zBottom ? Math.max(zBottom, z - thru) : z - thru; + } + function followZ(poly) { + if (op.dogbone) { + addDogbones(poly, toolDiam / 5, !op.revbone); + } + 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 (camStockClipTo && stockRect) { + slice.camLines = cutPolys([stockRect], slice.camLines, z, true); + } + if (reContour) { + state.contourPolys(widget, slice.camLines); + } + slice.output() + .setLayer("trace follow", {line: color}, false) + .addPolys(slice.camLines) + } + function clearZnew(polys, z, down) { + if (down) { + // adjust step down to a value <= down that + // ends on the lowest z specified + let diff = zTop - z; + down = diff / Math.ceil(diff / down); + } + let zs = down ? base_util.lerp(zTop, z, down) : [ z ]; + let zpro = 0, zinc = 1 / (polys.length * zs.length); + for (let poly of polys) { + // newPocket(); + for (let z of zs) { + let clip = [], shadow; + shadow = shadowAt(z); + POLY.subtract([ poly ], shadow, clip, undefined, undefined, 0); + if (op.outline) { + POLY.clearInner(clip); + } + if (clip.length === 0) { + continue; + } + let slice = newSliceOut(z); + let count = 999; + slice.camTrace = { tool, rate, plunge }; + if (toolDiam) { + const offs = [ -toolDiam / 2, -toolOver ]; + POLY.offset(clip, offs, { + count, outs: slice.camLines = [], flat:true, z, minArea: 0 + }); + } else { + // when engraving with a 0 width tip + slice.camLines = clip; + } + if (tabs) { + slice.camLines = cutTabs(tabs, POLY.flatten(slice.camLines, null, true), z); + } else { + slice.camLines = POLY.flatten(slice.camLines, null, true); + } + POLY.setWinding(slice.camLines, cutdir, false); + slice.output() + .setLayer("trace", {line: color}, false) + .addPolys(slice.camLines) + if (debug && shadow) slice.output() + .setLayer("trace shadow", {line: 0xff8811}, false) + .addPolys(shadow) + progress(zpro, "trace"); + zpro += zinc; + addSlices(slice); + } + } + } + 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); + }); + let output = []; + for (let poly of POLY.nest(routed)) { + let offdist = offset !== 'none' ? offover : 0; + if (!offdist) + switch (offset) { + case "outside": offdist = traceOffset; break; + case "inside": offdist = -traceOffset; break; + } else if (offset === "inside") { + offdist = -offdist; + } + if (offdist) { + let pnew = POLY.offset([poly], offdist, { minArea: 0, open: true }); + if (pnew) { + poly = POLY.setZ(pnew, poly.getZ()); + } else { + continue; + } + } else { + poly = [ poly ]; + } + for (let pi of POLY.flatten(poly, [], true)) + if (down) { + let zto = minZ(pi.getZ()); + if (zThru && similar(zto,0)) { + zto -= zThru; + } + for (let z of base_util.lerp(zTop, zto, down)) { + output.push(pi.clone().setZ(z)); + } + } else { + if (thru) { + pi.setZ(pi.getZ() - thru); + } + output.push(pi); + } + if (!down && op.merge) { + let nest = POLY.nest(output); + let union = POLY.union(nest, 0, true); + output = POLY.flatten(union, [], true); + } + } + for (let poly of output) { + followZ(poly); + } + break; + case "clear": + const zbo = widget.track.top - widget.track.box.d; + let zmap = {}; + for (let poly of polys) { + let z = minZ(poly.minZ()); + if (offover) { + let pnew = POLY.offset([poly], -offover, { minArea: 0, open: true }); + if (pnew) { + poly = POLY.setZ(pnew, poly.getZ()); + } else { + continue; + } + } else { + poly = [ poly ]; + } + (zmap[z] = zmap[z] || []).appendAll(poly); + } + for (let [zv, polys] of Object.entries(zmap)) { + clearZnew(polys, parseFloat(zv), down); + } + } + } + + prepare(ops, progress) { + let { op, state } = this; + let { settings } = state; + let { setTool, setSpindle } = ops; + + setTool(op.tool, op.rate); + setSpindle(op.spindle); + for (let slice of this.sliceOut) { + ops.emitTrace(slice); + } + } +} + +export { OpTrace }; diff --git a/src2/kiri-mode/cam/op-xray.js b/src2/kiri-mode/cam/op-xray.js new file mode 100644 index 00000000..cbc9168b --- /dev/null +++ b/src2/kiri-mode/cam/op-xray.js @@ -0,0 +1,32 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; +import { newSlice } from '../../kiri/slice.js'; +import { Slicer } from './slicer.js'; + +class OpXRay extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let { widget, addSlices } = this.state; + let slicer = new Slicer(widget); + let xrayind = Object.keys(slicer.zLine) + .map(v => parseFloat(v).round(5)) + .sort((a,b) => a-b); + let xrayopt = { each: data => { + let slice = newSlice(data.z); + slice.addTops(data.tops); + // data.tops.forEach(top => slice.addTop(top)); + slice.lines = data.lines; + slice.xray(); + addSlices(slice); + }, over: false, flatoff: 0, edges: true, openok: true }; + await slicer.slice(xrayind, xrayopt); + // xrayopt.over = true; + // slicer.slice(xrayind, xrayopt); + } +} + +export { OpXRay }; diff --git a/src2/kiri-mode/cam/op.js b/src2/kiri-mode/cam/op.js new file mode 100644 index 00000000..9b27734c --- /dev/null +++ b/src2/kiri-mode/cam/op.js @@ -0,0 +1,20 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +export class CamOp { + constructor(state, op) { + this.state = state; + this.op = op + } + + type() { + return this.op.type; + } + + weight() { + return 1; + } + + async slice() { } + + prepare() { } +} diff --git a/src2/kiri-mode/cam/ops.js b/src2/kiri-mode/cam/ops.js index 33494f16..2eb1dbd8 100644 --- a/src2/kiri-mode/cam/ops.js +++ b/src2/kiri-mode/cam/ops.js @@ -1,1851 +1,21 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { CAM } from './driver-be.js'; -import { util as base_util } from '../../geo/base.js'; -import { Tool } from './tool.js'; -import { newPoint } from '../../geo/point.js'; -import { newPolygon } from '../../geo/polygon.js'; -import { newSlice } from '../../kiri/slice.js'; -import { polygons as POLY } from '../../geo/polygons.js'; -import { poly2polyEmit, tip2tipEmit, calc_normal as segmentNormal, calc_vertex as vertexNormal } from '../../geo/paths.js'; - -const DEG2RAG = Math.PI / 180; - -class CamOp { - constructor(state, op) { - this.state = state; - this.op = op - } - - type() { - return this.op.type; - } - - weight() { - return 1; - } - - async slice() { } - - prepare() { } -} - -class OpIndex extends CamOp { - constructor(state, op) { - super(state, op); - } - - async slice() { - let { op, state } = this; - if (!state.isIndexed) { - throw 'index op requires indexed stock'; - } - let { widget, updateSlicer, computeShadows, setAxisIndex } = state; - this.degrees = setAxisIndex(op.degrees, op.absolute); - // force recompute of topo - widget.topo = undefined; - updateSlicer(); - await computeShadows(); - } - - prepare(ops, progress) { - const { lastPoint, zmax, zclear, camOut, stock } = ops; - let last = lastPoint(); - if (last) { - // max point of stock corner radius when rotating (safe z when indexing) - const rzmax = (Math.max(stock.y, stock.z) * Math.sqrt(2)) / 2 + zclear; - const zmove = Math.max(rzmax, zmax); - // move above rotating stock - camOut(last = last.clone().setZ(zmove), 0); - // issue rotation command - camOut(last = last.clone().setY(0).setA(this.degrees), 0); - } - } -} - -class OpGCode extends CamOp { - constructor(state, op) { - super(state, op); - } - - prepare(ops, progress) { - ops.addGCode(this.op.gcode); - } -} - -class OpLaserOn extends CamOp { - constructor(state, op) { - super(state, op); - } - - prepare(ops, progress) { - const { printPoint, setPrintPoint, setTool, zmax, camOut } = ops; - this.op.silent = true; - setTool(0); - ops.addGCode(this.op.enable); - ops.setLasering(true, this.op.power); - } -} - -class OpLaserOff extends CamOp { - constructor(state, op) { - super(state, op); - } - - prepare(ops, progress) { - const { printPoint, zmax, camOut } = ops; - this.op.silent = true; - ops.addGCode(this.op.disable); - ops.setLasering(false); - camOut(printPoint.clone().setZ(zmax), 0); - } -} - -class OpLevel extends CamOp { - constructor(state, op) { - super(state, op); - } - - async slice(progress) { - let { op, state } = this; - let { settings, widget, addSlices } = state; - let { updateToolDiams, tabs, cutTabs } = state; - let { bounds, zMax, ztOff, color, tshadow } = state; - let { stock } = settings; - - let toolDiam = new Tool(settings, op.tool).fluteDiameter(); - let stepOver = this.stepOver = toolDiam * op.step; - let z = zMax + ztOff - op.down; - - updateToolDiams(toolDiam); - - let points = []; - let clear = op.stock ? - [ newPolygon().centerRectangle({x:0,y:0,z:0}, stock.x, stock.y) ] : - POLY.outer(POLY.offset(tshadow, toolDiam * (op.over || 0))); - - POLY.fillArea(clear, 90, stepOver, points); - - let lines = this.lines = []; - for (let i=0; i { return { first: p.first(), last: p.last(), poly: p } }); - printPoint = tip2tipEmit(lines, printPoint, (el, point, count) => { - let poly = el.poly; - if (poly.last() === point) { - poly.reverse(); - } - poly.forEachPoint((point, pidx) => { - camOut(point.clone(), true, stepOver); - }, false); - }); - setPrintPoint(printPoint); - - newLayer(); - } -} - -class OpRough extends CamOp { - constructor(state, op) { - super(state, op); - } - - async slice(progress) { - let { op, state } = this; - let { settings, slicer, addSlices, unsafe, color } = state; - let { updateToolDiams, thruHoles, tabs, cutTabs, cutPolys } = state; - let { ztOff, zMax, shadowAt, isIndexed} = state; - let { workarea } = state; - let { process, stock } = settings; - - if (op.down <= 0) { - throw `invalid step down "${op.down}"`; - } - - let roughIn = op.inside; - let roughDown = op.down; - let roughLeave = op.leave || 0; - let roughLeaveZ = op.leavez || 0; - let roughStock = op.all && isIndexed; - let toolDiam = new Tool(settings, op.tool).fluteDiameter(); - let trueShadow = process.camTrueShadow === true; - - updateToolDiams(toolDiam); - - // clear the stock above the area to be roughed out - if (workarea.top_z > workarea.top_part) { - let shadow = state.shadow.base.clone(); - let step = toolDiam * op.step; - let inset = roughStock ? - POLY.offset([ newPolygon().centerRectangle(stock.center, stock.x, stock.y) ], step) : - 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 = newSlice(); - slice.z = z; - slice.camLines = POLY.setZ(facing.clone(true), slice.z + roughLeaveZ); - slice.output() - .setLayer("face", {face: color, line: color}) - .addPolys(slice.camLines); - addSlices(slice); - camFaces.push(slice); - z -= zstep; - } - } - - // create roughing slices - let flats = []; - let shadow = []; - let slices = []; - let indices = slicer.interval(roughDown, { - down: true, min: 0, fit: true, off: 0.01 - }); - - // shift out first (top-most) slice - indices.shift(); - - // find flats and add to indices for slicing - 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 >= workarea.bottom_z); - 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); - } - - indices = indices.filter(v => v >= workarea.bottom_z); - // console.log('indices', ...indices, {zBottom}); - - let cnt = 0; - let tot = 0; - await slicer.slice(indices, { each: data => { - 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; - } - if (data.z > workarea.top_z) { - return; - } - data.shadow = trueShadow ? shadowAt(data.z) : shadow.clone(true); - data.slice.shadow = data.shadow; - slices.push(data.slice); - progress(0.25 + 0.25 * (++cnt / tot)); - }, progress: (index, total) => { - tot = total; - progress((index / total) * 0.25); - } }); - - if (trueShadow) { - shadow = state.shadow.base.clone(true); - } else { - shadow = POLY.union(shadow.appendAll(state.shadow.base), 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)) { - let po = POLY.offset([p], -(toolDiam / 2 + roughLeave + 0.05)); - return po ? po[0] : undefined; - } else { - return p; - } - }).filter(p => p); - }); - shadow = POLY.nest(shadow); - if (op.voids) { - // eliminate voids from shadow when "clear voids" enables - for (let s of shadow) s.inner = undefined; - } - - // shell = shadow expanded by half tool diameter + leave stock - const sadd = roughIn ? toolDiam / 2 : toolDiam / 2; - const shell = roughStock ? - POLY.offset([ newPolygon().centerRectangle(stock.center, stock.x, stock.y) ], sadd) : - 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: Math.min(0.01, toolDiam * op.step / 4), - 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 && !roughStock) { - 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 (roughLeaveZ) { - // offset roughing in Z as well to minimize - // tool marks on curved surfaces - // const roughLeaveZ = 1 * Math.min(roughDown, roughLeave / 2); - slice.camLines.forEach(p => { - p.setZ(p.getZ() + roughLeaveZ); - }); - } - 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); - progress(0.5 + 0.5 * (index / slices.length)); - }); - - let last = slices[slices.length-1]; - - if (workarea.bottom_z < 0) - for (let zneg of base_util.lerp(0, -workarea.bottom_cut, op.down)) { - if (!last) continue; - let add = last.clone(true); - add.z -= zneg; - add.camLines = last.camLines.clone(true); - add.camLines.forEach(p => p.setZ(add.z + roughLeaveZ)); - // add.tops.forEach(top => top.poly.setZ(add.z)); - // add.shadow = last.shadow.clone(true); - slices.push(add); - } - - slices.forEach(slice => { - slice.output() - .setLayer("roughing", {face: color, line: color}) - .addPolys(slice.camLines); - }); - this.sliceOut = slices.filter(slice => slice.camLines); - - addSlices(this.sliceOut); - } - - prepare(ops, progress) { - let { op, state, sliceOut, camFaces } = this; - let { setTool, setSpindle, setPrintPoint, sliceOutput, polyEmit } = ops; - let { camOut, newLayer, printPoint } = ops; - let { settings } = state; - let { process } = settings; - - let easeDown = process.camEaseDown; - let cutdir = op.ov_conv; - let depthFirst = process.camDepthFirst && !state.isIndexed; - let depthData = []; - - setTool(op.tool, op.rate, op.plunge); - setSpindle(op.spindle); - - // output the clearing of stock above roughing - 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); - poly2polyEmit(level, printPoint, (poly, index, count) => { - printPoint = polyEmit(poly, index, count, printPoint); - }); - newLayer(); - } - - // output the roughing passes - setPrintPoint(printPoint); - sliceOutput(sliceOut, { - cutdir, - depthFirst, - easeDown: op.down && easeDown ? 0.001 : 0, - progress: (n,m) => progress(n/m, "routing") - }); - } -} - -class OpOutline extends CamOp { - constructor(state, op) { - super(state, op); - } - - async slice(progress) { - let { op, state } = this; - let { settings, widget, slicer, addSlices, tshadow, thruHoles, unsafe, color } = state; - let { updateToolDiams, tabs, cutTabs, cutPolys, workarea, zMax } = state; - let { process, stock } = settings; - - if (op.down <= 0) { - throw `invalid step down "${op.down}"`; - } - let toolDiam = this.toolDiam = new Tool(settings, op.tool).fluteDiameter(); - updateToolDiams(toolDiam); - - let shadow = []; - let slices = []; - let intopt = { - off: 0.01, - fit: true, - down: true, - min: Math.max(0, workarea.bottom_z), - max: workarea.top_z - }; - let indices = slicer.interval(op.down, intopt); - 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); - - let cnt = 0; - let tot = 0; - 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 - await slicer.slice(indices, { each: data => { - 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 ? 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(0.5 + 0.5 * (++cnt / tot)); - }, progress: (index, total) => { - tot = total; - progress((index / total) * 0.5); - } }); - shadow = POLY.union(shadow.appendAll(state.shadow.base), 0.01, true); - - // start slices at top of stock when `clear top` enabled - if (op.top) { - let first = slices[0]; - let zlist = slices.map(s => s.z); - for (let z of indices.filter(v => v >= zMax)) { - if (zlist.contains(z)) { - continue; - } - let add = first.clone(true); - add.tops.forEach(top => top.poly.setZ(add.z)); - add.shadow = first.shadow.clone(true); - add.z = z; - slices.splice(0,0,add); - } - } - - // extend cut thru (only when z bottom is 0) - if (workarea.bottom_z < 0) { - let last = slices[slices.length-1]; - for (let zneg of base_util.lerp(0, -workarea.bottom_cut, op.down)) { - if (!last) continue; - 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, false, 0.1); - }); - } - } - } - - if (op.omitvoid) { - for (let top of tops) { - delete top.inner; - } - } - - let offset = POLY.expand(tops, toolDiam / 2, slice.z); - if (!(offset && offset.length)) { - return; - } - - // 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 0; c--){ - offset.slice().forEach(op => { - // clone removes inners but the real solution is - // to limit expanded shells to through holes - let wideCut = POLY.expand([op.clone(true)], stepover*c, slice.z, [], 1); - wideCut.forEach(cut =>{ //set order of cuts when wide - cut.order = c - if(cut.inner) cut.inner.forEach(inn =>{ inn.order = c }) - }); - wideCuts.push(...wideCut) - }); - } - offset.appendAll(wideCuts); - } - } - - if (op.dogbones && !op.wide) { - addDogbones(offset, toolDiam / 5); - } - - if (tabs) { - tabs.forEach(tab => { - tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); - }); - offset = cutTabs(tabs, offset, slice.z); - } - - if (process.camStockClipTo && stock.x && stock.y && stock.center) { - let rect = newPolygon().centerRectangle({x:0,y:0}, stock.x, stock.y); - offset = cutPolys([rect], offset, slice.z, true); - } - - // offset.xout(`slice ${slice.z}`); - slice.camLines = offset; - }); - - // when top expand fails above, it creates an empty slice - slices = slices.filter(s => s.camLines); - - // 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); - } - - addSlices(slices); - this.sliceOut = slices; - } - - 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 easeDown = process.camEaseDown; - let toolDiam = this.toolDiam; - let cutdir = op.ov_conv;//process.camConventional; - let depthFirst = process.camDepthFirst; - let depthData = []; - - setTool(op.tool, op.rate, op.plunge); - setSpindle(op.spindle); - - // printpoint becomes NaN in engine mode. not sure why but this fixes it - if(Object.values(printPoint).some(v=>Number.isNaN(v))){ - printPoint = newPoint(0,0,0); - } - - for (let slice of sliceOut) { - let polys = [], t = [], c = []; - let lines =POLY.flatten(slice.camLines) - // console.log(lines); - lines.forEach((poly)=> { - poly.order = poly.order ?? 0; - 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 { - let orderSplit = {} - polys.forEach(poly => { - if(poly.order in orderSplit) orderSplit[poly.order].push(poly); - else orderSplit[poly.order] = [poly]; - }) - Object.entries(orderSplit) //split the polys by order - .sort((a,b) => -(a[0] - b[0] )) //sort by order (highest first) - .forEach(([order, orderPolys]) => { // emit based on closest for each order - let polyLast; - // console.log({order, orderPolys}); - printPoint = poly2polyEmit(orderPolys, printPoint, function(poly, index, count) { - - polyLast = polyEmit(poly, index, count, polyLast); - }, { 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 = op.down && easeDown ? 0.001 : 0; - printPoint = depthOutlinePath(printPoint, 0, flatLevels, toolDiam, polyEmit, false, ease); - printPoint = depthOutlinePath(printPoint, 0, flatLevels, toolDiam, polyEmit, true, ease); - } - } - - setPrintPoint(printPoint); - } -} - -function createFilter(op) { - let filter = slices => slices; - if (op.filter) { - try { - const obj = eval(`( ${op.filter.join('\n')} )`); - let idx = 0; - if (obj && obj.slices) { - const nadd = []; - filter = function(slices) { - for (let slice of slices) { - if (obj.slices(slice, idx++)) { - nadd.push(slice); - } - } - return nadd; - }; - } - } catch (e) { - console.log('filter parse error', e, op.filter); - } - } - return filter; -} - -class OpContour extends CamOp { - constructor(state, op) { - super(state, op); - } - - async slice(progress) { - let { op, state } = this; - let { addSlices } = state; - let filter = createFilter(op); - // 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 = await Topo({ - // onupdate: (update, msg) => { - onupdate: (index, total, msg) => { - progress(index / total, msg); - }, - ondone: (slices) => { - slices = filter(slices); - this.sliceOut = slices; - addSlices(slices); - }, - contour: op, - state: state - }); - // computed if set to 0 - this.tolerance = topo.tolerance; - } - - prepare(ops, progress) { - let { op, state, sliceOut } = this; - let { settings, widget } = state; - let { process } = settings; - - let { setTolerance, setTool, setSpindle, setPrintPoint } = ops; - let { camOut, polyEmit, newLayer, printPoint, lastPoint } = ops; - let { bounds, zmax } = ops; - - let toolDiam = this.toolDiam = new Tool(settings, op.tool).fluteDiameter(); - let stepover = toolDiam * op.step * 2; - let depthFirst = process.camDepthFirst; - let depthData = []; - - setTool(op.tool, op.rate, process.camFastFeedZ); - setSpindle(op.spindle); - setTolerance(this.tolerance); - - printPoint = newPoint(bounds.min.x,bounds.min.y,zmax); - - for (let slice of sliceOut) { - // ignore debug slices - 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(); - } - } - - 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 OpLathe extends CamOp { - constructor(state, op) { - super(state, op); - } - - async slice(progress) { - let { op, state } = this; - let { addSlices } = state; - let filter = createFilter(op); - - this.topo = await Topo4({ - op, - state, - onupdate: (pct, msg) => { - progress(pct, msg); - }, - ondone: (slices) => { - slices = filter(slices); - this.slices = slices; - addSlices(slices, false); - } - }); - } - - prepare(ops, progress) { - let { op, state, slices, topo } = this; - let { settings } = state; - - let { setTool, setSpindle, setPrintPoint } = ops; - let { camOut, newLayer, printPoint } = ops; - let { zmax } = ops; - - let toolDiam = new Tool(settings, op.tool).fluteDiameter(); - let stepover = toolDiam * op.step * 2; - let rez = topo.resolution; - - setTool(op.tool, op.rate, op.plunge); - setSpindle(op.spindle); - - // start top center, X = 0, Y = 0 closest to 4th axis chuck - printPoint = newPoint(0, 0, zmax); - - for (let slice of slices) { - // ignore debug slices - if (!slice.camLines) { - continue; - } - - let last; - for (let path of slice.camLines) { - let latent; - path.forEachPoint((point, pidx) => { - if (last) { - const dz = Math.abs(last.z - point.z); - if (dz < rez) { - // latent point should still be included in - // preview b/c arcs would look like straight lines - latent = point.clone(); - return; - } - if (latent) { - camOut(latent, true, stepover); - latent = undefined; - } - } - camOut(last = point.clone(), pidx > 0, stepover); - }, false); - if (latent) { - camOut(latent, true, stepover); - } - } - - newLayer(); - } - - // move to safe height and reset A axis - let last = ops.lastPoint(); - let amax = (Math.round(last.a / 360) * 360).round(2); - // camOut(last = last.clone().setZ(zmax), 0); - // camOut(last = last.clone().setA(amax), 0); - newLayer(); - ops.addGCode([`G0 Z${zmax.round(2)}`,`G0 A${amax}`,"G92 A0"]); - - setPrintPoint(last); - } -} - -class OpTrace extends CamOp { - constructor(state, op) { - super(state, op); - } - - async slice(progress) { - const debug = false; - let { op, state } = this; - let { tool, rate, down, plunge, offset, offover, thru } = op; - let { ov_conv } = op; - let { settings, widget, addSlices, zThru, tabs, workarea } = state; - let { updateToolDiams, cutTabs, cutPolys, healPolys, color, shadowAt } = state; - let { process, stock } = settings; - let { camStockClipTo } = process; - if (state.isIndexed) { - throw 'trace op not supported with indexed stock'; - } - // generate tracing offsets from chosen features - let zTop = workarea.top_z; - let zBottom = workarea.bottom_z; - let sliceOut = this.sliceOut = []; - let areas = op.areas[widget.id] || []; - let camTool = new Tool(settings, tool); - let toolDiam = camTool.fluteDiameter(); - let toolOver = toolDiam * op.step; - let traceOffset = camTool.traceOffset() - let cutdir = ov_conv; - let polys = []; - let reContour = false; - let canRecontour = offset !== 'none' && down === 0; - let stockRect = stock.center && stock.x && stock.y ? - newPolygon().centerRectangle({x:0,y:0}, 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); - let zs = poly.points.map(p => p.z); - let min = Math.min(...zs); - let max = Math.max(...zs); - if (max - min > 0.0001 && canRecontour) { - reContour = true; - } - } - if (false) newSliceOut(0).output() - .setLayer("polys", {line: 0xaaaa00}, false) - .addPolys(polys); - function newSliceOut(z) { - let slice = newSlice(z); - addSlices(slice); - sliceOut.push(slice); - return slice; - } - function minZ(z) { - return zBottom ? Math.max(zBottom, z - thru) : z - thru; - } - function followZ(poly) { - if (op.dogbone) { - addDogbones(poly, toolDiam / 5, !op.revbone); - } - 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 (camStockClipTo && stockRect) { - slice.camLines = cutPolys([stockRect], slice.camLines, z, true); - } - if (reContour) { - state.contourPolys(widget, slice.camLines); - } - slice.output() - .setLayer("trace follow", {line: color}, false) - .addPolys(slice.camLines) - } - function clearZnew(polys, z, down) { - if (down) { - // adjust step down to a value <= down that - // ends on the lowest z specified - let diff = zTop - z; - down = diff / Math.ceil(diff / down); - } - let zs = down ? base_util.lerp(zTop, z, down) : [ z ]; - let zpro = 0, zinc = 1 / (polys.length * zs.length); - for (let poly of polys) { - // newPocket(); - for (let z of zs) { - let clip = [], shadow; - shadow = shadowAt(z); - POLY.subtract([ poly ], shadow, clip, undefined, undefined, 0); - if (op.outline) { - POLY.clearInner(clip); - } - if (clip.length === 0) { - continue; - } - let slice = newSliceOut(z); - let count = 999; - slice.camTrace = { tool, rate, plunge }; - if (toolDiam) { - const offs = [ -toolDiam / 2, -toolOver ]; - POLY.offset(clip, offs, { - count, outs: slice.camLines = [], flat:true, z, minArea: 0 - }); - } else { - // when engraving with a 0 width tip - slice.camLines = clip; - } - if (tabs) { - slice.camLines = cutTabs(tabs, POLY.flatten(slice.camLines, null, true), z); - } else { - slice.camLines = POLY.flatten(slice.camLines, null, true); - } - POLY.setWinding(slice.camLines, cutdir, false); - slice.output() - .setLayer("trace", {line: color}, false) - .addPolys(slice.camLines) - if (debug && shadow) slice.output() - .setLayer("trace shadow", {line: 0xff8811}, false) - .addPolys(shadow) - progress(zpro, "trace"); - zpro += zinc; - addSlices(slice); - } - } - } - 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); - }); - let output = []; - for (let poly of POLY.nest(routed)) { - let offdist = offset !== 'none' ? offover : 0; - if (!offdist) - switch (offset) { - case "outside": offdist = traceOffset; break; - case "inside": offdist = -traceOffset; break; - } else if (offset === "inside") { - offdist = -offdist; - } - if (offdist) { - let pnew = POLY.offset([poly], offdist, { minArea: 0, open: true }); - if (pnew) { - poly = POLY.setZ(pnew, poly.getZ()); - } else { - continue; - } - } else { - poly = [ poly ]; - } - for (let pi of POLY.flatten(poly, [], true)) - if (down) { - let zto = minZ(pi.getZ()); - if (zThru && similar(zto,0)) { - zto -= zThru; - } - for (let z of base_util.lerp(zTop, zto, down)) { - output.push(pi.clone().setZ(z)); - } - } else { - if (thru) { - pi.setZ(pi.getZ() - thru); - } - output.push(pi); - } - if (!down && op.merge) { - let nest = POLY.nest(output); - let union = POLY.union(nest, 0, true); - output = POLY.flatten(union, [], true); - } - } - for (let poly of output) { - followZ(poly); - } - break; - case "clear": - const zbo = widget.track.top - widget.track.box.d; - let zmap = {}; - for (let poly of polys) { - let z = minZ(poly.minZ()); - if (offover) { - let pnew = POLY.offset([poly], -offover, { minArea: 0, open: true }); - if (pnew) { - poly = POLY.setZ(pnew, poly.getZ()); - } else { - continue; - } - } else { - poly = [ poly ]; - } - (zmap[z] = zmap[z] || []).appendAll(poly); - } - for (let [zv, polys] of Object.entries(zmap)) { - clearZnew(polys, parseFloat(zv), down); - } - } - } - - prepare(ops, progress) { - let { op, state } = this; - let { settings } = state; - let { setTool, setSpindle } = ops; - - setTool(op.tool, op.rate); - setSpindle(op.spindle); - for (let slice of this.sliceOut) { - ops.emitTrace(slice); - } - } -} - -class OpPocket extends CamOp { - constructor(state, op) { - super(state, op); - } - - async slice(progress) { - const pocket = this; - const debug = false; - let { op, state } = this; - let { tool, rate, down, plunge, expand, contour, smooth, tolerance } = op; - let { ov_botz, ov_conv } = op; - let { settings, widget, addSlices, zBottom, zThru, tabs, color } = state; - let { updateToolDiams, cutTabs, healPolys, shadowAt, workarea } = state; - let { process } = settings; - zBottom = ov_botz ? workarea.bottom_stock + ov_botz : zBottom; - // generate tracing offsets from chosen features - let sliceOut; - let pockets = this.pockets = []; - let camTool = new Tool(settings, tool); - let toolDiam = camTool.fluteDiameter(); - let toolOver = toolDiam * op.step; - let cutdir = ov_conv; - let engrave = contour && op.engrave; - let zTop = workarea.top_z; - if (contour) { - down = 0; - this.topo = await Topo({ - // onupdate: (update, msg) => { - onupdate: (index, total, msg) => { - progress((index / total) * 0.9, msg); - }, - ondone: (slices) => { - // console.log({ contour: slices }); - }, - contour: { - tool, - tolerance, - inside: true, - axis: "-" - }, - state: state - }); - } - updateToolDiams(toolDiam); - if (tabs) { - tabs.forEach(tab => { - tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); - }); - } - function newPocket() { - pockets.push(sliceOut = []); - } - function newSliceOut(z) { - let slice = newSlice(z); - sliceOut.push(slice); - return slice; - } - function clearZ(polys, z, down) { - if (down) { - // adjust step down to a value <= down that - // ends on the lowest z specified - let diff = zTop - z; - down = diff / Math.ceil(diff / down); - } - let zs = down ? base_util.lerp(zTop, z, down) : [ z ]; - if (engrave) { - toolDiam = toolOver; - } - if (contour) { - expand = engrave ? 0 : expand; - } else if (expand) { - polys = POLY.offset(polys, expand); - } - let zpro = 0, zinc = 1 / (polys.length * zs.length); - for (let poly of polys) { - newPocket(); - for (let z of zs) { - let clip = [], shadow; - if (contour) { - if (smooth) { - clip = POLY.offset(POLY.offset([ poly ], smooth), -smooth); - } else { - clip = [ poly ]; - } - } else { - shadow = shadowAt(z); - if (smooth) { - shadow = POLY.setZ(POLY.offset(POLY.offset(shadow, smooth), -smooth), z); - } - POLY.subtract([ poly ], shadow, clip, undefined, undefined, 0); - if (op.outline) { - POLY.clearInner(clip); - } - } - if (clip.length === 0) { - continue; - } - let slice = newSliceOut(z); - let count = engrave ? 1 : 999; - slice.camTrace = { tool, rate, plunge }; - if (toolDiam) { - const offs = contour ? - [ expand || (-0.02), -toolOver ] : - [ -toolDiam / 2, -toolOver ]; - POLY.offset(clip, offs, { - count, outs: slice.camLines = [], flat:true, z, minArea: 0 - }); - } else { - // when engraving with a 0 width tip - slice.camLines = clip; - } - if (tabs) { - slice.camLines = cutTabs(tabs, POLY.flatten(slice.camLines, null, true), z); - } else { - slice.camLines = POLY.flatten(slice.camLines, null, true); - } - POLY.setWinding(slice.camLines, cutdir, false); - if (contour) { - slice.camLines = pocket.conform(slice.camLines, op.refine, engrave, pct => { - progress(0.9 + (zpro + zinc * pct) * 0.1, "conform"); - }); - } - slice.output() - .setLayer("pocket", {line: color}, false) - .addPolys(slice.camLines) - if (debug && shadow) slice.output() - .setLayer("pocket shadow", {line: 0xff8811}, false) - .addPolys(shadow) - if (!contour) { - progress(zpro, "pocket"); - } - zpro += zinc; - addSlices(slice); - } - } - } - let surfaces = op.surfaces[widget.id] || []; - let vert = widget.getGeoVertices({ unroll: true, translate: true }).map(v => v.round(4)); - // let vert = widget.getVertices().array.map(v => v.round(4)); - let outline = []; - let faces = CAM.surface_find(widget, surfaces, (op.follow || 5) * DEG2RAG); - let zmin = Infinity; - let j=0, k=faces.length; - for (let face of faces) { - let i = face * 9; - outline.push(newPolygon() - .add(vert[i++], vert[i++], zmin = Math.min(zmin, vert[i++])) - .add(vert[i++], vert[i++], zmin = Math.min(zmin, vert[i++])) - .add(vert[i++], vert[i++], zmin = Math.min(zmin, vert[i++])) - ); - } - zmin = Math.max(zBottom, zmin); - outline = POLY.union(outline, 0.0001, true); - outline = POLY.setWinding(outline, cutdir, false); - outline = healPolys(outline); - if (smooth) { - outline = POLY.offset(POLY.offset(outline, smooth), -smooth); - } - if (outline.length) { - // option to skip interior features (holes, pillars) - if (op.outline) { - POLY.clearInner(outline); - } - if (debug) newSliceOut(zmin).output() - .setLayer("pocket area", {line: 0x1188ff}, false) - .addPolys(outline) - clearZ(outline, zmin + 0.0001, down); - progress(1, "pocket"); - } - } - - // mold cam output lines to the surface of the topo offset by tool geometry - conform(camLines, refine, engrave, progress) { - const topo = this.topo; - // re-segment polygon to a higher resolution - const hirez = camLines.map(p => p.segment(topo.tolerance * 2)); - // walk points and offset from surface taking into account tool geometry - let steps = hirez.length; - let iter = 0; - for (let poly of hirez) { - for (let point of poly.points) { - point.z = engrave ? topo.zAtXY(point.x, point.y) : topo.toolAtXY(point.x, point.y); - } - progress((iter++ / steps) * 0.8); - } - steps = steps * refine; - iter = 0; - // walk points noting z deltas and smoothing z sawtooth patterns - for (let j=0; j p.midpoints(topo.tolerance * 8)); - return hirez; - } - - prepare(ops, progress) { - let { op, state, pockets } = this; - let { setTool, setSpindle, setTolerance, sliceOutput, getPrintPoint } = ops; - let { process } = state.settings; - - setTool(op.tool, op.rate); - setSpindle(op.spindle); - - if (this.topo) { - setTolerance(this.topo.tolerance); - } - - // eliminate empty pockets - pockets = pockets.filter(p => p.length); - - // pockets is an [ array of an [ array of slices ] ] - // each top level array is a pocket containing a [ z layer array of slices ] - // follow each pocket to the next closest one from previous exit - for (;;) { - let printPoint = getPrintPoint(); - let min = { - dist: Infinity, - pocket: undefined - }; - for (let pocket of pockets.filter(p => !p.used)) { - let poly = pocket[0].camLines.slice().sort((a,b) => b.area() - a.area())[0]; - if (!poly) continue; - let find = poly.findClosestPointTo(printPoint); - if (find.distance < min.dist) { - min.pocket = pocket; - min.dist = find.distance; - } - } - if (min.pocket) { - min.pocket.used = true; - sliceOutput(min.pocket, { - cutdir: op.ov_conv, - depthFirst: process.camDepthFirst && !state.isIndexed, - easeDown: op.down && process.easeDown ? op.down : 0, - progress: (n,m) => progress(n/m, "pocket") - }); - } else { - break; - } - } - } -} - -class OpDrill extends CamOp { - constructor(state, op) { - super(state, op); - } - - async slice(progress) { - let { op, state } = this; - let { settings, addSlices, widget, updateToolDiams } = state; - let { zBottom, zThru, thruHoles, color } = state; - let { drills, drillThrough} = op - - let drillTool = new Tool(settings, op.tool), - drillToolDiam = drillTool.fluteDiameter(), - sliceOut = this.sliceOut = []; - - updateToolDiams(drillToolDiam); - - const allDrills = drills[widget.id] ?? [] - // drill points to use center (average of all points) of the polygon - allDrills.forEach((drill)=> { - if(!drill.selected){ - return - } - let slice = newSlice(0); - if (op.mark) { - // replace depth with single down peck - drill.depth = op.down - } - drill.zBottom = drill.z - drill.depth; - // for thru holes, follow z thru when set - if ((op.thru>0) ) { - drill.zBottom -= op.thru; - } - const poly = newPolygon() - poly.points.push(newPoint(drill.x, drill.y, drill.z)) - poly.points.push(newPoint(drill.x, drill.y, drill.zBottom)) - // poly.points.pop(); - slice.camTrace = { tool: op.tool, rate: op.feed, plunge: op.rate }; - slice.camLines = [poly]; - - slice.output() - .setLayer("drill", {face: color, line: color}) - .addPolys(slice.camLines); - addSlices(slice); - sliceOut.push(slice); - }); - } - - prepare(ops, progress) { - let { op, state } = this; - let { settings, widget, addSlices, updateToolDiams } = state; - let { setTool, setSpindle, setDrill, emitDrills } = ops; - setTool(op.tool, undefined, op.rate); - setDrill(op.down, op.lift, op.dwell,op.thru); - setSpindle(op.spindle); - emitDrills(this.sliceOut.map(slice => slice.camLines).flat()); - } -} - -class OpRegister extends CamOp { - constructor(state, op) { - super(state, op); - } - - async slice(progress) { - let { op, state } = this; - let { settings, widget, bounds, addSlices, zMax, zThru, color } = state; - let { updateToolDiams } = state; - - let tool = new 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 = op.thru || 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); - addSlices(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: color}, 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; - slice.output() - .setLayer("register", {face: color, line: color}) - .addPolys(polys); - addSlices(slice); - sliceOut.push(slice); - } - } - - prepare(ops, progress) { - let { op, state } = this; - let { settings, widget, addSlices, 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); - } - } 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 OpXRay extends CamOp { - constructor(state, op) { - super(state, op); - } - - async slice(progress) { - let { widget, addSlices } = this.state; - let slicer = new kiri.cam_slicer(widget); - let xrayind = Object.keys(slicer.zLine) - .map(v => parseFloat(v).round(5)) - .sort((a,b) => a-b); - let xrayopt = { each: data => { - let slice = newSlice(data.z); - slice.addTops(data.tops); - // data.tops.forEach(top => slice.addTop(top)); - slice.lines = data.lines; - slice.xray(); - addSlices(slice); - }, over: false, flatoff: 0, edges: true, openok: true }; - await slicer.slice(xrayind, xrayopt); - // xrayopt.over = true; - // slicer.slice(xrayind, xrayopt); - } -} - -/** - * Computes the Part "shadow" and attaches relevant data to the "state" object - * - * The part shadow consists of top-cown layers at which the polygon shadow changes - * shape. For curved or sloped surfaces, this is approximated and paths that clip - * to it should use the next lower layer from current Z to ensure no part collisions. - * - * The shadow at each layer is computed by top-down unioning the part outline with - * the shadow from the layer above. - * - * This operation is injected at the start of the operation chain before processing. - */ -class OpShadow extends CamOp { - constructor(state, op) { - super(state, op); - } - - async slice(progress) { - let state = this.state; - let { ops, slicer, widget, unsafe, addSlices, shadowAt } = state; - - let realOps = ops.map(rec => rec.op).filter(op => op); - let trueShadow = state.settings.process.camTrueShadow === true; - - let minStepDown = realOps - .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 skipTerrain = unsafe; - - if (skipTerrain) { - console.log("skipping terrain generation"); - tzindex = [ tzindex.pop() ]; - } - - let lsz; // only shadow up to bottom of last shadow for progressive union - let cnt = 0; - let tot = 0; - - // terrain is the "shadow stack" where index 0 = top of part - // thus array.length -1 = bottom of part - let terrain = await slicer.slice(tzindex, { each: data => { - let shadow = trueShadow ? shadowAt(data.z, lsz) : []; - tshadow = POLY.union(tshadow.slice().appendAll(data.tops).appendAll(shadow), 0.01, true); - tslices.push(data.slice); - // capture current shadow for this slice - data.slice.shadow = tshadow; - if (false) { - const slice = data.slice; - addSlices(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(0.5 + 0.5 * (++cnt / tot)); - }, progress: (index, total) => { - tot = total; - progress((index / total) * 0.5); - } }); - - if (terrain.length === 0) { - throw `invalid widget shadow`; - } - - // TODO: deprecate use of separate shadow vars in state - state.center = tshadow[0].bounds.center(); - state.tshadow = tshadow; // true shadow (base of part) - state.terrain = terrain; // stack of shadow slices stored in tops - state.tslices = tslices; // raw slicer 'data' layer outputs - state.skipTerrain = skipTerrain; - - // TODO: refactor ops to use a unified shadow object - state.shadow = { - base: tshadow, // computed shadow union at base of part - stack: terrain, // stack of shadow slices - slices: tslices, // raw slicer 'data' objects - skip: skipTerrain - }; - - // identify through holes which are inner/child polygons - // on the bottom-most layer of the shadow stack (tshadow, index == 0) - state.thruHoles = tshadow.map(p => p.inner || []).flat(); - } -} +import { OpXRay } from './op-xray.js'; +import { OpShadow } from './op-shadow.js'; +import { OpLevel } from './op-level.js'; +import { OpRough } from './op-rough.js'; +import { OpOutline } from './op-outline.js'; +import { OpContour } from './op-contour.js'; +import { OpPocket } from './op-pocket.js'; +import { OpLathe } from './op-lathe.js'; +import { OpTrace } from './op-trace.js'; +import { OpDrill } from './op-drill.js'; +import { OpRegister } from './op-register.js'; +import { OpLaserOn } from './op-laser-on.js'; +import { OpLaserOff } from './op-laser-off.js'; +import { OpGCode } from './op-gcode.js'; +import { OpIndex } from './op-index.js'; +import { CamOp } from './op.js'; export const ops = { "xray": OpXRay,