grid-apps-cmms/src/kiri-mode/cam/slice.js

582 lines
19 KiB
JavaScript
Raw Normal View History

/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
// dep: geo.point
// dep: geo.polygons
// dep: kiri.slice
// dep: kiri-mode.cam.driver
// use: kiri-mode.cam.slicer
2023-01-09 10:16:28 -05:00
// use: kiri-mode.cam.slicer2
// use: kiri-mode.cam.ops
2024-09-14 16:59:24 -04:00
gapp.register("kiri-mode.cam.slice", (root, exports) => {
const { base, kiri } = root;
const { util } = base;
const { driver, newSlice, setSliceTracker } = kiri;
const { polygons, newPoint, newPolygon } = base;
const { CAM } = driver;
const { OPS } = CAM;
const PRO = CAM.process;
const POLY = polygons;
/**
* DRIVER SLICE CONTRACT
*
* @param {Object} settings
* @param {Widget} widget
* @param {Function} output
*/
2023-01-08 14:04:02 -05:00
CAM.slice = async function(settings, widget, onupdate, ondone) {
2024-09-14 11:36:56 -04:00
let proc = settings.process,
stock = settings.stock || {},
2023-02-14 13:25:56 -05:00
isIndexed = proc.camStockIndexed,
camOps = widget.camops = [],
sliceAll = widget.slices = [],
bounds = widget.getBoundingBox(),
2022-11-03 12:29:07 -04:00
track = widget.track,
{ camZTop, camZBottom, camZThru } = proc,
2022-11-03 12:29:07 -04:00
// widget top z as defined by setTopz()
wztop = track.top,
// distance between top of part and top of stock
2023-02-14 13:25:56 -05:00
ztOff = isIndexed ? 0 : (stock.z - wztop),
2022-11-03 12:29:07 -04:00
// distance between bottom of part and bottom of stock
2023-02-14 13:25:56 -05:00
zbOff = isIndexed ? 0 : (wztop - track.box.d),
2022-11-03 12:29:07 -04:00
// defined z bottom offset by distance to stock bottom
// keeps the z bottom relative to the part when z align changes
zBottom = isIndexed ? camZBottom : camZBottom - zbOff,
2022-11-03 12:29:07 -04:00
// greater of widget bottom and z bottom
zMin = isIndexed ? bounds.min.z : Math.max(bounds.min.z, zBottom),
zMax = bounds.max.z,
zThru = camZThru,
zTop = zMax + ztOff,
minToolDiam = Infinity,
maxToolDiam = -Infinity,
dark = settings.controller.dark ? true : false,
color = dark ? 0xbbbbbb : 0,
tabs = widget.anno.tab,
unsafe = proc.camExpertFast,
units = settings.controller.units === 'in' ? 25.4 : 1,
axisRotation,
axisIndex,
// new work area tracking
part_size = bounds.dim,
bottom_gap = zbOff,
bottom_part = 0,
bottom_stock = -bottom_gap,
bottom_thru = zThru,
bottom_z = Math.max(
(camZBottom ? bottom_stock + camZBottom : bottom_part) - bottom_thru,
(camZBottom ? bottom_stock + camZBottom : bottom_stock - bottom_thru)
),
bottom_cut = Math.max(bottom_z, -zThru),
top_stock = zTop,
top_part = zMax,
top_gap = ztOff,
top_z = camZTop ? bottom_stock + camZTop : top_stock,
workarea = util.round({
top_stock,
top_part,
top_gap,
top_z,
bottom_stock,
bottom_part,
bottom_gap,
bottom_z,
bottom_cut
}, 3);
// console.table({ workarea });
// console.table({ part_size });
// console.table({ stock });
if (tabs) {
// make tab polygons
tabs.forEach(tab => {
let zero = newPoint(0,0,0),
point = newPoint(tab.pos.x, tab.pos.y, tab.pos.z),
poly = newPolygon().centerRectangle(zero, tab.dim.x, tab.dim.y),
[ rx, ry, rz, rw ] = tab.rot,
m4 = new THREE.Matrix4().makeRotationFromQuaternion(
new THREE.Quaternion(rx, ry, rz, rw)
);
poly.points = poly.points
.map(p => new THREE.Vector3(p.x,p.y,p.z).applyMatrix4(m4))
.map(v => newPoint(v.x, v.y, v.z));
poly.move(point);
tab.poly = poly;
// tslice.output().setLayer("tabs", 0xff0000).addPoly(poly);
// sliceAll.push(tslice);
});
}
2023-03-27 15:20:51 -04:00
function error(msg) {
ondone(msg);
}
if (unsafe) {
console.log("disabling overhang safeties");
}
if (!proc.ops || proc.ops.length === 0) {
2023-03-27 15:20:51 -04:00
return error('no processes specified');
}
if (stock.x && stock.y && stock.z) {
if (stock.x + 0.00001 < bounds.max.x - bounds.min.x) {
2023-03-27 15:20:51 -04:00
return error('stock X too small for part. resize stock or use offset stock');
}
if (stock.y + 0.00001 < bounds.max.y - bounds.min.y) {
2023-03-27 15:20:51 -04:00
return error('stock Y too small for part. resize stock or use offset stock');
}
if (stock.z + 0.00001 < bounds.max.z - bounds.min.z) {
2023-03-27 15:20:51 -04:00
return error('stock Z too small for part. resize stock or use offset stock');
}
}
if (zMin >= bounds.max.z) {
2023-03-27 15:20:51 -04:00
return error(`invalid z bottom ${(zMin/units).round(3)} >= bounds z max ${(zMax/units).round(3)}`);
}
let mark = Date.now();
2023-01-15 13:05:53 -05:00
let slicer = new kiri.cam_slicer(widget);
function updateToolDiams(toolDiam) {
minToolDiam = Math.min(minToolDiam, toolDiam);
maxToolDiam = Math.max(maxToolDiam, toolDiam);
}
let shadows = {};
function shadowAt(z) {
let cached = shadows[z];
if (cached) {
return cached;
}
// find closest shadow above and use to speed up delta shadow gen
let minZabove;
let zover = Object.keys(shadows).map(v => parseFloat(v)).filter(v => v > z);
for (let zkey of zover) {
if (minZabove && zkey < minZabove) {
minZabove = zkey;
} else {
minZabove = zkey;
}
}
let shadow = CAM.shadowAt(widget, z, minZabove);
if (minZabove) {
// const merge = shadow.length;
// const plus = shadows[minZabove].length;
// const mark = Date.now();
shadow = POLY.union([...shadow, ...shadows[minZabove]], 0.001, true);
// console.log({merge, plus, equals: shadow.length, time: Date.now() - mark});
}
return shadows[z] = POLY.setZ(shadow, z);
}
function setAxisIndex(degrees = 0, absolute = true) {
axisIndex = absolute ? degrees : axisIndex + degrees;
axisRotation = (Math.PI / 180) * axisIndex;
2022-11-03 12:29:07 -04:00
widget.setAxisIndex(isIndexed ? -axisIndex : 0);
return isIndexed ? -axisIndex : 0;
}
2022-11-03 22:57:48 -04:00
function addPolyIndexing(poly, a) {
if (!poly) {
return;
}
if (Array.isArray(poly)) {
for (let p of poly) {
addPolyIndexing(p, a);
}
return;
}
for (let point of poly.points) {
point.a = a;
}
addPolyIndexing(poly.inner, a);
}
function addSlices(slices, addIndexing = isIndexed) {
if (!Array.isArray(slices)) {
slices = [ slices ];
}
sliceAll.appendAll(slices);
if (addIndexing && axisIndex !== undefined) {
// update slice cam lines to add axis indexing
for (let slice of slices.filter(s => s.camLines)) {
2022-11-03 22:57:48 -04:00
addPolyIndexing(slice.camLines, -axisIndex);
}
}
}
let state = {
settings,
widget,
bounds,
tabs,
cutTabs,
cutPolys,
healPolys,
shadowAt,
slicer,
addSlices,
isIndexed,
setAxisIndex,
updateToolDiams,
zBottom,
zThru,
ztOff,
zMax,
zTop,
unsafe,
color,
dark,
};
let opList = [
// silently preface op list with OpShadow
new CAM.OPS.shadow(state, { type: "shadow", silent: true })
];
if (false) {
opList.push(new CAM.OPS.xray(state, { type: "xray" }));
}
let opSum = 0;
let opTot = opList.length ? opList.map(op => op.weight()).reduce((a,v) => a + v) : 0;
// determing # of steps and step weighting for progress bar
for (let op of proc.ops.filter(op => !op.disabled)) {
if (op.type === '|') {
break;
}
let opfn = OPS[op.type];
if (opfn) {
let opin = new opfn(state, op);
opList.push(opin);
opTot += opin.weight();
}
}
// give ops access to entire sequence
state.ops = opList;
// call slice() function on all ops in order
let tracker = setSliceTracker({ rotation: 0 });
let workarea_orig = structuredClone(workarea);
setAxisIndex();
for (let op of opList) {
let weight = op.weight();
// apply operation override vars
let workover = structuredClone(workarea_orig);
let valz = op.op;
2025-04-12 17:58:40 -04:00
if (valz.ov_topz) {
workover.top_z = bottom_stock + valz.ov_topz;
}
if (valz.ov_botz) {
workover.bottom_z = bottom_stock + valz.ov_botz;
workover.bottom_cut = Math.max(workover.bottom_z, -zThru);
}
state.workarea = workover;
2023-01-08 14:04:02 -05:00
await op.slice((progress, message) => {
onupdate((opSum + (progress * weight)) / opTot, message || op.type());
});
tracker.rotation = isIndexed ? axisRotation : 0;
// setup new state when indexing the workspace
if (op.op.type === "index") {
widget.topo = undefined;
2023-01-17 22:42:24 -05:00
// let points = base.verticesToPoints();
2023-01-15 13:05:53 -05:00
state.slicer = new kiri.cam_slicer(widget);
shadows = {};
2023-01-21 18:09:07 -05:00
await new CAM.OPS.shadow(state, { type: "shadow", silent: true }).slice(progress => {
// console.log('reshadow', progress.round(3));
});
}
camOps.push(op);
opSum += weight;
}
setSliceTracker();
// reindex
sliceAll.forEach((slice, index) => slice.index = index);
// used in printSetup()
// used in CAM.prepare.getZClearPath()
// add tabs to terrain tops so moves avoid them
if (tabs) {
state.terrain.forEach(slab => {
tabs.forEach(tab => {
if (tab.pos.z + tab.dim.z/2 >= slab.z) {
let all = [...slab.tops, tab.poly];
slab.tops = POLY.union(all, 0, true);
// slab.slice.output()
// .setLayer("debug-tabs", {line: 0x880088, thin: true })
// .addPolys(POLY.setZ(slab.tops.clone(true), slab.z), { thin: true });
}
});
});
}
// add shadow perimeter to terrain to catch outside moves off part
let tabpoly = tabs ? tabs.map(tab => tab.poly) : [];
let allpoly = POLY.union([...state.shadow.base, ...tabpoly], 0, true);
let shadowOff = maxToolDiam < 0 ? allpoly :
POLY.offset(allpoly, [minToolDiam/2,maxToolDiam/2], { count: 2, flat: true, minArea: 0 });
state.terrain.forEach(level => level.tops.appendAll(shadowOff));
widget.terrain = state.skipTerrain ? null : state.terrain;
widget.minToolDiam = minToolDiam;
widget.maxToolDiam = maxToolDiam;
2025-04-24 11:30:38 -06:00
ondone(state);
};
2025-04-24 11:30:38 -06:00
CAM.addDogbones = function(poly, dist, reverse) {
if (Array.isArray(poly)) {
return poly.forEach(p => CAM.addDogbones(p, dist));
}
2023-02-23 21:01:12 -05:00
let open = poly.open;
let isCW = poly.isClockwise();
if (reverse || poly.parent) isCW = !isCW;
let oldpts = poly.points.slice();
let lastpt = oldpts[oldpts.length - 1];
let lastsl = lastpt.slopeTo(oldpts[0]).toUnit();
let length = oldpts.length + (open ? 0 : 1);
let newpts = [ ];
for (let i=0; i<length; i++) {
let nextpt = oldpts[i % oldpts.length];
let nextsl = lastpt.slopeTo(nextpt).toUnit();
let adiff = lastsl.angleDiff(nextsl, true);
let bdiff = ((adiff < 0 ? (180 - adiff) : (180 + adiff)) / 2) + 180;
if (!open || (i > 1 && i < length)) {
if (isCW && adiff > 45) {
let newa = base.newSlopeFromAngle(lastsl.angle + bdiff);
newpts.push(lastpt.projectOnSlope(newa, dist));
newpts.push(lastpt.clone());
} else if (!isCW && adiff < -45) {
let newa = base.newSlopeFromAngle(lastsl.angle - bdiff);
newpts.push(lastpt.projectOnSlope(newa, dist));
newpts.push(lastpt.clone());
}
2020-12-03 19:51:14 -05:00
}
lastsl = nextsl;
lastpt = nextpt;
if (i < oldpts.length) {
newpts.push(nextpt);
}
}
poly.points = newpts;
if (poly.inner) {
CAM.addDogbones(poly.inner, dist, true);
}
};
2020-12-03 19:51:14 -05:00
CAM.traces = async function(settings, widget, single) {
if (widget.traces && widget.trace_single === single) {
// do no work if cached
return false;
}
2023-01-15 13:05:53 -05:00
let slicer = new kiri.cam_slicer(widget);
let zees = Object.assign({}, slicer.zFlat, slicer.zLine);
let indices = [...new Set(Object.keys(zees)
2023-01-28 10:22:42 -05:00
.map(kv => parseFloat(kv).round(5))
.appendAll(Object.entries(single ? slicer.zLine : {}).map(ze => {
let [ zk, zv ] = ze;
return zv > 1 ? parseFloat(zk).round(5) : null;
})
.filter(v => v !== null))
)]
.sort((a,b) => b - a);
let traces = [];
// find and trim polys (including open) to shadow
let oneach = data => {
if (single) {
for (let line of data.lines) {
if (line.p1.distTo2D(line.p2) > 1) {
traces.push(newPolygon().append(line.p1).append(line.p2).setOpen());
}
}
} else
2023-02-25 23:26:57 -05:00
base.polygons.flatten(data.tops).forEach(poly => {
poly.inner = null;
poly.parent = null;
let z = poly.getZ();
for (let i=0, il=traces.length; i<il; i++) {
let trace = traces[i];
let dz = Math.abs(z - trace.getZ());
// only compare polys farther apart in Z
if (dz > 0.01) {
continue;
}
// do not add duplicates
2023-01-15 00:52:19 -05:00
if (traces[i].isEquivalent(poly) && dz < 0.1) {
2023-02-25 23:26:57 -05:00
// console.log({ dup: poly });
return;
}
}
traces.push(poly);
});
};
2025-04-24 11:30:38 -06:00
let opts = { each: oneach, over: false, flatoff: 0, edges: true, openok: true, lines: true };
await slicer.slice(indices, opts);
// pick up bottom features
opts.over = true;
await slicer.slice(indices, opts);
widget.traces = traces;
widget.trace_single = single;
return true;
};
2025-04-24 11:30:38 -06:00
CAM.holes = async function(settings, widget, diam) {
let proc = settings.process,
stock = settings.stock || {},
isIndexed = proc.camStockIndexed,
track = widget.track,
{ camZTop, camZBottom, camZThru } = proc,
// widget top z as defined by setTopz()
wztop = track.top,
// distance between top of part and top of stock
ztOff = isIndexed ? 0 : (stock.z - wztop),
// distance between bottom of part and bottom of stock
zbOff = isIndexed ? 0 : (wztop - track.box.d),
// defined z bottom offset by distance to stock bottom
// keeps the z bottom relative to the part when z align changes
zBottom = isIndexed ? camZBottom : camZBottom - zbOff;
let slicer = new kiri.cam_slicer(widget);
let zees = Object.assign({}, slicer.zFlat);
let indices = [...new Set(Object.keys(zees)
.map(kv => parseFloat(kv).round(5))
.filter(z => z !== null)
)]
let centerDiff = diam * 0.1,
area = (diam/2) * (diam/2) * Math.PI,
areaDelta = area * 0.05,
2025-04-24 11:41:27 -06:00
drills = [],
slices = [],
selectall = (diam <= 0);
2025-04-24 11:30:38 -06:00
function onEach(slice) {
slices.push(slice);
}
let opts = { each: onEach, over: false, flatoff: 0, edges: true, openok: false, lines: false };
await slicer.slice(indices, opts);
const tslices = slices.map(s=>s.tops).flat()
for (let slice of tslices) {
slice.shadow = CAM.shadowAt(widget,slice.z, 0)
2025-04-24 11:41:27 -06:00
// console.log("looking at slice",slice)
2025-04-24 11:30:38 -06:00
let inner = slice.inner;
for (let poly of inner) {
2025-04-24 11:41:27 -06:00
let center = poly.calcCircleCenter();
center.selected = (selectall || (poly.circularity() >= 0.985 && Math.abs(poly.area() - area) <= areaDelta ));
// center.points = poly.points;
2025-04-24 11:41:27 -06:00
if (center.isInPolygon(slice.shadow)) {
//TODO: test if this is working
continue;
}
let overlap = false;
for (let [i,drill] of drills.entries()) {
let dist = drill.distTo2D(center)
if (dist <= centerDiff) { // too close
// console.log("overlap",center,drill);
if(center.z > drill.z) { //if current is higher than old
drills[i] = center;
2025-04-24 11:30:38 -06:00
}
2025-04-24 11:41:27 -06:00
// otherwise, don't add and continue
overlap = true;
continue;
2025-04-24 11:30:38 -06:00
}
}
2025-04-24 11:41:27 -06:00
if(!overlap) drills.push(center);
2025-04-24 11:30:38 -06:00
}
}
widget.holes = drills;
return drills;
}
function cutTabs(tabs, offset, z, inter) {
tabs = tabs.filter(tab => z < tab.pos.z + tab.dim.z/2).map(tab => tab.off).flat();
return cutPolys(tabs, offset, z, false);
}
function cutPolys(polys, offset, z, inter) {
let noff = [];
offset.forEach(op => noff.appendAll( op.cut(POLY.union(polys, 0, true), inter) ));
return healPolys(noff);
}
function healPolys(noff) {
if (noff.length > 1) {
let heal = 0;
// heal/rejoin open segments that share endpoints
outer: for(;; heal++) {
let ntmp = noff, tlen = ntmp.length;
for (let i=0; i<tlen; i++) {
let s1 = ntmp[i];
if (!s1) continue;
for (let j=i+1; j<tlen; j++) {
let s2 = ntmp[j];
if (!s2) continue;
// require polys at same Z to heal
if (Math.abs(s1.getZ() - s2.getZ()) > 0.01) {
2020-12-01 18:19:28 -05:00
continue;
}
if (!(s1.open && s2.open)) continue;
if (s1.last().isMergable2D(s2.first())) {
s1.addPoints(s2.points.slice(1));
ntmp[j] = null;
continue outer;
2020-12-01 12:31:38 -05:00
}
if (s2.last().isMergable2D(s1.first())) {
s2.addPoints(s1.points.slice(1));
ntmp[i] = null;
continue outer;
}
if (s1.first().isMergable2D(s2.first())) {
s1.reverse();
s1.addPoints(s2.points.slice(1));
ntmp[j] = null;
continue outer;
}
if (s1.last().isMergable2D(s2.last())) {
s2.reverse();
s1.addPoints(s2.points.slice(1));
ntmp[j] = null;
continue outer;
}
}
}
break;
}
if (heal > 0) {
// cull nulls
noff = noff.filter(o => o);
}
// close poly if head meets tail
for (let poly of noff) {
if (poly.open && poly.first().isMergable2D(poly.last())) {
poly.points.pop();
poly.open = false;
2021-01-14 09:25:58 -05:00
}
}
}
return noff;
}
});