2020-11-05 15:41:52 -05:00
|
|
|
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
|
|
|
|
|
|
|
|
|
|
|
"use strict";
|
|
|
|
|
|
|
2025-06-21 19:14:11 -04:00
|
|
|
|
// dep: geo.line
|
2022-02-21 10:01:19 -05:00
|
|
|
|
// dep: geo.point
|
|
|
|
|
|
// dep: geo.polygons
|
|
|
|
|
|
// dep: kiri.slice
|
2022-02-22 00:05:29 -05:00
|
|
|
|
// dep: kiri-mode.cam.driver
|
|
|
|
|
|
// use: kiri-mode.cam.slicer
|
2023-01-09 10:16:28 -05:00
|
|
|
|
// use: kiri-mode.cam.slicer2
|
2022-02-22 00:05:29 -05:00
|
|
|
|
// use: kiri-mode.cam.ops
|
2024-09-14 16:59:24 -04:00
|
|
|
|
gapp.register("kiri-mode.cam.slice", (root, exports) => {
|
2022-02-21 10:01:19 -05:00
|
|
|
|
|
|
|
|
|
|
const { base, kiri } = root;
|
2025-02-07 17:52:41 -05:00
|
|
|
|
const { util } = base;
|
2022-11-03 10:44:58 -04:00
|
|
|
|
const { driver, newSlice, setSliceTracker } = kiri;
|
2025-06-21 19:14:11 -04:00
|
|
|
|
const { polygons, newLine, newPoint, newPolygon } = base;
|
2022-02-21 10:01:19 -05:00
|
|
|
|
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,
|
2022-02-21 10:01:19 -05:00
|
|
|
|
sliceAll = widget.slices = [],
|
2025-05-26 17:33:12 -06:00
|
|
|
|
camOps = widget.camops = [],
|
|
|
|
|
|
isIndexed = proc.camStockIndexed;
|
|
|
|
|
|
|
|
|
|
|
|
let stock, bounds, track,
|
|
|
|
|
|
camZTop, camZBottom, camZThru, wztop, ztOff, zbOff,
|
|
|
|
|
|
zBottom, zMin, zMax, zThru, zTop,
|
|
|
|
|
|
minToolDiam, maxToolDiam, dark, color, tabs, unsafe, units,
|
|
|
|
|
|
axisRotation, axisIndex,
|
|
|
|
|
|
part_size,
|
|
|
|
|
|
bottom_gap, bottom_part, bottom_stock, bottom_thru, bottom_z, bottom_cut,
|
|
|
|
|
|
top_stock, top_part, top_gap, top_z,
|
|
|
|
|
|
workarea;
|
|
|
|
|
|
|
|
|
|
|
|
// allow recomputing later if widget or settings changes
|
|
|
|
|
|
const var_compute = () => {
|
2025-06-22 22:09:02 -04:00
|
|
|
|
let { camStockX, camStockY, camStockZ, camStockOffset } = proc;
|
2025-05-26 17:33:12 -06:00
|
|
|
|
bounds = widget.getBoundingBox();
|
2025-06-22 22:09:02 -04:00
|
|
|
|
stock = camStockOffset ? {
|
|
|
|
|
|
x: bounds.dim.x + camStockX,
|
|
|
|
|
|
y: bounds.dim.y + camStockY,
|
|
|
|
|
|
z: bounds.dim.z + camStockZ,
|
|
|
|
|
|
} : {
|
|
|
|
|
|
x: camStockX,
|
|
|
|
|
|
y: camStockY,
|
|
|
|
|
|
z: camStockZ
|
|
|
|
|
|
}
|
2025-05-26 17:33:12 -06:00
|
|
|
|
track = widget.track;
|
|
|
|
|
|
({ camZTop, camZBottom, camZThru } = proc);
|
|
|
|
|
|
wztop = track.top;
|
2025-06-12 23:11:34 -04:00
|
|
|
|
ztOff = isIndexed ? (stock.z - bounds.dim.z) / 2 : (stock.z - wztop);
|
|
|
|
|
|
zbOff = isIndexed ? (stock.z - bounds.dim.z) / 2 : (wztop - track.box.d);
|
2025-05-26 17:33:12 -06:00
|
|
|
|
zBottom = isIndexed ? camZBottom : camZBottom - zbOff;
|
|
|
|
|
|
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;
|
|
|
|
|
|
color = dark ? 0xbbbbbb : 0;
|
|
|
|
|
|
tabs = widget.anno.tab;
|
|
|
|
|
|
unsafe = proc.camExpertFast;
|
|
|
|
|
|
units = settings.controller.units === 'in' ? 25.4 : 1;
|
|
|
|
|
|
axisRotation = axisIndex = undefined;
|
|
|
|
|
|
part_size = bounds.dim;
|
|
|
|
|
|
bottom_gap = zbOff;
|
|
|
|
|
|
bottom_part = 0;
|
|
|
|
|
|
bottom_stock = -bottom_gap;
|
|
|
|
|
|
bottom_thru = zThru;
|
2025-06-22 22:09:02 -04:00
|
|
|
|
bottom_z = isIndexed ? zBottom : Math.max(
|
2025-02-09 23:38:52 -05:00
|
|
|
|
(camZBottom ? bottom_stock + camZBottom : bottom_part) - bottom_thru,
|
2025-02-07 21:48:55 -05:00
|
|
|
|
(camZBottom ? bottom_stock + camZBottom : bottom_stock - bottom_thru)
|
2025-05-26 17:33:12 -06:00
|
|
|
|
);
|
|
|
|
|
|
bottom_cut = Math.max(bottom_z, -zThru);
|
|
|
|
|
|
top_stock = zTop;
|
|
|
|
|
|
top_part = zMax;
|
|
|
|
|
|
top_gap = ztOff;
|
|
|
|
|
|
top_z = camZTop ? bottom_stock + camZTop : top_stock;
|
2025-02-07 17:52:41 -05:00
|
|
|
|
workarea = util.round({
|
2025-05-26 17:33:12 -06:00
|
|
|
|
top_stock, top_part, top_gap, top_z,
|
|
|
|
|
|
bottom_stock, bottom_part, bottom_gap,
|
|
|
|
|
|
bottom_z, bottom_cut
|
2025-02-07 17:52:41 -05:00
|
|
|
|
}, 3);
|
|
|
|
|
|
|
2025-05-26 17:33:12 -06:00
|
|
|
|
return structuredClone(workarea);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// initial setup
|
|
|
|
|
|
var_compute();
|
2022-02-21 10:01:19 -05:00
|
|
|
|
|
|
|
|
|
|
if (tabs) {
|
|
|
|
|
|
// make tab polygons
|
|
|
|
|
|
tabs.forEach(tab => {
|
2025-03-01 15:09:01 -05:00
|
|
|
|
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)
|
|
|
|
|
|
);
|
2022-02-21 10:01:19 -05:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-02-21 10:01:19 -05:00
|
|
|
|
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');
|
2022-02-21 10:01:19 -05:00
|
|
|
|
}
|
2020-11-05 15:41:52 -05:00
|
|
|
|
|
2025-06-12 23:11:34 -04:00
|
|
|
|
if (stock.x && stock.y && stock.z && !isIndexed) {
|
2022-02-21 10:01:19 -05:00
|
|
|
|
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');
|
2021-01-04 10:01:13 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2022-02-21 10:01:19 -05:00
|
|
|
|
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');
|
2020-12-16 21:25:45 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2022-02-21 10:01:19 -05:00
|
|
|
|
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');
|
2022-02-21 10:01:19 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2020-11-05 15:41:52 -05:00
|
|
|
|
|
2022-02-21 10:01:19 -05:00
|
|
|
|
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)}`);
|
2022-02-21 10:01:19 -05:00
|
|
|
|
}
|
2020-11-05 15:41:52 -05:00
|
|
|
|
|
2022-02-21 10:01:19 -05:00
|
|
|
|
let mark = Date.now();
|
2025-06-22 22:09:02 -04:00
|
|
|
|
let opList = [];
|
|
|
|
|
|
let opSum = 0;
|
|
|
|
|
|
let opTot = 0;
|
|
|
|
|
|
let shadows = {};
|
|
|
|
|
|
let slicer;
|
|
|
|
|
|
let state = {
|
|
|
|
|
|
settings,
|
|
|
|
|
|
widget,
|
|
|
|
|
|
bounds,
|
|
|
|
|
|
tabs,
|
|
|
|
|
|
cutTabs,
|
|
|
|
|
|
cutPolys,
|
2025-06-27 01:02:12 -04:00
|
|
|
|
contourPolys,
|
2025-06-22 22:09:02 -04:00
|
|
|
|
healPolys,
|
|
|
|
|
|
shadowAt,
|
|
|
|
|
|
slicer,
|
|
|
|
|
|
addSlices,
|
|
|
|
|
|
isIndexed,
|
|
|
|
|
|
setAxisIndex,
|
|
|
|
|
|
updateToolDiams,
|
|
|
|
|
|
updateSlicer,
|
|
|
|
|
|
computeShadows,
|
|
|
|
|
|
zBottom,
|
|
|
|
|
|
zThru,
|
|
|
|
|
|
ztOff,
|
|
|
|
|
|
zMax,
|
|
|
|
|
|
zTop,
|
|
|
|
|
|
unsafe,
|
|
|
|
|
|
color,
|
|
|
|
|
|
dark,
|
|
|
|
|
|
ops: opList
|
|
|
|
|
|
};
|
|
|
|
|
|
let tracker = setSliceTracker({ rotation: 0 });
|
|
|
|
|
|
|
|
|
|
|
|
function updateSlicer() {
|
|
|
|
|
|
slicer = state.slicer = new kiri.cam_slicer(widget);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function computeShadows() {
|
|
|
|
|
|
shadows = {};
|
|
|
|
|
|
await new CAM.OPS.shadow(state, { type: "shadow", silent: true }).slice(progress => {
|
|
|
|
|
|
// console.log('reshadow', progress.round(3));
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2020-11-05 15:41:52 -05:00
|
|
|
|
|
2022-02-21 10:01:19 -05:00
|
|
|
|
function updateToolDiams(toolDiam) {
|
|
|
|
|
|
minToolDiam = Math.min(minToolDiam, toolDiam);
|
|
|
|
|
|
maxToolDiam = Math.max(maxToolDiam, toolDiam);
|
|
|
|
|
|
}
|
2020-11-05 15:41:52 -05:00
|
|
|
|
|
2022-05-02 16:54:13 -04:00
|
|
|
|
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();
|
2022-05-03 09:02:09 -04:00
|
|
|
|
shadow = POLY.union([...shadow, ...shadows[minZabove]], 0.001, true);
|
2022-05-02 16:54:13 -04:00
|
|
|
|
// console.log({merge, plus, equals: shadow.length, time: Date.now() - mark});
|
|
|
|
|
|
}
|
|
|
|
|
|
return shadows[z] = POLY.setZ(shadow, z);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-11-03 10:44:58 -04:00
|
|
|
|
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);
|
2023-02-17 18:33:18 -05:00
|
|
|
|
return isIndexed ? -axisIndex : 0;
|
2022-10-28 17:18:01 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-02-11 11:10:20 -05:00
|
|
|
|
function addSlices(slices, addIndexing = isIndexed) {
|
2022-10-28 17:18:01 -04:00
|
|
|
|
if (!Array.isArray(slices)) {
|
|
|
|
|
|
slices = [ slices ];
|
|
|
|
|
|
}
|
|
|
|
|
|
sliceAll.appendAll(slices);
|
2023-02-11 11:10:20 -05:00
|
|
|
|
if (addIndexing && axisIndex !== undefined) {
|
2022-10-31 12:14:28 -04:00
|
|
|
|
// update slice cam lines to add axis indexing
|
2022-11-02 10:43:58 -04:00
|
|
|
|
for (let slice of slices.filter(s => s.camLines)) {
|
2022-11-03 22:57:48 -04:00
|
|
|
|
addPolyIndexing(slice.camLines, -axisIndex);
|
2022-10-30 17:04:16 -04:00
|
|
|
|
}
|
2022-10-28 17:18:01 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-02-21 10:01:19 -05:00
|
|
|
|
if (false) {
|
|
|
|
|
|
opList.push(new CAM.OPS.xray(state, { type: "xray" }));
|
|
|
|
|
|
}
|
2020-11-05 15:41:52 -05:00
|
|
|
|
|
2025-06-22 22:09:02 -04:00
|
|
|
|
let activeOps = proc.ops.filter(op => !op.disabled);
|
|
|
|
|
|
|
|
|
|
|
|
// silently preface op list with OpShadow
|
|
|
|
|
|
if (isIndexed) {
|
|
|
|
|
|
if (activeOps.length === 0 || activeOps[0].type !== 'index')
|
|
|
|
|
|
opList.push(new CAM.OPS.index(state, { type: "index", index: 0 }));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
opList.push(new CAM.OPS.shadow(state, { type: "shadow", silent: true }));
|
|
|
|
|
|
}
|
2020-12-16 21:25:45 -05:00
|
|
|
|
|
2022-10-31 12:14:28 -04:00
|
|
|
|
// determing # of steps and step weighting for progress bar
|
2025-06-22 22:09:02 -04:00
|
|
|
|
for (let op of activeOps) {
|
2022-11-02 22:22:38 -04:00
|
|
|
|
if (op.type === '|') {
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2022-02-21 10:01:19 -05:00
|
|
|
|
let opfn = OPS[op.type];
|
|
|
|
|
|
if (opfn) {
|
|
|
|
|
|
let opin = new opfn(state, op);
|
|
|
|
|
|
opList.push(opin);
|
|
|
|
|
|
opTot += opin.weight();
|
2020-11-05 15:41:52 -05:00
|
|
|
|
}
|
2022-02-21 10:01:19 -05:00
|
|
|
|
}
|
2020-11-05 15:41:52 -05:00
|
|
|
|
|
2022-10-31 12:14:28 -04:00
|
|
|
|
// call slice() function on all ops in order
|
2022-11-03 10:44:58 -04:00
|
|
|
|
setAxisIndex();
|
2025-06-22 22:09:02 -04:00
|
|
|
|
updateSlicer();
|
2022-02-21 10:01:19 -05:00
|
|
|
|
for (let op of opList) {
|
|
|
|
|
|
let weight = op.weight();
|
2025-02-09 23:38:52 -05:00
|
|
|
|
// apply operation override vars
|
2025-05-26 17:33:12 -06:00
|
|
|
|
let workover = var_compute();
|
2025-02-09 23:38:52 -05:00
|
|
|
|
let valz = op.op;
|
2025-04-12 17:58:40 -04:00
|
|
|
|
if (valz.ov_topz) {
|
2025-06-22 22:09:02 -04:00
|
|
|
|
workover.top_z = isIndexed ? valz.ov_topz : bottom_stock + valz.ov_topz;
|
2025-04-12 17:58:40 -04:00
|
|
|
|
}
|
2025-02-09 23:38:52 -05:00
|
|
|
|
if (valz.ov_botz) {
|
2025-06-22 22:09:02 -04:00
|
|
|
|
workover.bottom_z = isIndexed ? valz.ov_botz : bottom_stock + valz.ov_botz;
|
2025-02-09 23:38:52 -05:00
|
|
|
|
workover.bottom_cut = Math.max(workover.bottom_z, -zThru);
|
|
|
|
|
|
}
|
2025-06-22 22:09:02 -04:00
|
|
|
|
// state.workarea = workover;
|
|
|
|
|
|
Object.assign(state, {
|
|
|
|
|
|
zBottom,
|
|
|
|
|
|
zThru,
|
|
|
|
|
|
ztOff,
|
|
|
|
|
|
zMax,
|
|
|
|
|
|
zTop,
|
|
|
|
|
|
workarea: workover
|
|
|
|
|
|
});
|
|
|
|
|
|
// console.log({
|
|
|
|
|
|
// op,
|
|
|
|
|
|
// workover,
|
|
|
|
|
|
// bounds: structuredClone(bounds),
|
|
|
|
|
|
// stock: structuredClone(stock)
|
|
|
|
|
|
// });
|
2023-01-08 14:04:02 -05:00
|
|
|
|
await op.slice((progress, message) => {
|
2022-02-21 10:01:19 -05:00
|
|
|
|
onupdate((opSum + (progress * weight)) / opTot, message || op.type());
|
|
|
|
|
|
});
|
2025-06-22 22:09:02 -04:00
|
|
|
|
// update tracker rotation for next slice output() visualization
|
2022-11-03 17:26:45 -04:00
|
|
|
|
tracker.rotation = isIndexed ? axisRotation : 0;
|
2022-02-21 10:01:19 -05:00
|
|
|
|
camOps.push(op);
|
|
|
|
|
|
opSum += weight;
|
|
|
|
|
|
}
|
2022-11-03 10:44:58 -04:00
|
|
|
|
setSliceTracker();
|
2020-11-05 15:41:52 -05:00
|
|
|
|
|
2022-02-21 10:01:19 -05:00
|
|
|
|
// 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 });
|
|
|
|
|
|
}
|
2020-11-28 00:07:38 -05:00
|
|
|
|
});
|
2022-02-21 10:01:19 -05:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2020-12-03 09:06:03 -05:00
|
|
|
|
|
2022-02-21 10:01:19 -05:00
|
|
|
|
// add shadow perimeter to terrain to catch outside moves off part
|
|
|
|
|
|
let tabpoly = tabs ? tabs.map(tab => tab.poly) : [];
|
2025-02-07 17:52:41 -05:00
|
|
|
|
let allpoly = POLY.union([...state.shadow.base, ...tabpoly], 0, true);
|
2022-02-21 10:01:19 -05:00
|
|
|
|
let shadowOff = maxToolDiam < 0 ? allpoly :
|
2024-12-15 15:14:59 -05:00
|
|
|
|
POLY.offset(allpoly, [minToolDiam/2,maxToolDiam/2], { count: 2, flat: true, minArea: 0 });
|
2022-02-21 10:01:19 -05:00
|
|
|
|
state.terrain.forEach(level => level.tops.appendAll(shadowOff));
|
2020-12-04 09:41:39 -05:00
|
|
|
|
|
2022-02-21 10:01:19 -05:00
|
|
|
|
widget.terrain = state.skipTerrain ? null : state.terrain;
|
|
|
|
|
|
widget.minToolDiam = minToolDiam;
|
|
|
|
|
|
widget.maxToolDiam = maxToolDiam;
|
2020-11-05 15:41:52 -05:00
|
|
|
|
|
2025-04-25 20:44:48 -06:00
|
|
|
|
ondone();
|
2022-02-21 10:01:19 -05:00
|
|
|
|
};
|
2020-11-05 15:41:52 -05:00
|
|
|
|
|
2022-02-21 10:01:19 -05: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;
|
2022-02-21 10:01:19 -05:00
|
|
|
|
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();
|
2023-02-25 13:07:40 -05:00
|
|
|
|
let length = oldpts.length + (open ? 0 : 1);
|
2022-02-21 10:01:19 -05:00
|
|
|
|
let newpts = [ ];
|
2023-02-25 13:07:40 -05:00
|
|
|
|
for (let i=0; i<length; i++) {
|
2022-02-21 10:01:19 -05:00
|
|
|
|
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;
|
2023-02-25 13:07:40 -05:00
|
|
|
|
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
|
|
|
|
}
|
2022-02-21 10:01:19 -05:00
|
|
|
|
lastsl = nextsl;
|
|
|
|
|
|
lastpt = nextpt;
|
|
|
|
|
|
if (i < oldpts.length) {
|
|
|
|
|
|
newpts.push(nextpt);
|
2020-12-03 21:14:52 -05:00
|
|
|
|
}
|
2022-02-21 10:01:19 -05:00
|
|
|
|
}
|
|
|
|
|
|
poly.points = newpts;
|
|
|
|
|
|
if (poly.inner) {
|
|
|
|
|
|
CAM.addDogbones(poly.inner, dist, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2020-12-03 19:51:14 -05:00
|
|
|
|
|
2023-01-15 00:45:52 -05:00
|
|
|
|
CAM.traces = async function(settings, widget, single) {
|
2025-06-21 19:14:11 -04:00
|
|
|
|
if (widget.traces) {
|
2022-02-21 10:01:19 -05:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-06-21 19:14:11 -04:00
|
|
|
|
|
|
|
|
|
|
// --- points → line segments ---
|
|
|
|
|
|
let edges = new THREE.EdgesGeometry(widget.mesh.geometry, 20);
|
|
|
|
|
|
let array = edges.attributes.position.array;
|
|
|
|
|
|
let pcache = {};
|
|
|
|
|
|
let points = new Array(2);
|
|
|
|
|
|
let lines = new Array(points.length / 2);
|
|
|
|
|
|
for (let i=0, j=0, k=0, l=array.length; i<l; ) {
|
|
|
|
|
|
let ps = [ array[i++], array[i++], array[i++] ];
|
|
|
|
|
|
let key = ps.map(v => (v * 10000) | 0).join(',');
|
|
|
|
|
|
let point = pcache[key];
|
|
|
|
|
|
if (!point) {
|
|
|
|
|
|
point = newPoint(ps[0], ps[1], ps[2], key);
|
|
|
|
|
|
point.lines = [];
|
|
|
|
|
|
pcache[key] = point;
|
|
|
|
|
|
}
|
|
|
|
|
|
points[j++ % 2] = point;
|
|
|
|
|
|
if (j % 2 === 0) {
|
|
|
|
|
|
let [ p0, p1 ] = points;
|
|
|
|
|
|
let line = lines[k++] = newLine(p0, p1);
|
|
|
|
|
|
p0.lines.push(line);
|
|
|
|
|
|
p1.lines.push(line);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- segments → chains (bidirectional walk) ---
|
|
|
|
|
|
let chains = [];
|
|
|
|
|
|
{
|
|
|
|
|
|
const segs = lines.slice(); // shallow copy
|
|
|
|
|
|
segs.forEach(s => (s.visited = false));
|
|
|
|
|
|
|
|
|
|
|
|
function step(dirPoint, prevSeg, pushFront, chain) {
|
|
|
|
|
|
let curr = dirPoint;
|
|
|
|
|
|
let prev = prevSeg;
|
|
|
|
|
|
|
|
|
|
|
|
while (curr.lines.length === 2) { // stay inside the arc
|
|
|
|
|
|
const next = curr.lines.find(l => !l.visited && l !== prev);
|
|
|
|
|
|
if (!next) break; // should not happen
|
|
|
|
|
|
|
|
|
|
|
|
next.visited = true;
|
|
|
|
|
|
if (pushFront) chain.unshift(next); // prepend or append?
|
|
|
|
|
|
else chain.push(next);
|
|
|
|
|
|
|
|
|
|
|
|
curr = next.p1 === curr ? next.p2 : next.p1;
|
|
|
|
|
|
prev = next;
|
2022-02-21 10:01:19 -05:00
|
|
|
|
}
|
2025-06-21 19:14:11 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (const seed of segs) {
|
|
|
|
|
|
if (seed.visited) continue;
|
|
|
|
|
|
|
|
|
|
|
|
seed.visited = true; // always include the seed
|
|
|
|
|
|
const chain = [seed];
|
|
|
|
|
|
|
|
|
|
|
|
// grow backwards from p1 (prepend)
|
|
|
|
|
|
step(seed.p1, seed, true, chain);
|
|
|
|
|
|
|
|
|
|
|
|
// grow forwards from p2 (append)
|
|
|
|
|
|
step(seed.p2, seed, false, chain);
|
|
|
|
|
|
|
|
|
|
|
|
chains.push(chain);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- chains → polylines ---
|
|
|
|
|
|
const polylines = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (const chain of chains) {
|
|
|
|
|
|
const poly = newPolygon().setOpen();
|
|
|
|
|
|
|
|
|
|
|
|
// choose any node whose degree !== 2; if none, it’s a closed loop
|
|
|
|
|
|
let start = null;
|
|
|
|
|
|
for (const s of chain) {
|
|
|
|
|
|
if (s.p1.lines.length !== 2) { start = s.p1; break; }
|
|
|
|
|
|
if (s.p2.lines.length !== 2) { start = s.p2; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!start) start = chain[0].p1; // closed loop
|
|
|
|
|
|
|
|
|
|
|
|
let curr = start, prevSeg = null;
|
|
|
|
|
|
poly.push(curr); // first point
|
|
|
|
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
|
|
const nextSeg = curr.lines.find(
|
|
|
|
|
|
l => l !== prevSeg && chain.includes(l)
|
|
|
|
|
|
);
|
|
|
|
|
|
if (!nextSeg) break; // open end reached
|
|
|
|
|
|
|
|
|
|
|
|
curr = nextSeg.p1 === curr ? nextSeg.p2 : nextSeg.p1;
|
|
|
|
|
|
poly.push(curr); // << push *before* test
|
|
|
|
|
|
if (curr === start) break; // loop closed
|
|
|
|
|
|
|
|
|
|
|
|
prevSeg = nextSeg;
|
|
|
|
|
|
}
|
|
|
|
|
|
polylines.push(poly);
|
|
|
|
|
|
}
|
2025-04-24 11:30:38 -06:00
|
|
|
|
|
2025-06-21 19:14:11 -04:00
|
|
|
|
widget.traces = polylines;
|
2025-04-24 11:30:38 -06:00
|
|
|
|
|
2022-02-21 10:01:19 -05:00
|
|
|
|
return true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-06-27 01:02:12 -04:00
|
|
|
|
/**
|
|
|
|
|
|
* Generate a list of holes in the model based on the given diameter.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {Object} settings - settings object
|
|
|
|
|
|
* @param {Object} widget - widget object
|
|
|
|
|
|
* @param {boolean} individual - if true, drill holes individually
|
|
|
|
|
|
* @param {Object} rec - DrillOp record
|
|
|
|
|
|
* @param {Function} onProgress - callback function to report progress
|
|
|
|
|
|
* @returns {Array} list of hole centers as objects with `x`, `y`, `z`, `depth`, and `selected` properties.
|
|
|
|
|
|
*/
|
2025-05-31 22:21:55 -06:00
|
|
|
|
CAM.holes = async function(settings, widget, individual, rec,onProgress) {
|
2025-05-27 11:19:51 -06:00
|
|
|
|
|
|
|
|
|
|
let {tool,mark,precision} = rec //TODO: display some visual difference if mark is selected
|
|
|
|
|
|
let toolDiam = new CAM.Tool(settings,tool).fluteDiameter()
|
|
|
|
|
|
let diam = individual ? 1 : toolDiam; // sets default diameter when select individual used
|
2025-04-24 11:30:38 -06:00
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-05-27 11:19:51 -06:00
|
|
|
|
let slicerOpts = {flatoff: 0.001}
|
2025-05-01 09:39:54 -06:00
|
|
|
|
let slicer = new kiri.cam_slicer(widget,slicerOpts);
|
2025-05-27 11:19:51 -06:00
|
|
|
|
let zFlats = Object.keys(slicer.zFlat).map(Number).map(z=>[z,z-0.002]).flat()
|
2025-06-12 23:11:34 -04:00
|
|
|
|
|
2025-05-27 11:19:51 -06:00
|
|
|
|
precision = Math.max( 0, precision )
|
|
|
|
|
|
let intervals = (precision == 0) ? [] : slicer.interval(
|
|
|
|
|
|
precision,
|
|
|
|
|
|
{
|
|
|
|
|
|
fit: false, off: -0.01, flats: true
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
2025-05-01 09:39:54 -06:00
|
|
|
|
|
|
|
|
|
|
let zees = [...zFlats,...intervals]
|
|
|
|
|
|
let indices = [...new Set(zees
|
2025-04-24 11:30:38 -06:00
|
|
|
|
.map(kv => parseFloat(kv).round(5))
|
|
|
|
|
|
.filter(z => z !== null)
|
|
|
|
|
|
)]
|
|
|
|
|
|
let centerDiff = diam * 0.1,
|
|
|
|
|
|
area = (diam/2) * (diam/2) * Math.PI,
|
2025-05-08 11:13:14 -06:00
|
|
|
|
circles = [],
|
|
|
|
|
|
slices = [];
|
2025-04-24 11:30:38 -06:00
|
|
|
|
|
|
|
|
|
|
function onEach(slice) {
|
|
|
|
|
|
slices.push(slice);
|
|
|
|
|
|
}
|
2025-05-31 22:21:55 -06:00
|
|
|
|
let opts = { each: onEach, progress: (num,total)=> onProgress(num/total*0.5,"slice") };
|
2025-04-24 11:30:38 -06:00
|
|
|
|
await slicer.slice(indices, opts);
|
2025-05-01 09:39:54 -06:00
|
|
|
|
let shadowedDrills = false;
|
|
|
|
|
|
// console.log("slices",slices)
|
2025-05-31 22:21:55 -06:00
|
|
|
|
for (let [i,slice] of slices.entries()) {
|
2025-05-01 09:39:54 -06:00
|
|
|
|
for(let top of slice.tops){
|
|
|
|
|
|
// console.log("slicing",slice.z,top)
|
2025-05-27 11:19:51 -06:00
|
|
|
|
slice.shadow = CAM.shadowAt(widget, slice.z, 0)
|
2025-05-01 09:39:54 -06:00
|
|
|
|
let inner = top.inner;
|
|
|
|
|
|
if (!inner) { //no holes
|
2025-04-24 11:41:27 -06:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2025-05-01 09:39:54 -06:00
|
|
|
|
for (let poly of inner) {
|
2025-05-31 09:25:38 -06:00
|
|
|
|
if ( poly.points.length < 7 ) continue;
|
2025-05-01 09:39:54 -06:00
|
|
|
|
let center = poly.calcCircleCenter();
|
2025-05-01 10:10:01 -06:00
|
|
|
|
center.area = poly.area();
|
2025-05-08 11:13:14 -06:00
|
|
|
|
center.overlapping = [center]
|
|
|
|
|
|
center.depth = 0;
|
2025-05-01 09:39:54 -06:00
|
|
|
|
// console.log("center",center)
|
2025-05-08 11:13:14 -06:00
|
|
|
|
if ( poly.circularity() < 0.985 ){
|
|
|
|
|
|
// if not circular, don't add to holes
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2025-05-01 09:39:54 -06:00
|
|
|
|
if (center.isInPolygon(slice.shadow)) {
|
|
|
|
|
|
// if shadowed, don't add, and inform client
|
|
|
|
|
|
shadowedDrills = true;
|
2025-04-24 11:41:27 -06:00
|
|
|
|
continue;
|
2025-04-24 11:30:38 -06:00
|
|
|
|
}
|
2025-05-01 09:39:54 -06:00
|
|
|
|
let overlap = false;
|
2025-05-08 11:13:14 -06:00
|
|
|
|
for (let [i,circle] of circles.entries()) {
|
|
|
|
|
|
let dist = circle.distTo2D(center)
|
|
|
|
|
|
|
|
|
|
|
|
// //if on the same xy point,
|
|
|
|
|
|
if (dist <= centerDiff ) {
|
|
|
|
|
|
// console.log("overlap",center,circle);
|
|
|
|
|
|
circle.overlapping.push(center);
|
2025-05-01 09:39:54 -06:00
|
|
|
|
// if overlapping, don't add and continue
|
|
|
|
|
|
overlap = true;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-05-08 11:13:14 -06:00
|
|
|
|
if (!overlap) circles.push(center);
|
2025-04-24 11:30:38 -06:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-05-31 22:21:55 -06:00
|
|
|
|
onProgress(0.5+(i/slices.length*0.25),"recognize circles")
|
2025-04-24 11:30:38 -06:00
|
|
|
|
}
|
2025-05-08 11:13:14 -06:00
|
|
|
|
|
|
|
|
|
|
let drills = []
|
|
|
|
|
|
|
2025-05-31 22:21:55 -06:00
|
|
|
|
for (let [i,c] of circles.entries()) {
|
2025-05-08 11:13:14 -06:00
|
|
|
|
let overlapping = c.overlapping
|
|
|
|
|
|
|
|
|
|
|
|
let last = overlapping.shift();
|
|
|
|
|
|
while (overlapping.length) {
|
|
|
|
|
|
let circ = overlapping.shift();
|
|
|
|
|
|
let aveArea = (circ.area + last.area) / 2;
|
|
|
|
|
|
let areaDelta = Math.abs(circ.area -last.area)
|
|
|
|
|
|
if (areaDelta < aveArea * 0.05){ // if area delta less than 5% of average area
|
|
|
|
|
|
//keep top circle selected
|
|
|
|
|
|
last.depth = last.z - circ.z;
|
|
|
|
|
|
}else{ // if not the same area
|
|
|
|
|
|
//push and move on
|
|
|
|
|
|
drills.push(last);
|
|
|
|
|
|
last = circ;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (last.depth != 0) drills.push(last) //add last circle
|
2025-05-31 22:21:55 -06:00
|
|
|
|
onProgress(0.75+(i/circles.length*0.25),"assemble holes")
|
2025-05-08 11:13:14 -06:00
|
|
|
|
}
|
|
|
|
|
|
drills.forEach( h=>{
|
2025-06-12 09:41:39 -06:00
|
|
|
|
if(rec.fromTop){
|
|
|
|
|
|
// set z top if selected
|
|
|
|
|
|
h.depth += wztop - h.z
|
|
|
|
|
|
h.z = wztop
|
|
|
|
|
|
}
|
2025-05-08 11:13:14 -06:00
|
|
|
|
delete h.overlapping //for encoding
|
2025-05-27 11:19:51 -06:00
|
|
|
|
h.diam = toolDiam // for mesh generation
|
2025-05-08 11:13:14 -06:00
|
|
|
|
h.selected = (!individual && Math.abs(h.area - area) <= area * 0.05 ); //for same size selection
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// console.log("unfiltered circles",circles)
|
2025-05-08 11:29:29 -06:00
|
|
|
|
// console.log("drills",drills)
|
2025-04-25 19:54:57 -06:00
|
|
|
|
drills = drills.filter(drill => drill.depth > 0)
|
2025-05-01 09:39:54 -06:00
|
|
|
|
widget.shadowedDrills = shadowedDrills
|
2025-05-08 11:13:14 -06:00
|
|
|
|
widget.drills = drills;
|
2025-04-24 11:30:38 -06:00
|
|
|
|
return drills;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-02-21 10:01:19 -05:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-27 01:02:12 -04:00
|
|
|
|
function contourPolys(widget, polys) {
|
|
|
|
|
|
const raycaster = new THREE.Raycaster();
|
|
|
|
|
|
raycaster.ray.direction.set(0, 0, -1); // ray pointing down Z
|
|
|
|
|
|
for (let poly of polys) {
|
|
|
|
|
|
for (let point of poly.points) {
|
|
|
|
|
|
raycaster.ray.origin.set(point.x, point.y, 10000);
|
|
|
|
|
|
const intersects = raycaster.intersectObject(widget.mesh, true);
|
|
|
|
|
|
const firstHit = intersects[0] || null;
|
|
|
|
|
|
point.z = firstHit.point.z;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-02-21 10:01:19 -05:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2022-02-21 10:01:19 -05:00
|
|
|
|
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
|
|
|
|
}
|
2022-02-21 10:01:19 -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;
|
2020-12-02 18:05:31 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2022-02-21 10:01:19 -05:00
|
|
|
|
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
|
|
|
|
}
|
2020-12-02 18:05:31 -05:00
|
|
|
|
}
|
2020-11-25 22:50:44 -05:00
|
|
|
|
}
|
2022-02-21 10:01:19 -05:00
|
|
|
|
return noff;
|
|
|
|
|
|
}
|
2020-11-25 22:50:44 -05:00
|
|
|
|
|
2022-02-21 10:01:19 -05:00
|
|
|
|
});
|