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

798 lines
26 KiB
JavaScript
Raw Normal View History

/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
2025-12-06 18:08:09 -05:00
import { CAM } from './driver-be.js';
import { newLine } from '../../../geo/line.js';
import { newPoint } from '../../../geo/point.js';
import { newPolygon } from '../../../geo/polygon.js';
2025-12-06 18:08:09 -05:00
import { ops as OPS } from './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';
2025-12-06 18:08:09 -05:00
import { Tool } from './tool.js';
import { util } from '../../../geo/base.js';
/**
* DRIVER SLICE CONTRACT
*
* @param {Object} settings
* @param {Widget} widget
* @param {Function} output
*/
export async function cam_slice(settings, widget, onupdate, ondone) {
if (widget.track.synth) return ondone();
2025-12-11 22:47:24 -05:00
// get tab widgets
let tabW = widget.group.filter(w => w != widget);
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);
2024-09-14 11:36:56 -04:00
let proc = settings.process,
camOps = widget.camops = [],
sliceAll = widget.slices = [],
isIndexed = proc.camStockIndexed;
let axisRotation, axisIndex,
bounds, dark, color, stock, tabs, track, tool, unsafe, units, workarea,
camZTop, camZBottom, camZThru, minToolDiam, maxToolDiam,
2025-12-14 00:42:23 -05:00
bottom_gap, bottom_part, bottom_stock, bottom_z,
top_stock, top_part, top_gap, top_z,
2025-12-14 00:42:23 -05:00
zBottom, zMin, zMax, zTop,
ztOff, zbOff, wztop;
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 ?? [];
unsafe = proc.camExpertFast;
units = settings.controller.units === 'in' ? 25.4 : 1;
// allow recomputing later if widget or settings changes
const var_compute = () => {
let { camStockX, camStockY, camStockZ, camStockOffset } = proc;
({ camZTop, camZBottom, camZThru } = proc);
bounds = widget.getBoundingBox();
let pos = widget.track.pos;
stock = camStockOffset ? {
x: bounds.dim.x + camStockX,
y: bounds.dim.y + camStockY,
z: bounds.dim.z + camStockZ,
center: newPoint(pos.x, pos.y, pos.z)
} : {
x: camStockX,
y: camStockY,
z: camStockZ,
center: newPoint(pos.x, pos.y, pos.z)
};
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;
}
}
track = widget.track;
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);
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;
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)
);
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,
2025-12-14 00:42:23 -05:00
bottom_stock, bottom_part, bottom_gap, bottom_z,
}, 3);
2025-11-26 12:58:46 -05:00
// console.log({ bounds, stock, track, workarea });
return structuredClone(workarea);
};
// initial setup
var_compute();
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);
}
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 === 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) {
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');
}
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');
}
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');
}
}
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)}`);
}
let opList = [];
let opSum = 0;
let opTot = 0;
let slicer;
let state = {
2025-11-18 00:41:10 -05:00
addSlices,
bounds,
2025-11-18 00:41:10 -05:00
color,
computeShadows,
contourPolys,
2025-11-18 00:41:10 -05:00
cutTabs,
dark,
isIndexed,
2025-12-12 18:10:06 -05:00
newSlicer,
2025-11-18 00:41:10 -05:00
ops: opList,
setAxisIndex,
2025-11-28 21:09:09 -05:00
setToolDiam,
2025-11-18 00:41:10 -05:00
settings,
shadowAt(z) { return widget.shadowAt(z) },
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,
tool,
2025-11-18 00:41:10 -05:00
unsafe,
updateSlicer,
2025-11-18 00:41:10 -05:00
updateToolDiams,
widget,
zBottom,
2025-11-18 00:41:10 -05:00
zMax,
ztOff,
2025-11-18 00:41:10 -05:00
zTop
};
let tracker = setSliceTracker({ rotation: 0 });
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
}
}
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);
}
async function computeShadows() {
// console.log('(re)compute shadows');
await new OPS.shadow(state, { type: "shadow", silent: true }).slice(progress => {
// console.log('reshadowing', progress.round(3));
});
}
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();
});
}
}
function updateToolDiams(toolDiam) {
minToolDiam = Math.min(minToolDiam, toolDiam);
maxToolDiam = Math.max(maxToolDiam, toolDiam);
}
async function setAxisIndex(degrees = 0, absolute = true) {
axisIndex = absolute ? degrees : (axisIndex || 0) + degrees;
axisRotation = (Math.PI / 180) * axisIndex;
2022-11-03 12:29:07 -04:00
widget.setAxisIndex(isIndexed ? -axisIndex : 0);
state.tabs = tabs = [];
for (let tab of tabW) {
tabs.push(await updateTab(tab));
}
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) {
for (let slice of slices) {
slice.angle = axisIndex;
}
// 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);
}
}
}
if (false) {
opList.push(new OPS.xray(state, { type: "xray" }));
}
let activeOps = proc.ops.filter(op => !op.disabled);
if (isIndexed) {
// preface op list with OpIndex
2025-07-12 16:08:34 -04:00
if (activeOps.length === 0 || activeOps[0].type !== 'index') {
opList.push(new OPS.index(state, { type: "index", degrees: 0, absolute: true }));
opTot += opList.peek().weight();
2025-07-12 16:08:34 -04:00
}
} else {
// preface op list with OpShadow
opList.push(new OPS.shadow(state, { type: "shadow", silent: true }));
opTot += opList.peek().weight();
}
// determine # of steps and step weighting for progress bar
for (let op of activeOps) {
if (op.type === '|') {
break;
}
let opfn = OPS[op.type];
if (opfn) {
let opin = new opfn(state, op);
opList.push(opin);
opTot += opin.weight();
}
}
// call slice() function on all ops in order
await setAxisIndex();
updateSlicer();
for (let op of opList) {
let weight = op.weight();
// apply operation override vars
let workover = var_compute();
let valz = op.op;
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-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");
}
if (valz.tool) {
tool = new Tool(settings, valz.tool);
}
let { note, type } = op.op;
let named = note ? note.split(' ').filter(v => v.charAt(0) === '#') : [];
let layername = named.length ? named : (note ? `${type} (${note})` : type);
Object.assign(state, {
layername,
stock,
tool,
zBottom,
ztOff,
zMax,
zTop,
workarea: workover
});
let operr;
2023-01-08 14:04:02 -05:00
await op.slice((progress, message) => {
onupdate((opSum + (progress * weight)) / opTot, message || op.type());
}).catch(e => {
operr = e;
console.trace(e);
});
if (operr) {
return error(operr);
}
// update tracker rotation for next slice output() visualization
tracker.rotation = isIndexed ? axisRotation : 0;
camOps.push(op);
opSum += weight;
}
setSliceTracker();
// reindex
sliceAll.forEach((slice, index) => slice.index = index);
widget.minToolDiam = minToolDiam;
widget.maxToolDiam = maxToolDiam;
ondone();
};
export async function traces(settings, widget) {
if (widget.traces) {
return false;
}
// --- points → line segments ---
2025-06-27 13:32:17 -04:00
let edges = new THREE.EdgesGeometry(widget.mesh.geometry, settings.controller.edgeangle ?? 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;
}
}
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
widget.traces = polylines;
2025-04-24 11:30:38 -06: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 = {
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;
// 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
}
/**
* 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.
*/
export async function holes(settings, widget, individual, rec, onProgress) {
2025-11-28 21:09:09 -05:00
let { tool, precision } = rec //TODO: display some visual difference if mark is selected
let toolDiam = new 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;
let slicerOpts = { flatoff: 0.001 };
let slicer = new cam_slicer(widget, slicerOpts);
let zFlats = Object.keys(slicer.zFlat).map(Number).map(z => [z, z - 0.002]).flat();
2025-06-12 23:11:34 -04:00
precision = Math.max(0, precision)
let intervals = (precision == 0) ? [] : slicer.interval(
precision,
{
fit: false, off: -0.01, flats: true
}
)
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)
)];
2025-04-24 11:30:38 -06:00
let centerDiff = diam * 0.1,
area = (diam / 2) * (diam / 2) * Math.PI,
circles = [],
slices = [];
2025-04-24 11:30:38 -06:00
function onEach(slice) {
slices.push(slice);
}
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);
let shadowedDrills = false;
for (let [i, slice] of slices.entries()) {
for (let top of slice.tops) {
// console.log("slicing",slice.z,top)
slice.shadow = await widget.shadowAt(slice.z);
let inner = top.inner;
if (!inner) { //no holes
2025-04-24 11:41:27 -06:00
continue;
}
for (let poly of inner) {
if (poly.points.length < 7) {
continue;
}
let center = poly.calcCircleCenter();
2025-05-01 10:10:01 -06:00
center.area = poly.area();
center.overlapping = [center];
center.depth = 0;
if (poly.circularity() < 0.98) {
// if not circular, don't add to holes
continue;
}
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
}
let overlap = false;
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);
// if overlapping, don't add and continue
overlap = true;
continue;
}
}
if (!overlap) circles.push(center);
2025-04-24 11:30:38 -06:00
}
}
onProgress(0.5 + (i / slices.length * 0.25), "recognize circles");
2025-04-24 11:30:38 -06:00
}
let drills = [];
for (let [i, c] of circles.entries()) {
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
onProgress(0.75 + (i / circles.length * 0.25), "assemble holes");
}
drills.forEach(h => {
if (rec.fromTop) {
2025-06-12 09:41:39 -06:00
// set z top if selected
h.depth += wztop - h.z;
h.z = wztop;
2025-06-12 09:41:39 -06: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-12-20 15:46:21 -07:00
// console.log("unfiltered circles",circles);
// console.log("drills",drills);
drills = drills.filter(drill => drill.depth > 0);
widget.shadowedDrills = shadowedDrills;
widget.drills = drills;
2025-04-24 11:30:38 -06:00
return drills;
}
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) {
let heal = POLY.reconnect([ ...lo, ...hi ], false);
POLY.setWinding(heal, poly.isClockwise());
out.push(...heal);
} else {
out.push(poly);
}
}
return out;
}
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, false);
const firstHit = intersects[0] || null;
if (firstHit) point.z = firstHit.point.z;
}
}
}