grid-apps-cmms/src/mode/cam/ops.js
2021-01-17 22:23:51 -05:00

1129 lines
44 KiB
JavaScript

/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
(function() {
let KIRI = self.kiri,
BASE = self.base,
UTIL = BASE.util,
CAM = KIRI.driver.CAM,
PRO = CAM.process,
POLY = BASE.polygons,
newSlice = KIRI.newSlice,
newPoint = BASE.newPoint,
newPolygon = BASE.newPolygon;
class CamOp {
constructor(state, op) {
this.state = state;
this.op = op
}
type() {
return this.op.type;
}
weight() {
return 1;
}
slice() { }
prepare() { }
}
class OpLevel extends CamOp {
constructor(state, op) {
super(state, op);
}
slice(progress) {
let { op, state } = this;
let { settings, widget, sliceAll } = state;
let { updateToolDiams, thruHoles, tabs, cutTabs } = state;
let { bounds, zMax, ztOff } = state;
let { stock } = settings;
let toolDiam = new CAM.Tool(settings, op.tool).fluteDiameter();
let stepOver = toolDiam * op.step;
updateToolDiams(toolDiam);
let path = newPolygon().setOpen(),
center = {x:0, y:0, z:0},
x1 = bounds.min.x,
y1 = bounds.min.y,
x2 = bounds.max.x,
y2 = bounds.max.y,
z = bounds.max.z - (op.down || 0);
if (stock.x && stock.y && stock.z) {
x1 = -stock.x / 2;
y1 = -stock.y / 2;
x2 = -x1;
y2 = -y1;
z = zMax + ztOff - (op.down || 0);
}
let ei = 0,
xd = x2 - x1,
xi = xd / Math.floor(xd / stepOver);
for (let x = x1, lx = x, ei = 0; x <= x2; x += xi, ei++, lx = x) {
if (ei % 2 === 0) {
path.add(x,y1,z).add(x,y2,z);
} else {
path.add(x,y2,z).add(x,y1,z);
}
}
let slice = newSlice(z);
this.lines = slice.camLines = [ path ];
slice.output()
.setLayer("level", {face: 0, line: 0})
.addPolys(this.lines);
sliceAll.push(slice);
}
prepare(ops, progress) {
let { op, state, lines } = this;
let { setTool, setSpindle } = ops;
let { polyEmit, newLayer } = ops;
setTool(op.tool, op.rate);
setSpindle(op.spindle);
polyEmit(lines[0]);
newLayer();
}
}
class OpRough extends CamOp {
constructor(state, op) {
super(state, op);
}
slice(progress) {
let { op, state } = this;
let { settings, widget, slicer, sliceAll, unsafe } = state;
let { updateToolDiams, thruHoles, tabs, cutTabs, cutPolys } = state;
let { tshadow, shadowTop, ztOff, zBottom, zMax } = state;
let { process, stock } = settings;
let roughIn = op.inside;
let roughTop = op.top;
let roughDown = op.down;
let roughLeave = op.leave;
let toolDiam = new CAM.Tool(settings, op.tool).fluteDiameter();
// create facing slices
if (roughTop) {
let shadow = tshadow.clone();
let inset = POLY.offset(shadow, (toolDiam / (roughIn ? 2 : 1)));
let facing = POLY.offset(inset, -(toolDiam * op.step), { count: 999, flat: true });
let zdiv = ztOff / roughDown;
let zstep = (zdiv % 1 > 0) ? ztOff / (Math.floor(zdiv) + 1) : roughDown;
if (ztOff === 0) {
// compensate for lack of z top offset in this scenario
ztOff = zstep;
}
let zsteps = Math.round(ztOff / zstep);
let camFaces = this.camFaces = [];
let zstart = zMax + ztOff - zstep;
for (let z = zstart; zsteps > 0; zsteps--) {
let slice = shadowTop.slice.clone(false);
slice.z = z;
slice.camLines = POLY.setZ(facing.clone(true), slice.z);
slice.output()
.setLayer("face", {face: 0, line: 0})
.addPolys(slice.camLines);
sliceAll.push(slice);
camFaces.push(slice);
z -= zstep;
}
}
// create roughing slices
updateToolDiams(toolDiam);
let flats = [];
let shadow = [];
let slices = [];
let indices = slicer.interval(roughDown, {
down: true, min: zBottom, fit: true, off: 0.01
});
// shift out first (top-most) slice
indices.shift();
if (op.flats) {
let flats = Object.keys(slicer.zFlat)
.map(v => parseFloat(v).round(4))
.filter(v => v >= zBottom);
flats.forEach(v => {
if (!indices.contains(v)) {
indices.push(v);
}
});
indices = indices.sort((a,b) => { return b - a });
// if layer is not on a flat and next one is,
// then move this layer up to mid-point to previous layer
// this is not perfect. the best method is to interpolate
// between flats so that each step is < step down. on todo list
for (let i=1; i<indices.length-1; i++) {
const prev = indices[i-1];
const curr = indices[i];
const next = indices[i+1];
if (!flats.contains(curr) && flats.contains(next)) {
// console.log('move',curr,'up toward',prev,'b/c next',next,'is flat');
indices[i] = next + ((prev - next) / 2);
}
}
} else {
// add flats to shadow
flats = Object.keys(slicer.zFlat)
.map(v => (parseFloat(v) - 0.01).round(5))
.filter(v => v > 0 && indices.indexOf(v) < 0);
indices = indices.appendAll(flats).sort((a,b) => b-a);
}
// console.log('indices', ...indices, {zBottom});
slicer.slice(indices, { each: (data, index, total) => {
shadow = unsafe ? data.tops : POLY.union(shadow.slice().appendAll(data.tops), 0.01, true);
if (flats.indexOf(data.z) >= 0) {
// exclude flats injected to complete shadow
return;
}
data.shadow = shadow.clone(true);
data.slice.shadow = data.shadow;
// data.slice.tops[0].inner = data.shadow;
// data.slice.tops[0].inner = POLY.setZ(tshadow.clone(true), data.z);
slices.push(data.slice);
progress((index / total) * 0.5);
}, genso: true });
shadow = POLY.union(shadow.appendAll(shadowTop.tops), 0.01, true);
// inset or eliminate thru holes from shadow
shadow = POLY.flatten(shadow.clone(true), [], true);
thruHoles.forEach(hole => {
shadow = shadow.map(p => {
if (p.isEquivalent(hole)) {
// eliminate thru holes when roughing voids enabled
if (op.voids) {
return undefined;
}
let po = POLY.offset([p], -(toolDiam / 2 + roughLeave + 0.01));
return po ? po[0] : undefined;
} else {
return p;
}
}).filter(p => p);
});
shadow = POLY.nest(shadow);
// expand shadow by half tool diameter + stock to leave
const sadd = roughIn ? toolDiam / 2 : toolDiam / 2;
const shell = POLY.offset(shadow, sadd + roughLeave);
slices.forEach((slice, index) => {
let offset = [shell.clone(true),slice.shadow.clone(true)].flat();
let flat = POLY.flatten(offset, [], true);
let nest = POLY.setZ(POLY.nest(flat), slice.z);
// inset offset array by 1/2 diameter then by tool overlap %
offset = POLY.offset(nest, [-(toolDiam / 2 + roughLeave), -toolDiam * op.step], {
minArea: 0,
z: slice.z,
count: 999,
flat: true,
call: (polys, count, depth) => {
// used in depth-first path creation
polys.forEach(p => {
p.depth = depth;
if (p.inner) {
p.inner.forEach(p => p.depth = depth);
}
});
}
}) || [];
// add outside pass if not inside only
if (!roughIn) {
const outside = POLY.offset(shadow.clone(), toolDiam / 2, {z: slice.z});
if (outside) {
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) {
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 operim = op.perimeter();
let newinner = [];
op.inner.forEach(oi => {
if (Math.abs(oi.perimeter() - operim) > 0.01) {
newinner.push(oi);
}
});
op.inner = newinner;
}
});
slice.camLines = offset;
if (false) slice.output()
.setLayer("slice", {line: 0xaaaa00}, true)
.addPolys(slice.topPolys())
// .setLayer("top shadow", {line: 0x0000aa})
// .addPolys(tshadow)
// .setLayer("rough shadow", {line: 0x00aa00})
// .addPolys(shadow)
// .setLayer("rough shell", {line: 0xaa0000})
// .addPolys(shell);
slice.output()
.setLayer("roughing", {face: 0, line: 0})
.addPolys(offset);
progress(0.5 + (index / slices.length) * 0.5);
});
this.sliceOut = slices.filter(slice => slice.camLines);
sliceAll.appendAll(this.sliceOut);
}
prepare(ops, progress) {
let { op, state, sliceOut, camFaces } = this;
let { setTool, setSpindle, setPrintPoint } = ops;
let { polyEmit, poly2polyEmit, depthRoughPath } = ops;
let { camOut, newLayer, printPoint } = ops;
let { settings, widget } = state;
let { process } = settings;
let cutdir = process.camConventional;
let depthFirst = process.camDepthFirst;
let depthData = [];
setTool(op.tool, op.rate, op.plunge);
setSpindle(op.spindle);
for (let slice of (camFaces || [])) {
const level = [];
for (let poly of slice.camLines) {
level.push(poly);
if (poly.inner) {
poly.inner.forEach(function(inner) {
level.push(inner);
});
}
}
// set winding specified in output
POLY.setWinding(level, cutdir, false);
printPoint = poly2polyEmit(level, printPoint, function(poly, index, count) {
poly.forEachPoint(function(point, pidx, points, offset) {
camOut(point.clone(), offset !== 0);
}, true, index);
});
newLayer();
}
for (let slice of sliceOut) {
let polys = [], t = [], c = [];
POLY.flatten(slice.camLines).forEach(function (poly) {
let child = poly.parent;
if (depthFirst) { poly = poly.clone(); poly.parent = child ? 1 : 0 }
if (child) c.push(poly); else t.push(poly);
poly.layer = depthData.layer;
polys.push(poly);
});
// set cut direction on outer polys
POLY.setWinding(t, cutdir);
// set cut direction on inner polys
POLY.setWinding(c, !cutdir);
if (depthFirst) {
depthData.push(polys);
} else {
printPoint = poly2polyEmit(polys, printPoint, function(poly, index, count) {
poly.forEachPoint(function(point, pidx, points, offset) {
camOut(point.clone(), offset !== 0);
}, poly.isClosed(), index);
});
newLayer();
}
}
if (depthFirst) {
let tops = depthData.map(level => {
// return POLY.nest(level.filter(poly => poly.depth === 0));
return POLY.nest(level.filter(poly => poly.depth === 0).clone());
});
printPoint = depthRoughPath(printPoint, 0, depthData, tops, polyEmit);
// printPoint = depthRoughPath(printPoint, 0, depthData, tops, (poly, index, count, start) => {
// console.log({z: poly.getZ(), i: poly.id, index, poly});
// return polyEmit(poly, index, count, start);
// });
}
setPrintPoint(printPoint);
}
}
class OpOutline extends CamOp {
constructor(state, op) {
super(state, op);
}
slice(progress) {
let { op, state } = this;
let { settings, widget, slicer, sliceAll, tshadow, thruHoles, unsafe } = state;
let { updateToolDiams, zThru, zBottom, shadowTop, tabs, cutTabs, cutPolys } = state;
let { process, stock } = settings;
let toolDiam = this.toolDiam = new CAM.Tool(settings, op.tool).fluteDiameter();
updateToolDiams(toolDiam);
let shadow = [];
let slices = [];
let indices = slicer.interval(op.down, { down: true, min: zBottom, fit: true, off: 0.01 });
// shift out first (top-most) slice
indices.shift();
// add flats to shadow
const flats = Object.keys(slicer.zFlat)
.map(v => (parseFloat(v) - 0.01).round(5))
.filter(v => v > 0 && indices.indexOf(v) < 0);
indices = indices.appendAll(flats).sort((a,b) => b-a);
// console.log('indices', ...indices, {zBottom, slicer});
if (op.outside && !op.inside) {
console.log({outline_bypass: indices, down: op.down});
indices.forEach((ind,i) => {
if (flats.indexOf(ind) >= 0) {
// exclude flats
return;
}
let slice = newSlice(ind);
slice.shadow = shadow.clone(true);
slices.push(slice);
});
} else
slicer.slice(indices, { each: (data, index, total) => {
shadow = unsafe ? data.tops : POLY.union(shadow.slice().appendAll(data.tops), 0.01, true);
if (flats.indexOf(data.z) >= 0) {
// exclude flats injected to complete shadow
return;
}
data.shadow = shadow.clone(true);
data.slice.shadow = data.shadow;
// data.slice.tops[0].inner = data.shadow;
// data.slice.tops[0].inner = POLY.setZ(tshadow.clone(true), data.z);
slices.push(data.slice);
// data.slice.xray();
// onupdate(0.2 + (index/total) * 0.1, "outlines");
progress((index / total) * 0.5);
}, genso: true });
shadow = POLY.union(shadow.appendAll(shadowTop.tops), 0.01, true);
// extend cut thru (only when z bottom is 0)
if (zThru) {
let last = slices[slices.length-1];
for (let zneg of BASE.util.lerp(0, zThru, op.down)) {
let add = last.clone(true);
add.tops.forEach(top => top.poly.setZ(add.z));
add.shadow = last.shadow.clone(true);
add.z -= zneg;
slices.push(add);
}
}
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);
});
}
}
}
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<shell.length; sp++) {
// eliminate shell only polys
if (poly.isEquivalent(shell[sp])) {
if (poly.inner) return poly.inner;
return null;
}
}
return poly;
});
} else {
if (op.wide) {
let stepover = toolDiam * op.step;
offset.slice().forEach(op => {
// clone removes inners but the real solution is
// to limit expanded shells to through holes
POLY.expand([op.clone(true)], stepover, slice.z, offset, 1);
});
}
}
if (tabs) {
tabs.forEach(tab => {
tab.off = POLY.expand([tab.poly], toolDiam / 2).flat();
});
offset = cutTabs(tabs, offset, slice.z);
}
if (op.dogbones && !op.wide) {
CAM.addDogbones(offset, toolDiam / 5);
}
if (process.camStockClipTo) {
let rect = newPolygon().centerRectangle(stock.center, stock.x, stock.y);
offset = cutPolys([rect], offset, slice.z, true);
}
// offset.xout(`slice ${slice.z}`);
slice.camLines = offset;
if (false) slice.output()
.setLayer("slice", {line: 0xaaaa00}, false)
.addPolys(slice.topPolys())
slice.output()
.setLayer("outline", {face: 0, line: 0})
.addPolys(offset);
});
sliceAll.appendAll(slices);
this.sliceOut = slices;
}
prepare(ops, progress) {
let { op, state, sliceOut } = this;
let { setTool, setSpindle, setPrintPoint } = ops;
let { polyEmit, poly2polyEmit, depthOutlinePath } = ops;
let { camOut, newLayer, printPoint } = ops;
let { settings, widget } = state;
let { process } = settings;
let toolDiam = this.toolDiam;
let cutdir = process.camConventional;
let depthFirst = process.camDepthFirst;
let depthData = [];
setTool(op.tool, op.rate, op.plunge);
setSpindle(op.spindle);
if (!process.camOutlinePocket) {
cutdir = !cutdir;
}
for (let slice of sliceOut) {
let polys = [], t = [], c = [];
POLY.flatten(slice.camLines).forEach(function (poly) {
let child = poly.parent;
if (depthFirst) { poly = poly.clone(); poly.parent = child ? 1 : 0 }
if (child) c.push(poly); else t.push(poly);
poly.layer = depthData.layer;
polys.push(poly);
});
// set cut direction on outer polys
POLY.setWinding(t, cutdir);
// set cut direction on inner polys
POLY.setWinding(c, !cutdir);
if (depthFirst) {
depthData.push(polys);
} else {
printPoint = poly2polyEmit(polys, printPoint, function(poly, index, count) {
poly.forEachPoint(function(point, pidx, points, offset) {
camOut(point.clone(), offset !== 0);
}, poly.isClosed(), index);
});
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();
printPoint = depthOutlinePath(printPoint, 0, flatLevels, toolDiam, polyEmit, false);
printPoint = depthOutlinePath(printPoint, 0, flatLevels, toolDiam, polyEmit, true);
}
}
setPrintPoint(printPoint);
}
}
class OpContour extends CamOp {
constructor(state, op) {
super(state, op);
}
slice(progress) {
let { op, state } = this;
let { sliceAll } = state;
// we need topo for safe travel moves when roughing and outlining
// not generated when drilling-only. then all z moves use bounds max.
// also generates x and y contouring when selected
let topo = new CAM.Topo({
// onupdate: (update, msg) => {
onupdate: (index, total, msg) => {
progress(index / total, msg);
},
ondone: (slices) => {
this.sliceOut = slices;
sliceAll.appendAll(slices);
},
contour: op,
state: state
});
// computed if set to 0
this.tolerance = topo.tolerance;
}
prepare(ops, progress) {
let { op, state, sliceOut } = this;
let { setTolerance, setTool, setSpindle, setPrintPoint } = ops;
let { polyEmit, poly2polyEmit, tip2tipEmit } = ops;
let { camOut, newLayer, printPoint, lastPoint } = ops;
let { bounds, zmax } = ops;
let { settings, widget } = state;
let { process } = settings;
let toolDiam = this.toolDiam;
let cutdir = process.camConventional;
let depthFirst = process.camDepthFirst;
let depthData = [];
setTool(op.tool, op.rate, op.plunge);
setSpindle(op.spindle);
setTolerance(this.tolerance);
printPoint = newPoint(bounds.min.x,bounds.min.y,zmax);
for (let slice of sliceOut) {
if (!slice.camLines) continue;
let polys = [], poly, emit;
slice.camLines.forEach(function (poly) {
if (depthFirst) poly = poly.clone(true);
polys.push({first:poly.first(), last:poly.last(), poly:poly});
});
if (depthFirst) {
depthData.appendAll(polys);
} else {
printPoint = tip2tipEmit(polys, printPoint, function(el, point, count) {
poly = el.poly;
if (poly.last() === point) {
poly.reverse();
}
poly.forEachPoint(function(point, pidx) {
camOut(point.clone(), pidx > 0);
}, 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);
}, false);
newLayer();
return lastPoint();
});
}
setPrintPoint(printPoint);
}
}
class OpTrace extends CamOp {
constructor(state, op) {
super(state, op);
}
slice(progress) {
let { op, state } = this;
let { tool, rate, down, plunge, offset } = op;
let { settings, widget, sliceAll, zMax, tabs } = state;
let { updateToolDiams, cutTabs, cutPolys, healPolys } = state;
let { process, stock } = settings;
// generate tracing offsets from chosen features
let sliceOut = this.sliceOut = [];
let areas = op.areas[widget.id] || [];
let toolDiam = new CAM.Tool(settings, tool).fluteDiameter();
let toolOver = toolDiam * op.step;
let cutdir = process.camConventional;
let polys = [];
let stockRect = newPolygon().centerRectangle(stock.center, stock.x, stock.y);
updateToolDiams(toolDiam);
if (tabs) {
tabs.forEach(tab => {
tab.off = POLY.expand([tab.poly], toolDiam / 2).flat();
});
}
for (let arr of areas) {
let poly = newPolygon().fromArray(arr);
POLY.setWinding([ poly ], cutdir, false);
polys.push(poly);
}
function newSliceOut(z) {
let slice = newSlice(z);
sliceAll.push(slice);
sliceOut.push(slice);
return slice;
}
function followZ(poly) {
let z = poly.getZ();
let slice = newSliceOut(z);
slice.camTrace = { tool, rate, plunge };
if (tabs) {
slice.camLines = cutTabs(tabs, [poly], z);
} else {
slice.camLines = [ poly ];
}
if (process.camStockClipTo) {
slice.camLines = cutPolys([stockRect], slice.camLines, z, true);
}
slice.output()
.setLayer("follow", {line: 0xaa00aa}, false)
.addPolys(slice.camLines)
}
function clearZ(polys, z, down) {
let zs = down ? BASE.util.lerp(zMax, z, down) : [ z ];
let nested = POLY.nest(polys);
for (let poly of nested) {
for (let z of zs) {
let slice = newSliceOut(z);
slice.camTrace = { tool, rate, plunge };
POLY.offset([ poly ], -toolOver, {
count:999, outs: slice.camLines = [], flat:true, z
});
if (tabs) {
slice.camLines = cutTabs(tabs, POLY.flatten(slice.camLines, null, true), z);
} else {
slice.camLines = POLY.flatten(slice.camLines, null, true);
}
POLY.setWinding(slice.camLines, cutdir, false);
slice.output()
.setLayer("clear", {line: 0xaa00aa}, false)
.addPolys(slice.camLines)
}
}
}
// connect selected segments if open and touching
polys = healPolys(polys);
switch (op.mode) {
case "follow":
for (let poly of polys) {
let offdist = 0;
switch (offset) {
case "outside": offdist = toolDiam / 2; break;
case "inside": offdist = -toolDiam / 2; break;
}
if (offdist) {
let pnew = POLY.offset([poly], offdist)[0];
if (pnew) {
poly = pnew.setZ(poly.getZ());
} else {
continue;
}
}
if (down) {
for (let z of BASE.util.lerp(zMax, poly.getZ(), down)) {
followZ(poly.clone().setZ(z));
}
} else {
followZ(poly);
}
}
break;
case "clear":
let zmap = {};
for (let poly of polys) {
let z = poly.getZ();
(zmap[z] = zmap[z] || []).push(poly);
}
for (let [zv, polys] of Object.entries(zmap)) {
clearZ(polys, parseFloat(zv), down);
}
}
}
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 OpDrill extends CamOp {
constructor(state, op) {
super(state, op);
}
slice(progress) {
let { op, state } = this;
let { settings, widget, sliceAll, tslices, updateToolDiams, zBottom } = state;
let drills = [],
drillTool = new CAM.Tool(settings, op.tool),
drillToolDiam = drillTool.fluteDiameter(),
centerDiff = drillToolDiam * 0.1,
area = (drillToolDiam/2) * (drillToolDiam/2) * Math.PI,
areaDelta = area * 0.05,
sliceOut = this.sliceOut = [];
updateToolDiams(drillToolDiam);
// for each slice, look for polygons with 98.5% circularity whose
// area is within the tolerance of a circle matching the tool diameter
tslices.forEach(function(slice) {
if (slice.z < zBottom) return;
let inner = slice.topPolyInners([]);
inner.forEach(function(poly) {
if (poly.circularity() >= 0.985 && Math.abs(poly.area() - area) <= areaDelta) {
let center = poly.circleCenter(),
merged = false,
closest = Infinity,
dist;
// TODO reject if inside camShellPolys (means there is material above)
// if (center.isInPolygon(camShellPolys)) return;
drills.forEach(function(drill) {
if (merged) return;
if ((dist = drill.last().distTo2D(center)) <= centerDiff) {
merged = true;
drill.push(center);
}
closest = Math.min(closest,dist);
});
if (!merged) {
drills.push(newPolygon().append(center));
}
}
});
});
// drill points to use center (average of all points) of the polygon
drills.forEach(function(drill) {
let center = drill.center(true),
slice = newSlice(0,null);
drill.points.forEach(function(point) {
point.x = center.x;
point.y = center.y;
});
slice.camLines = [ drill ];
slice.output()
.setLayer("drill", {face: 0, line: 0})
.addPolys(drill);
sliceAll.push(slice);
sliceOut.push(slice);
});
}
prepare(ops, progress) {
let { op, state } = this;
let { settings, widget, sliceAll, tslices, updateToolDiams } = state;
let { setTool, setSpindle, setDrill, emitDrills } = ops;
setTool(op.tool, undefined, op.rate);
setDrill(op.down, op.lift, op.dwell);
setSpindle(op.spindle);
emitDrills(this.sliceOut.map(slice => slice.camLines).flat());
}
}
class OpRegister extends CamOp {
constructor(state, op) {
super(state, op);
}
slice(progress) {
let { op, state } = this;
let { settings, widget, bounds, sliceAll, tslices, zMax, zThru } = state;
let { updateToolDiams } = state;
let tool = new CAM.Tool(settings, op.tool);
let sliceOut = this.sliceOut = [];
updateToolDiams(tool.fluteDiameter());
let { stock } = settings,
lx = bounds.min.x,
hx = bounds.max.x,
ly = bounds.min.y,
hy = bounds.max.y,
o3 = tool.fluteDiameter() * 2,
mx = (lx + hx) / 2,
my = (ly + hy) / 2,
mz = zThru || 0,
dx = (stock.x - (hx - lx)) / 4,
dy = (stock.y - (hy - ly)) / 4,
dz = stock.z,
points = [];
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(zMax, -zThru, op.down)) {
let slice = newSlice(z);
sliceAll.push(slice);
sliceOut.push(slice);
start(z);
rept(4, o2, oy => {
move(0, -oy);
move(x4, 0);
});
rept(4, o2, ox => {
move(ox, 0);
move(0, y4);
});
rept(4, o2, oy => {
move(0, oy);
move(-x4, 0);
});
rept(4, o2, ox => {
move(-ox, 0);
move(0, -y4);
});
poly.points.pop();
poly.length--;
slice.camTrace = { tool: tool.getID(), rate: op.feed, plunge: op.rate };
slice.camLines = [ poly ];
slice.output()
.setLayer("register", {line: 0xaa00aa}, false)
.addPolys(slice.camLines)
}
break;
}
if (points.length) {
let slice = newSlice(0,null), polys = [];
points.forEach(point => {
polys.push(newPolygon()
.append(point.clone().setZ(bounds.max.z))
.append(point.clone().setZ(bounds.max.z - stock.z - mz)));
});
slice.camLines = polys;
slice.output()
.setLayer("register", {face: 0, line: 0})
.addPolys(polys);
sliceAll.push(slice);
sliceOut.push(slice);
}
}
prepare(ops, progress) {
let { op, state } = this;
let { settings, widget, sliceAll, tslices, 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);
}
slice(progress) {
let { widget, sliceAll } = this.state;
let slicer = new KIRI.slicer2(widget.getPoints(), {
zlist: true,
zline: true
});
let xrayind = Object.keys(slicer.zLine)
.map(v => parseFloat(v).round(5))
.sort((a,b) => a-b);
let xrayopt = { each: (data, index, total) => {
let slice = newSlice(data.z);
slice.addTops(data.tops);
// data.tops.forEach(top => slice.addTop(top));
slice.lines = data.lines;
slice.xray();
sliceAll.push(slice);
}, over: false, flatoff: 0, edges: true, openok: true };
slicer.slice(xrayind, xrayopt);
// xrayopt.over = true;
// slicer.slice(xrayind, xrayopt);
}
}
class OpShadow extends CamOp {
constructor(state, op) {
super(state, op);
}
slice(progress) {
let state = this.state;
let { ops, slicer, widget, unsafe } = state;
let real = ops.map(rec => rec.op).filter(op => op);
let rough = real.filter(op => op.type === 'rough').length > 0;
let outlineOut = real.filter(op => op.type === 'outline' && op.outside).length > 0;
let minStepDown = real
.map(op => (op.down || 3) / 3)
.reduce((a,v) => Math.min(a, v, 1));
let tslices = [];
let tshadow = [];
let tzindex = slicer.interval(minStepDown, { fit: true, off: 0.01, down: true, flats: true });
let complex = tzindex.length > 50 || widget.vertices.length > 1000000;
let skipTerrain = unsafe || (!rough && !outlineOut && complex);
if (skipTerrain) {
console.log("skipping terrain generation");
tzindex = [ tzindex.pop() ];
}
let terrain = slicer.slice(tzindex, { each: (data, index, total) => {
tshadow = POLY.union(tshadow.slice().appendAll(data.tops), 0.01, true);
tslices.push(data.slice);
if (false) {
const slice = data.slice;
sliceAll.push(slice);
slice.output()
.setLayer("terrain", {line: 0x888800, thin: true })
.addPolys(POLY.setZ(tshadow.clone(true), data.z), { thin: true });
}
progress(index / total);
}, genso: true });
state.shadowTop = terrain[terrain.length - 1];
state.center = tshadow[0].bounds.center();
state.tshadow = tshadow;
state.terrain = terrain;
state.tslices = tslices;
state.skipTerrain = skipTerrain;
// identify through holes
state.thruHoles = tshadow.map(p => p.inner || []).flat();
}
}
CAM.OPS = CamOp.MAP = {
"xray": OpXRay,
"shadow": OpShadow,
"level": OpLevel,
"rough": OpRough,
"outline": OpOutline,
"contour": OpContour,
"trace": OpTrace,
"drill": OpDrill,
"register": OpRegister,
"flip": CamOp
};
})();