2020-11-05 15:41:52 -05:00
|
|
|
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
|
|
|
|
|
|
2026-01-08 16:52:41 -05:00
|
|
|
|
import { CAM } from './init-work.js';
|
|
|
|
|
|
import { newLine } from '../../../../geo/line.js';
|
|
|
|
|
|
import { newPoint } from '../../../../geo/point.js';
|
|
|
|
|
|
import { newPolygon } from '../../../../geo/polygon.js';
|
|
|
|
|
|
import { ops as OPS } from '../core/ops.js';
|
|
|
|
|
|
import { polygons as POLY } from '../../../../geo/polygons.js';
|
|
|
|
|
|
import { setSliceTracker } from '../../../core/slice.js';
|
|
|
|
|
|
import { Slicer as cam_slicer } from './slicer-cam.js';
|
|
|
|
|
|
import { Tool } from '../core/tool.js';
|
|
|
|
|
|
import { util } from '../../../../geo/base.js';
|
2022-02-21 10:01:19 -05:00
|
|
|
|
|
2026-01-03 11:42:37 -05:00
|
|
|
|
// state shared across all widget slicing
|
|
|
|
|
|
let shared;
|
|
|
|
|
|
|
2026-01-05 11:21:21 -05:00
|
|
|
|
/**
|
|
|
|
|
|
* DRIVER SLICE CONTRACT
|
|
|
|
|
|
*
|
|
|
|
|
|
* global slice operation run before individual widget slice() operations
|
|
|
|
|
|
*/
|
2026-01-03 11:42:37 -05:00
|
|
|
|
export function cam_slice_pre(settings) {
|
|
|
|
|
|
shared = { ops: { } };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-02-21 10:01:19 -05:00
|
|
|
|
/**
|
|
|
|
|
|
* DRIVER SLICE CONTRACT
|
|
|
|
|
|
*
|
2026-01-05 11:21:21 -05:00
|
|
|
|
* slice operations run per-widget between slicePre and sliceAll
|
|
|
|
|
|
*
|
2022-02-21 10:01:19 -05:00
|
|
|
|
* @param {Object} settings
|
|
|
|
|
|
* @param {Widget} widget
|
|
|
|
|
|
* @param {Function} output
|
|
|
|
|
|
*/
|
2025-07-06 23:44:33 -04:00
|
|
|
|
export async function cam_slice(settings, widget, onupdate, ondone) {
|
2025-11-27 10:32:41 -05:00
|
|
|
|
if (widget.track.synth) return ondone();
|
|
|
|
|
|
|
2026-05-16 14:50:34 -04:00
|
|
|
|
// get tab widgets for this specific widget
|
|
|
|
|
|
let tabW = widget.group.filter(w => w != widget && w.track.tabOwner === widget.id);
|
2025-12-25 16:17:44 -05:00
|
|
|
|
|
2025-12-11 22:47:24 -05:00
|
|
|
|
// merge overlapping tabs
|
|
|
|
|
|
for (let i=0; i<tabW.length; i++) {
|
|
|
|
|
|
for (let j=i+1; j<tabW.length; j++) {
|
|
|
|
|
|
let t1 = tabW[i];
|
|
|
|
|
|
let t2 = tabW[j];
|
|
|
|
|
|
let b1 = t1.mesh.getBoundingBox();
|
|
|
|
|
|
let b2 = t2.mesh.getBoundingBox();
|
|
|
|
|
|
if (b1.intersectsBox(b2)) {
|
|
|
|
|
|
let a = t1.getVertices().array;
|
|
|
|
|
|
let b = t2.getVertices().array;
|
|
|
|
|
|
let arr = new Float32Array(a.length + b.length);
|
|
|
|
|
|
arr.set(a, 0);
|
|
|
|
|
|
arr.set(b, a.length);
|
|
|
|
|
|
t1.loadVertices(arr);
|
|
|
|
|
|
t2.del = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// filter merged tabs
|
|
|
|
|
|
tabW = tabW.filter(tw => !tw.del);
|
2025-11-27 10:32:41 -05:00
|
|
|
|
|
2024-09-14 11:36:56 -04:00
|
|
|
|
let proc = settings.process,
|
2025-05-26 17:33:12 -06:00
|
|
|
|
camOps = widget.camops = [],
|
2025-12-06 00:09:17 -05:00
|
|
|
|
sliceAll = widget.slices = [],
|
2026-01-03 16:28:18 -05:00
|
|
|
|
isIndexed = proc.camStockIndexed,
|
|
|
|
|
|
lastTime = Date.now();
|
2025-05-26 17:33:12 -06:00
|
|
|
|
|
2025-12-06 00:09:17 -05:00
|
|
|
|
let axisRotation, axisIndex,
|
|
|
|
|
|
bounds, dark, color, stock, tabs, track, tool, unsafe, units, workarea,
|
2025-12-27 00:16:39 -05:00
|
|
|
|
camZTop, camZBottom, minToolDiam, maxToolDiam,
|
2025-12-14 00:42:23 -05:00
|
|
|
|
bottom_gap, bottom_part, bottom_stock, bottom_z,
|
2025-05-26 17:33:12 -06:00
|
|
|
|
top_stock, top_part, top_gap, top_z,
|
2025-12-14 00:42:23 -05:00
|
|
|
|
zBottom, zMin, zMax, zTop,
|
2025-12-06 00:09:17 -05:00
|
|
|
|
ztOff, zbOff, wztop;
|
2025-05-26 17:33:12 -06:00
|
|
|
|
|
2025-07-16 01:44:22 -04:00
|
|
|
|
axisRotation = axisIndex = undefined;
|
|
|
|
|
|
dark = settings.controller.dark;
|
|
|
|
|
|
color = dark ? 0xbbbbbb : 0;
|
|
|
|
|
|
minToolDiam = Infinity;
|
|
|
|
|
|
maxToolDiam = -Infinity;
|
2025-12-11 22:47:24 -05:00
|
|
|
|
tabs = widget.anno.tab ?? [];
|
2025-07-16 01:44:22 -04:00
|
|
|
|
unsafe = proc.camExpertFast;
|
|
|
|
|
|
units = settings.controller.units === 'in' ? 25.4 : 1;
|
|
|
|
|
|
|
2025-05-26 17:33:12 -06:00
|
|
|
|
// 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-12-27 00:16:39 -05:00
|
|
|
|
({ camZTop, camZBottom } = proc);
|
2025-05-26 17:33:12 -06:00
|
|
|
|
bounds = widget.getBoundingBox();
|
2025-12-06 00:09:17 -05:00
|
|
|
|
let pos = widget.track.pos;
|
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,
|
2025-12-06 00:09:17 -05:00
|
|
|
|
center: newPoint(pos.x, pos.y, pos.z)
|
2025-06-22 22:09:02 -04:00
|
|
|
|
} : {
|
|
|
|
|
|
x: camStockX,
|
|
|
|
|
|
y: camStockY,
|
2025-12-06 00:09:17 -05:00
|
|
|
|
z: camStockZ,
|
|
|
|
|
|
center: newPoint(pos.x, pos.y, pos.z)
|
2025-07-16 01:44:22 -04:00
|
|
|
|
};
|
2025-12-13 11:56:08 -05:00
|
|
|
|
if (!camStockOffset && axisIndex && isIndexed) {
|
|
|
|
|
|
if (axisIndex === 0 || axisIndex === 180) {
|
|
|
|
|
|
// do nothing
|
|
|
|
|
|
} else if (axisIndex === 90 || axisIndex === 270) {
|
|
|
|
|
|
// swap YZ
|
|
|
|
|
|
let tmp = stock.y;
|
|
|
|
|
|
stock.y = stock.z;
|
|
|
|
|
|
stock.z = tmp;
|
|
|
|
|
|
stock.center = newPoint(pos.x, pos.z, pos.y);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// compute YZ hypotenuse
|
|
|
|
|
|
let p = new THREE.Vector2(stock.y, stock.z);
|
|
|
|
|
|
let center = new THREE.Vector2(0, 0);
|
|
|
|
|
|
p.rotateAround(center, axisRotation);
|
|
|
|
|
|
stock.y = p.y;
|
|
|
|
|
|
stock.z = p.z;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-07-16 01:44:22 -04:00
|
|
|
|
track = widget.track;
|
2025-05-26 17:33:12 -06:00
|
|
|
|
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;
|
|
|
|
|
|
zTop = zMax + ztOff;
|
|
|
|
|
|
bottom_gap = zbOff;
|
|
|
|
|
|
bottom_part = 0;
|
|
|
|
|
|
bottom_stock = -bottom_gap;
|
2025-06-22 22:09:02 -04:00
|
|
|
|
bottom_z = isIndexed ? zBottom : Math.max(
|
2025-12-14 00:42:23 -05:00
|
|
|
|
(camZBottom ? bottom_stock + camZBottom : bottom_part),
|
|
|
|
|
|
(camZBottom ? bottom_stock + camZBottom : bottom_stock)
|
2025-05-26 17:33:12 -06:00
|
|
|
|
);
|
|
|
|
|
|
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,
|
2025-12-14 00:42:23 -05:00
|
|
|
|
bottom_stock, bottom_part, bottom_gap, bottom_z,
|
2025-02-07 17:52:41 -05:00
|
|
|
|
}, 3);
|
|
|
|
|
|
|
2026-01-24 00:27:48 -05:00
|
|
|
|
// console.log({ bounds, stock, track, workarea, camZBottom });
|
2025-08-10 23:48:10 -04:00
|
|
|
|
|
2025-05-26 17:33:12 -06:00
|
|
|
|
return structuredClone(workarea);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// initial setup
|
|
|
|
|
|
var_compute();
|
2022-02-21 10:01:19 -05:00
|
|
|
|
|
2023-03-27 15:20:51 -04:00
|
|
|
|
function error(msg) {
|
|
|
|
|
|
ondone(msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-22 11:21:32 -05:00
|
|
|
|
function alert(msg) {
|
|
|
|
|
|
onupdate(null,null,msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-05 00:31:27 -05:00
|
|
|
|
function shadowAt(z) {
|
|
|
|
|
|
return widget.shadowAt(z);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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-29 12:21:14 -04:00
|
|
|
|
if (stock.x === 0 || stock.y === 0 || stock.z === 0) {
|
|
|
|
|
|
return error("one or more stock dimensions is zero<br>offset stock or set to non zero value");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-12 23:11:34 -04:00
|
|
|
|
if (stock.x && stock.y && stock.z && !isIndexed) {
|
2025-09-23 18:33:03 -04:00
|
|
|
|
let maxDelta = 1e-3;
|
|
|
|
|
|
|
|
|
|
|
|
if (stock.x + maxDelta < (bounds.max.x - bounds.min.x)) {
|
2025-11-22 11:21:32 -05:00
|
|
|
|
alert('stock X too small for part');
|
2021-01-04 10:01:13 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-23 18:33:03 -04:00
|
|
|
|
if (stock.y + maxDelta < (bounds.max.y - bounds.min.y)) {
|
2025-11-22 11:21:32 -05:00
|
|
|
|
alert('stock Y too small for part');
|
2020-12-16 21:25:45 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-23 18:33:03 -04:00
|
|
|
|
if (stock.z + maxDelta < (bounds.max.z - bounds.min.z)) {
|
2025-11-22 11:21:32 -05:00
|
|
|
|
alert('stock Z too small for part');
|
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) {
|
2025-11-22 11:21:32 -05:00
|
|
|
|
alert(`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
|
|
|
|
|
2025-06-22 22:09:02 -04:00
|
|
|
|
let opList = [];
|
|
|
|
|
|
let opSum = 0;
|
|
|
|
|
|
let opTot = 0;
|
|
|
|
|
|
let slicer;
|
|
|
|
|
|
let state = {
|
2025-11-18 00:41:10 -05:00
|
|
|
|
addSlices,
|
2025-12-29 17:53:17 -05:00
|
|
|
|
axisIndex,
|
|
|
|
|
|
axisRotation,
|
2025-06-22 22:09:02 -04:00
|
|
|
|
bounds,
|
2025-11-18 00:41:10 -05:00
|
|
|
|
color,
|
|
|
|
|
|
computeShadows,
|
2025-06-27 01:02:12 -04:00
|
|
|
|
contourPolys,
|
2025-11-18 00:41:10 -05:00
|
|
|
|
cutTabs,
|
|
|
|
|
|
dark,
|
2025-06-22 22:09:02 -04:00
|
|
|
|
isIndexed,
|
2025-12-12 18:10:06 -05:00
|
|
|
|
newSlicer,
|
2025-11-18 00:41:10 -05:00
|
|
|
|
ops: opList,
|
2025-06-22 22:09:02 -04:00
|
|
|
|
setAxisIndex,
|
2025-11-28 21:09:09 -05:00
|
|
|
|
setToolDiam,
|
2025-11-18 00:41:10 -05:00
|
|
|
|
settings,
|
2026-01-05 00:31:27 -05:00
|
|
|
|
shadowAt,
|
2025-11-18 00:41:10 -05:00
|
|
|
|
slicer,
|
2025-12-06 00:21:31 -05:00
|
|
|
|
stock,
|
2025-11-18 00:41:10 -05:00
|
|
|
|
tabs,
|
2025-12-06 00:09:17 -05:00
|
|
|
|
tool,
|
2025-11-18 00:41:10 -05:00
|
|
|
|
unsafe,
|
2025-06-22 22:09:02 -04:00
|
|
|
|
updateSlicer,
|
2025-11-18 00:41:10 -05:00
|
|
|
|
updateToolDiams,
|
|
|
|
|
|
widget,
|
2025-06-22 22:09:02 -04:00
|
|
|
|
zBottom,
|
2025-11-18 00:41:10 -05:00
|
|
|
|
zMax,
|
2025-06-22 22:09:02 -04:00
|
|
|
|
ztOff,
|
2025-11-18 00:41:10 -05:00
|
|
|
|
zTop
|
2025-06-22 22:09:02 -04:00
|
|
|
|
};
|
|
|
|
|
|
let tracker = setSliceTracker({ rotation: 0 });
|
|
|
|
|
|
|
2025-11-27 10:32:41 -05:00
|
|
|
|
async function updateTab(tab) {
|
|
|
|
|
|
tab.setAxisIndex(isIndexed ? -axisIndex : 0)
|
|
|
|
|
|
let ts = new cam_slicer(tab);
|
|
|
|
|
|
let si = Object.keys(ts.zList).map(k => parseFloat(k));
|
|
|
|
|
|
si = [ ...si.map(v => v-0.01), ...si.map(v => v+0.01) ];
|
|
|
|
|
|
let zt = Math.max(...si);
|
|
|
|
|
|
let zb = Math.min(...si);
|
|
|
|
|
|
let shadow;
|
|
|
|
|
|
await ts.slice(si, { each: data => {
|
|
|
|
|
|
if (shadow) {
|
|
|
|
|
|
shadow = POLY.union([...shadow, ...data.polys]);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
shadow = data.polys;
|
|
|
|
|
|
}
|
|
|
|
|
|
}});
|
|
|
|
|
|
return {
|
|
|
|
|
|
top: zt,
|
|
|
|
|
|
poly: shadow[0],
|
|
|
|
|
|
pos: { z: (zt+zb)/2 },
|
|
|
|
|
|
dim: { z: (zt-zb) },
|
|
|
|
|
|
NEW: true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 16:28:18 -05:00
|
|
|
|
function updateProgress(percent, message) {
|
|
|
|
|
|
const mark = Date.now();
|
|
|
|
|
|
if (false) console.log({
|
|
|
|
|
|
time: ((mark - lastTime)/1000).toFixed(2),
|
|
|
|
|
|
pct: ((percent ?? 0) * 100).toFixed(2),
|
|
|
|
|
|
msg: message
|
|
|
|
|
|
});
|
|
|
|
|
|
onupdate(percent, message);
|
|
|
|
|
|
lastTime = mark;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-12 18:10:06 -05:00
|
|
|
|
function newSlicer(opts) {
|
|
|
|
|
|
return new cam_slicer(widget, opts);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updateSlicer(opts) {
|
|
|
|
|
|
return slicer = state.slicer = new cam_slicer(widget, opts);
|
2025-06-22 22:09:02 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 19:43:51 -05:00
|
|
|
|
async function computeShadows(progress) {
|
|
|
|
|
|
console.time('compute shadow');
|
|
|
|
|
|
await new OPS.shadow(state, { type: "shadow", xsilent: true }).slice(progress);
|
|
|
|
|
|
console.timeEnd('compute shadow');
|
2025-06-22 22:09:02 -04:00
|
|
|
|
}
|
2020-11-05 15:41:52 -05:00
|
|
|
|
|
2025-11-28 21:09:09 -05:00
|
|
|
|
function setToolDiam(toolDiam) {
|
|
|
|
|
|
updateToolDiams(toolDiam);
|
|
|
|
|
|
if (tabs) {
|
|
|
|
|
|
tabs.forEach(tab => {
|
|
|
|
|
|
tab.off = POLY.expand([ tab.poly ], toolDiam / 2).flat();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
|
2025-11-27 10:32:41 -05:00
|
|
|
|
async function setAxisIndex(degrees = 0, absolute = true) {
|
2025-12-29 17:53:17 -05:00
|
|
|
|
state.axisIndex = axisIndex = absolute ? degrees : (axisIndex || 0) + degrees;
|
|
|
|
|
|
state.axisRotation = axisRotation = (Math.PI / 180) * axisIndex;
|
2022-11-03 12:29:07 -04:00
|
|
|
|
widget.setAxisIndex(isIndexed ? -axisIndex : 0);
|
2025-11-27 10:32:41 -05:00
|
|
|
|
state.tabs = tabs = [];
|
|
|
|
|
|
for (let tab of tabW) {
|
|
|
|
|
|
tabs.push(await updateTab(tab));
|
|
|
|
|
|
}
|
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)) {
|
2025-07-06 23:44:33 -04:00
|
|
|
|
slices = [slices];
|
2022-10-28 17:18:01 -04:00
|
|
|
|
}
|
|
|
|
|
|
sliceAll.appendAll(slices);
|
2023-02-11 11:10:20 -05:00
|
|
|
|
if (addIndexing && axisIndex !== undefined) {
|
2025-12-14 23:00:10 -05:00
|
|
|
|
for (let slice of slices) {
|
|
|
|
|
|
slice.angle = axisIndex;
|
|
|
|
|
|
}
|
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) {
|
2026-01-03 11:42:37 -05:00
|
|
|
|
opList.push({ op: new OPS.xray(state, { type: "xray" }) });
|
2022-02-21 10:01:19 -05:00
|
|
|
|
}
|
2020-11-05 15:41:52 -05:00
|
|
|
|
|
2026-01-02 22:49:36 -05:00
|
|
|
|
// find index ops
|
2025-06-22 22:09:02 -04:00
|
|
|
|
let activeOps = proc.ops.filter(op => !op.disabled);
|
|
|
|
|
|
|
|
|
|
|
|
if (isIndexed) {
|
2025-12-06 00:09:17 -05:00
|
|
|
|
// preface op list with OpIndex
|
2025-07-12 16:08:34 -04:00
|
|
|
|
if (activeOps.length === 0 || activeOps[0].type !== 'index') {
|
2026-01-03 11:42:37 -05:00
|
|
|
|
opList.push({ op: new OPS.index(state, { type: "index", degrees: 0, absolute: true }) });
|
|
|
|
|
|
opTot += opList.peek().op.weight();
|
2025-07-12 16:08:34 -04:00
|
|
|
|
|
|
|
|
|
|
}
|
2025-06-22 22:09:02 -04:00
|
|
|
|
} else {
|
2025-12-06 00:09:17 -05:00
|
|
|
|
// preface op list with OpShadow
|
2026-01-03 11:42:37 -05:00
|
|
|
|
opList.push({ op: new OPS.shadow(state, { type: "shadow", silent: true }) });
|
|
|
|
|
|
opTot += opList.peek().op.weight();
|
2025-06-22 22:09:02 -04:00
|
|
|
|
}
|
2020-12-16 21:25:45 -05:00
|
|
|
|
|
2026-01-02 21:25:56 -05:00
|
|
|
|
// LOOP EXPANSION - duplicate next N operations M times
|
|
|
|
|
|
let expandedOps = [];
|
|
|
|
|
|
|
2026-01-02 22:49:36 -05:00
|
|
|
|
for (let i=0; i<activeOps.length; i++) {
|
2026-01-02 21:25:56 -05:00
|
|
|
|
let op = activeOps[i];
|
|
|
|
|
|
if (op.type === 'loop') {
|
2026-01-02 22:49:36 -05:00
|
|
|
|
let { count, repeat } = op;
|
|
|
|
|
|
let remain = activeOps.slice(i+1).filter(op => op.type !== '|');
|
|
|
|
|
|
if (remain < count) {
|
|
|
|
|
|
return error(`Loop operation requires ${count} following operation(s), but only ${remain.length - i - 1} available`);
|
2026-01-02 21:25:56 -05:00
|
|
|
|
}
|
2026-01-02 22:49:36 -05:00
|
|
|
|
for (let r=1; r<repeat; r++)
|
|
|
|
|
|
for (let c=0; c<count; c++) {
|
|
|
|
|
|
let dup = remain[c];
|
|
|
|
|
|
if (dup.type === 'loop') {
|
|
|
|
|
|
return error("Nested loops are not supported");
|
|
|
|
|
|
}
|
|
|
|
|
|
expandedOps.push(dup);
|
2026-01-02 21:25:56 -05:00
|
|
|
|
}
|
|
|
|
|
|
} else if (op.type === '|') {
|
2026-01-02 22:49:36 -05:00
|
|
|
|
break;
|
2026-01-02 21:25:56 -05:00
|
|
|
|
} else {
|
|
|
|
|
|
expandedOps.push(op);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
activeOps = expandedOps;
|
|
|
|
|
|
|
2025-11-30 18:03:18 -05:00
|
|
|
|
// determine # of steps and step weighting for progress bar
|
2026-01-03 11:42:37 -05:00
|
|
|
|
let opos = 0;
|
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);
|
|
|
|
|
|
opTot += opin.weight();
|
2026-01-03 11:42:37 -05:00
|
|
|
|
opos++;
|
|
|
|
|
|
let share = shared.ops[opos];
|
|
|
|
|
|
if (!share) {
|
|
|
|
|
|
share = shared.ops[opos] = {};
|
|
|
|
|
|
}
|
|
|
|
|
|
opList.push({ op: opin, share });
|
2020-11-05 15:41:52 -05:00
|
|
|
|
}
|
2022-02-21 10:01:19 -05:00
|
|
|
|
}
|
2020-11-05 15:41:52 -05:00
|
|
|
|
|
2026-01-03 16:28:18 -05:00
|
|
|
|
// drop last op if it's type index
|
|
|
|
|
|
if (opList.peek().op.op.type === 'index') {
|
|
|
|
|
|
opList.pop();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-10-31 12:14:28 -04:00
|
|
|
|
// call slice() function on all ops in order
|
2025-11-27 10:32:41 -05:00
|
|
|
|
await setAxisIndex();
|
2025-06-22 22:09:02 -04:00
|
|
|
|
updateSlicer();
|
2026-01-03 11:42:37 -05:00
|
|
|
|
for (let { op, share } of opList) {
|
2022-02-21 10:01:19 -05:00
|
|
|
|
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-12-13 23:44:50 -05:00
|
|
|
|
let { ov_topz, ov_botz } = valz;
|
|
|
|
|
|
if (ov_topz) {
|
|
|
|
|
|
workover.top_z = isIndexed ? ov_topz : bottom_stock + ov_topz;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ov_botz) {
|
|
|
|
|
|
workover.bottom_z = isIndexed ? ov_botz : bottom_stock + ov_botz;
|
2025-02-09 23:38:52 -05:00
|
|
|
|
}
|
2025-12-14 13:12:43 -05:00
|
|
|
|
if (workover.bottom_z >= workover.top_z) {
|
|
|
|
|
|
return error("Z Bottom cannot be above or equal to Z Top");
|
|
|
|
|
|
}
|
2025-12-06 00:09:17 -05:00
|
|
|
|
if (valz.tool) {
|
|
|
|
|
|
tool = new Tool(settings, valz.tool);
|
|
|
|
|
|
}
|
2025-11-13 22:25:06 -05:00
|
|
|
|
let { note, type } = op.op;
|
|
|
|
|
|
let named = note ? note.split(' ').filter(v => v.charAt(0) === '#') : [];
|
|
|
|
|
|
let layername = named.length ? named : (note ? `${type} (${note})` : type);
|
2025-06-22 22:09:02 -04:00
|
|
|
|
Object.assign(state, {
|
2025-11-13 22:25:06 -05:00
|
|
|
|
layername,
|
2026-01-03 11:42:37 -05:00
|
|
|
|
share,
|
2025-12-13 11:56:08 -05:00
|
|
|
|
stock,
|
2025-12-06 00:09:17 -05:00
|
|
|
|
tool,
|
2025-06-22 22:09:02 -04:00
|
|
|
|
zBottom,
|
|
|
|
|
|
ztOff,
|
|
|
|
|
|
zMax,
|
|
|
|
|
|
zTop,
|
|
|
|
|
|
workarea: workover
|
|
|
|
|
|
});
|
2025-12-06 00:09:17 -05:00
|
|
|
|
let operr;
|
2023-01-08 14:04:02 -05:00
|
|
|
|
await op.slice((progress, message) => {
|
2026-01-03 16:28:18 -05:00
|
|
|
|
updateProgress((opSum + (progress * weight)) / opTot, message || op.type());
|
2025-12-06 00:09:17 -05:00
|
|
|
|
}).catch(e => {
|
|
|
|
|
|
operr = e;
|
|
|
|
|
|
console.trace(e);
|
2022-02-21 10:01:19 -05:00
|
|
|
|
});
|
2025-12-06 00:09:17 -05:00
|
|
|
|
if (operr) {
|
|
|
|
|
|
return error(operr);
|
|
|
|
|
|
}
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2025-07-10 16:54:02 -04:00
|
|
|
|
export async function traces(settings, widget) {
|
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 ---
|
2025-06-27 13:32:17 -04:00
|
|
|
|
let edges = new THREE.EdgesGeometry(widget.mesh.geometry, settings.controller.edgeangle ?? 20);
|
2025-06-21 19:14:11 -04:00
|
|
|
|
let array = edges.attributes.position.array;
|
|
|
|
|
|
let pcache = {};
|
|
|
|
|
|
let points = new Array(2);
|
|
|
|
|
|
let lines = new Array(points.length / 2);
|
2025-07-06 23:44:33 -04:00
|
|
|
|
for (let i = 0, j = 0, k = 0, l = array.length; i < l;) {
|
|
|
|
|
|
let ps = [array[i++], array[i++], array[i++]];
|
2025-06-21 19:14:11 -04:00
|
|
|
|
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) {
|
2025-07-06 23:44:33 -04:00
|
|
|
|
let [p0, p1] = points;
|
2025-06-21 19:14:11 -04:00
|
|
|
|
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-09-22 21:38:02 -04:00
|
|
|
|
}
|
2025-06-30 20:46:58 -06:00
|
|
|
|
|
2025-07-11 15:45:42 -06:00
|
|
|
|
/**
|
|
|
|
|
|
* Returns an array of arrays of perpindicular triangles in the mesh.
|
|
|
|
|
|
* Each sub-array is a list of triangle data that are part of the same
|
|
|
|
|
|
* cylinder.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {object} settings - settings object
|
|
|
|
|
|
* @param {object} widget - widget object
|
|
|
|
|
|
* @param {object} opts - options object
|
|
|
|
|
|
* @return {array} - array of arrays of triangle data
|
|
|
|
|
|
*/
|
2025-09-22 21:38:02 -04:00
|
|
|
|
export async function cylinders(settings, widget, opts) {
|
|
|
|
|
|
let { } = opts ?? {};
|
|
|
|
|
|
let { array: verts } = widget.mesh.geometry.attributes.position;
|
|
|
|
|
|
let perpTriangles = [];
|
2025-07-02 17:10:32 -06:00
|
|
|
|
|
|
|
|
|
|
//iterate over all triangles
|
2025-09-22 21:38:02 -04:00
|
|
|
|
for (let i = 0; i < verts.length; i += 9) {
|
|
|
|
|
|
let a = [verts[i], verts[i + 1], verts[i + 2]],
|
|
|
|
|
|
b = [verts[i + 3], verts[i + 4], verts[i + 5]],
|
|
|
|
|
|
c = [verts[i + 6], verts[i + 7], verts[i + 8]];
|
2025-07-02 17:10:32 -06:00
|
|
|
|
|
|
|
|
|
|
//calculate normal
|
2025-09-22 21:38:02 -04:00
|
|
|
|
let normal = new THREE.Vector3(a[0] - b[0], a[1] - b[1], a[2] - b[2])
|
|
|
|
|
|
.cross(new THREE.Vector3(b[0] - c[0], b[1] - c[1], b[2] - c[2]))
|
|
|
|
|
|
.normalize();
|
2025-07-02 17:10:32 -06:00
|
|
|
|
|
|
|
|
|
|
// if perpindicular normal, and at least 2 Zs are the same
|
2025-09-22 21:38:02 -04:00
|
|
|
|
if (normal.z.round(5) == 0 && !(a[2] != b[2] && b[2] != c[2])) {
|
2025-07-02 17:10:32 -06:00
|
|
|
|
let minZ = Math.min(a[2], b[2], c[2]),
|
|
|
|
|
|
maxZ = Math.max(a[2], b[2], c[2]);
|
2025-09-22 21:38:02 -04:00
|
|
|
|
if (minZ == maxZ) { // all Zs are the same indicated malformed geometry
|
2025-07-02 17:10:32 -06:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2025-09-22 21:38:02 -04:00
|
|
|
|
perpTriangles.push({ ...[a, b, c], normal, minZ, maxZ, i: index });
|
2025-07-02 17:10:32 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
//map where zmax, zmin -> triangleData
|
2025-09-22 21:38:02 -04:00
|
|
|
|
let cylinderTriangles = new Map();
|
|
|
|
|
|
for (let t of perpTriangles) {
|
|
|
|
|
|
let hash = `${t.minZ.round(5)},${t.maxZ.round(5)}`;
|
|
|
|
|
|
if (!cylinderTriangles.has(hash)) {
|
|
|
|
|
|
cylinderTriangles.set(hash, []);
|
2025-07-02 17:10:32 -06:00
|
|
|
|
}
|
2025-09-22 21:38:02 -04:00
|
|
|
|
cylinderTriangles.get(hash).push(t);
|
2025-07-02 17:10:32 -06:00
|
|
|
|
}
|
2025-06-30 20:46:58 -06:00
|
|
|
|
|
2025-09-22 21:38:02 -04:00
|
|
|
|
return Array.from(cylinderTriangles.values());
|
|
|
|
|
|
}
|
2025-07-04 17:41:22 -06:00
|
|
|
|
|
2025-09-22 21:38:02 -04:00
|
|
|
|
/**
|
|
|
|
|
|
* Find all triangles that are part of the same cylinder as the given triangle
|
|
|
|
|
|
* @param {Widget} widget - widget object
|
|
|
|
|
|
* @param {number} face - index of face in triangle vertex data
|
|
|
|
|
|
* @return {array} - array of all triangle data that belong to the same cylinder
|
|
|
|
|
|
* @requires surface_prep must be called first
|
|
|
|
|
|
*/
|
2025-07-11 22:22:48 -06:00
|
|
|
|
export function cylinder_find (widget, face){
|
|
|
|
|
|
CAM.surface_prep(widget,false);
|
2025-07-04 17:41:22 -06:00
|
|
|
|
return widget.tool.findCylinderSurface(face);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-22 21:38:02 -04:00
|
|
|
|
export function cylinder_poly_find(widget, face) {
|
2025-07-11 15:45:42 -06:00
|
|
|
|
let faces = cylinder_find(widget, face);
|
2025-07-05 17:51:17 -06:00
|
|
|
|
let vert = widget.getGeoVertices({ unroll: true, translate: true }).map(v => v.round(4));
|
2025-09-22 21:38:02 -04:00
|
|
|
|
let slicer = new cam_slicer(widget, {});
|
|
|
|
|
|
let firstOffset = faces[0] * 9;
|
|
|
|
|
|
let [x1, y1, z1, x2, y2, z2, x3, y3, z3] = Array.from(vert.subarray(firstOffset, firstOffset + 9));
|
|
|
|
|
|
let zs = [z1, z2, z3].map(z => z.round(5));
|
2025-07-05 17:51:17 -06:00
|
|
|
|
let zmin = Math.min(...zs);
|
|
|
|
|
|
let zmax = Math.max(...zs);
|
2025-09-22 21:38:02 -04:00
|
|
|
|
let zmid = (zmin + zmax) / 2;
|
|
|
|
|
|
let cylVerts = new Float32Array(faces.length * 9);
|
|
|
|
|
|
let opts = {
|
2025-11-26 13:09:00 -05:00
|
|
|
|
dedup: true,
|
2025-09-22 21:38:02 -04:00
|
|
|
|
edges: false,
|
|
|
|
|
|
over: true
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-07-05 17:51:17 -06:00
|
|
|
|
let i = 0
|
|
|
|
|
|
//for each cylinder face index
|
2025-09-22 21:38:02 -04:00
|
|
|
|
for (let index of faces) {
|
|
|
|
|
|
let off = index * 9;
|
2025-07-05 17:51:17 -06:00
|
|
|
|
//copy the 9 triangle vertices floats in
|
2025-09-22 21:38:02 -04:00
|
|
|
|
for (let j = 0; j < 9; j++) {
|
|
|
|
|
|
cylVerts[i] = vert[off + j];
|
|
|
|
|
|
i++;
|
2025-07-05 17:51:17 -06:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-22 21:38:02 -04:00
|
|
|
|
|
2025-07-11 22:22:48 -06:00
|
|
|
|
//slice the poly at the midpoint
|
2025-09-22 21:38:02 -04:00
|
|
|
|
let poly = slicer.sliceZ(zmid, cylVerts, opts)?.polys?.at(0);
|
|
|
|
|
|
if (!poly) throw "slicing returned no poly";
|
|
|
|
|
|
|
2025-07-05 17:51:17 -06:00
|
|
|
|
let circular = poly.circularity() > 0.98;
|
2025-12-09 00:21:26 -05:00
|
|
|
|
// console.log({ zmid, cylVerts, opts, poly, circularity: poly.circularity() });
|
2025-09-22 21:38:02 -04:00
|
|
|
|
|
2025-07-05 17:51:17 -06:00
|
|
|
|
//throw error if not circular
|
2025-09-22 21:38:02 -04:00
|
|
|
|
if (!circular) throw "faces must be circular";
|
|
|
|
|
|
|
2025-07-05 17:51:17 -06:00
|
|
|
|
//calculate circle properties
|
|
|
|
|
|
let area = poly.area(),
|
2025-09-22 21:38:02 -04:00
|
|
|
|
diam = Math.sqrt(area / Math.PI) * 2,
|
2025-07-05 17:51:17 -06:00
|
|
|
|
center = poly.calcCircleCenter();
|
|
|
|
|
|
|
2025-09-22 21:38:02 -04:00
|
|
|
|
//find direction using normal to center point
|
2025-07-05 17:51:17 -06:00
|
|
|
|
let point = newPoint(x1, y1, z1),
|
|
|
|
|
|
delta = point.setZ(center.z).sub(center),
|
2025-09-22 21:38:02 -04:00
|
|
|
|
normal = new THREE.Vector3(x1 - x2, y1 - y2, z1 - z2)
|
|
|
|
|
|
.cross(new THREE.Vector3(x2 - x3, y2 - y3, z2 - z3)),
|
2025-07-05 17:51:17 -06:00
|
|
|
|
dotProd = normal.dot(delta),
|
2025-09-22 21:38:02 -04:00
|
|
|
|
interior = dotProd < 0
|
2025-07-05 17:51:17 -06:00
|
|
|
|
|
2025-09-22 21:38:02 -04:00
|
|
|
|
return { faces, zmin, zmax, poly, circular, area, diam, center, interior }
|
2025-07-05 17:51:17 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
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-07-06 23:44:33 -04:00
|
|
|
|
export async function holes(settings, widget, individual, rec, onProgress) {
|
2025-05-27 11:19:51 -06:00
|
|
|
|
|
2025-11-28 21:09:09 -05:00
|
|
|
|
let { tool, precision } = rec //TODO: display some visual difference if mark is selected
|
2025-07-06 23:44:33 -04:00
|
|
|
|
let toolDiam = new Tool(settings, tool).fluteDiameter()
|
2025-05-27 11:19:51 -06:00
|
|
|
|
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,
|
2025-12-27 00:16:39 -05:00
|
|
|
|
{ camZBottom } = proc,
|
2025-04-24 11:30:38 -06:00
|
|
|
|
// 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-08-31 22:13:42 -04:00
|
|
|
|
let slicerOpts = { flatoff: 0.001 };
|
2025-07-06 23:44:33 -04:00
|
|
|
|
let slicer = new cam_slicer(widget, slicerOpts);
|
2025-08-31 22:13:42 -04: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-07-06 23:44:33 -04:00
|
|
|
|
precision = Math.max(0, precision)
|
2025-05-27 11:19:51 -06:00
|
|
|
|
let intervals = (precision == 0) ? [] : slicer.interval(
|
|
|
|
|
|
precision,
|
2025-07-06 23:44:33 -04:00
|
|
|
|
{
|
2025-05-27 11:19:51 -06:00
|
|
|
|
fit: false, off: -0.01, flats: true
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
2025-05-01 09:39:54 -06:00
|
|
|
|
|
2025-08-31 22:13:42 -04:00
|
|
|
|
let zees = [...zFlats, ...intervals];
|
2025-05-01 09:39:54 -06:00
|
|
|
|
let indices = [...new Set(zees
|
2025-04-24 11:30:38 -06:00
|
|
|
|
.map(kv => parseFloat(kv).round(5))
|
|
|
|
|
|
.filter(z => z !== null)
|
2025-08-31 22:13:42 -04:00
|
|
|
|
)];
|
2025-04-24 11:30:38 -06:00
|
|
|
|
let centerDiff = diam * 0.1,
|
2025-07-06 23:44:33 -04:00
|
|
|
|
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-07-06 23:44:33 -04: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;
|
2025-10-29 19:40:52 -04:00
|
|
|
|
|
2025-07-06 23:44:33 -04:00
|
|
|
|
for (let [i, slice] of slices.entries()) {
|
|
|
|
|
|
for (let top of slice.tops) {
|
2025-05-01 09:39:54 -06:00
|
|
|
|
// console.log("slicing",slice.z,top)
|
2025-12-06 00:09:17 -05:00
|
|
|
|
slice.shadow = await widget.shadowAt(slice.z);
|
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-10-29 19:40:52 -04: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-08-31 22:13:42 -04:00
|
|
|
|
center.overlapping = [center];
|
2025-05-08 11:13:14 -06:00
|
|
|
|
center.depth = 0;
|
2025-08-31 22:13:42 -04:00
|
|
|
|
if (poly.circularity() < 0.98) {
|
2025-05-08 11:13:14 -06:00
|
|
|
|
// 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-07-06 23:44:33 -04:00
|
|
|
|
for (let [i, circle] of circles.entries()) {
|
2025-08-31 22:13:42 -04:00
|
|
|
|
let dist = circle.distTo2D(center);
|
2025-10-29 19:40:52 -04:00
|
|
|
|
// if on the same xy point,
|
2025-07-06 23:44:33 -04:00
|
|
|
|
if (dist <= centerDiff) {
|
2025-05-08 11:13:14 -06:00
|
|
|
|
// 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-08-31 22:13:42 -04:00
|
|
|
|
onProgress(0.5 + (i / slices.length * 0.25), "recognize circles");
|
2025-04-24 11:30:38 -06:00
|
|
|
|
}
|
2025-08-31 22:13:42 -04:00
|
|
|
|
let drills = [];
|
2025-05-08 11:13:14 -06:00
|
|
|
|
|
2025-07-06 23:44:33 -04:00
|
|
|
|
for (let [i, c] of circles.entries()) {
|
2025-08-31 22:13:42 -04:00
|
|
|
|
let overlapping = c.overlapping;
|
2025-05-08 11:13:14 -06:00
|
|
|
|
let last = overlapping.shift();
|
|
|
|
|
|
while (overlapping.length) {
|
|
|
|
|
|
let circ = overlapping.shift();
|
|
|
|
|
|
let aveArea = (circ.area + last.area) / 2;
|
2025-08-31 22:13:42 -04:00
|
|
|
|
let areaDelta = Math.abs(circ.area - last.area);
|
2025-07-06 23:44:33 -04:00
|
|
|
|
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;
|
2025-05-08 11:13:14 -06:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-31 22:13:42 -04:00
|
|
|
|
if (last.depth != 0) drills.push(last); // add last circle
|
|
|
|
|
|
onProgress(0.75 + (i / circles.length * 0.25), "assemble holes");
|
2025-05-08 11:13:14 -06:00
|
|
|
|
}
|
2025-07-06 23:44:33 -04:00
|
|
|
|
drills.forEach(h => {
|
|
|
|
|
|
if (rec.fromTop) {
|
2025-06-12 09:41:39 -06:00
|
|
|
|
// set z top if selected
|
2025-08-31 22:13:42 -04:00
|
|
|
|
h.depth += wztop - h.z;
|
|
|
|
|
|
h.z = wztop;
|
2025-06-12 09:41:39 -06:00
|
|
|
|
}
|
2025-08-31 22:13:42 -04:00
|
|
|
|
delete h.overlapping; //for encoding
|
|
|
|
|
|
h.diam = toolDiam; // for mesh generation
|
|
|
|
|
|
h.selected = (!individual && Math.abs(h.area - area) <= area * 0.05); // for same size selection
|
2025-07-06 23:44:33 -04:00
|
|
|
|
})
|
2025-05-08 11:13:14 -06:00
|
|
|
|
|
2025-12-20 15:46:21 -07:00
|
|
|
|
// console.log("unfiltered circles",circles);
|
|
|
|
|
|
// console.log("drills",drills);
|
2025-08-31 22:13:42 -04:00
|
|
|
|
|
|
|
|
|
|
drills = drills.filter(drill => drill.depth > 0);
|
|
|
|
|
|
widget.shadowedDrills = shadowedDrills;
|
2025-05-08 11:13:14 -06:00
|
|
|
|
widget.drills = drills;
|
2025-04-24 11:30:38 -06:00
|
|
|
|
return drills;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-14 18:26:50 -05:00
|
|
|
|
function cutTabs(tabs, offset) {
|
|
|
|
|
|
let out = [];
|
|
|
|
|
|
for (let tab of tabs) {
|
|
|
|
|
|
tab.top = tab.pos.z + tab.dim.z / 2;
|
|
|
|
|
|
}
|
|
|
|
|
|
for (let poly of offset) {
|
|
|
|
|
|
let polyZ = poly.getZ();
|
|
|
|
|
|
let cut = tabs.filter(tab => tab.top >= polyZ);
|
|
|
|
|
|
let lo = poly.cut(cut.map(tab => tab.off).flat(), false);
|
|
|
|
|
|
let hi = [];
|
|
|
|
|
|
for (let tab of cut) {
|
|
|
|
|
|
hi.appendAll(POLY.setZ(poly.cut(tab.off, true), tab.top));
|
|
|
|
|
|
}
|
|
|
|
|
|
if (hi.length) {
|
2025-12-06 14:23:56 -05:00
|
|
|
|
let heal = POLY.reconnect([ ...lo, ...hi ], false);
|
2025-11-14 18:26:50 -05:00
|
|
|
|
POLY.setWinding(heal, poly.isClockwise());
|
|
|
|
|
|
out.push(...heal);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
out.push(poly);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return out;
|
2022-02-21 10:01:19 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
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);
|
2025-06-27 01:09:58 -04:00
|
|
|
|
const intersects = raycaster.intersectObject(widget.mesh, false);
|
2025-06-27 01:02:12 -04:00
|
|
|
|
const firstHit = intersects[0] || null;
|
2025-07-06 23:44:33 -04:00
|
|
|
|
if (firstHit) point.z = firstHit.point.z;
|
2025-06-27 01:02:12 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|