reorganize and rename driver files for consistency and clarity
This commit is contained in:
parent
7f50bafe9e
commit
67f405fd7b
74 changed files with 283 additions and 283 deletions
870
src/kiri/mode/cam/work/slice.js
Normal file
870
src/kiri/mode/cam/work/slice.js
Normal file
|
|
@ -0,0 +1,870 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
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';
|
||||
|
||||
// state shared across all widget slicing
|
||||
let shared;
|
||||
|
||||
/**
|
||||
* DRIVER SLICE CONTRACT
|
||||
*
|
||||
* global slice operation run before individual widget slice() operations
|
||||
*/
|
||||
export function cam_slice_pre(settings) {
|
||||
shared = { ops: { } };
|
||||
}
|
||||
|
||||
/**
|
||||
* DRIVER SLICE CONTRACT
|
||||
*
|
||||
* slice operations run per-widget between slicePre and sliceAll
|
||||
*
|
||||
* @param {Object} settings
|
||||
* @param {Widget} widget
|
||||
* @param {Function} output
|
||||
*/
|
||||
export async function cam_slice(settings, widget, onupdate, ondone) {
|
||||
if (widget.track.synth) return ondone();
|
||||
|
||||
// get tab widgets
|
||||
let tabW = widget.group.filter(w => w != widget);
|
||||
|
||||
// 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);
|
||||
|
||||
let proc = settings.process,
|
||||
camOps = widget.camops = [],
|
||||
sliceAll = widget.slices = [],
|
||||
isIndexed = proc.camStockIndexed,
|
||||
lastTime = Date.now();
|
||||
|
||||
let axisRotation, axisIndex,
|
||||
bounds, dark, color, stock, tabs, track, tool, unsafe, units, workarea,
|
||||
camZTop, camZBottom, minToolDiam, maxToolDiam,
|
||||
bottom_gap, bottom_part, bottom_stock, bottom_z,
|
||||
top_stock, top_part, top_gap, top_z,
|
||||
zBottom, zMin, zMax, zTop,
|
||||
ztOff, zbOff, wztop;
|
||||
|
||||
axisRotation = axisIndex = undefined;
|
||||
dark = settings.controller.dark;
|
||||
color = dark ? 0xbbbbbb : 0;
|
||||
minToolDiam = Infinity;
|
||||
maxToolDiam = -Infinity;
|
||||
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 } = 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;
|
||||
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(
|
||||
(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,
|
||||
bottom_stock, bottom_part, bottom_gap, bottom_z,
|
||||
}, 3);
|
||||
|
||||
// console.log({ bounds, stock, track, workarea });
|
||||
|
||||
return structuredClone(workarea);
|
||||
};
|
||||
|
||||
// initial setup
|
||||
var_compute();
|
||||
|
||||
function error(msg) {
|
||||
ondone(msg);
|
||||
}
|
||||
|
||||
function alert(msg) {
|
||||
onupdate(null,null,msg);
|
||||
}
|
||||
|
||||
function shadowAt(z) {
|
||||
return widget.shadowAt(z);
|
||||
}
|
||||
|
||||
if (unsafe) {
|
||||
console.log("disabling overhang safeties");
|
||||
}
|
||||
|
||||
if (!proc.ops || proc.ops.length === 0) {
|
||||
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");
|
||||
}
|
||||
|
||||
if (stock.x && stock.y && stock.z && !isIndexed) {
|
||||
let maxDelta = 1e-3;
|
||||
|
||||
if (stock.x + maxDelta < (bounds.max.x - bounds.min.x)) {
|
||||
alert('stock X too small for part');
|
||||
}
|
||||
|
||||
if (stock.y + maxDelta < (bounds.max.y - bounds.min.y)) {
|
||||
alert('stock Y too small for part');
|
||||
}
|
||||
|
||||
if (stock.z + maxDelta < (bounds.max.z - bounds.min.z)) {
|
||||
alert('stock Z too small for part');
|
||||
}
|
||||
}
|
||||
|
||||
if (zMin >= bounds.max.z) {
|
||||
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 = {
|
||||
addSlices,
|
||||
axisIndex,
|
||||
axisRotation,
|
||||
bounds,
|
||||
color,
|
||||
computeShadows,
|
||||
contourPolys,
|
||||
cutTabs,
|
||||
dark,
|
||||
isIndexed,
|
||||
newSlicer,
|
||||
ops: opList,
|
||||
setAxisIndex,
|
||||
setToolDiam,
|
||||
settings,
|
||||
shadowAt,
|
||||
slicer,
|
||||
stock,
|
||||
tabs,
|
||||
tool,
|
||||
unsafe,
|
||||
updateSlicer,
|
||||
updateToolDiams,
|
||||
widget,
|
||||
zBottom,
|
||||
zMax,
|
||||
ztOff,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function newSlicer(opts) {
|
||||
return new cam_slicer(widget, opts);
|
||||
}
|
||||
|
||||
function updateSlicer(opts) {
|
||||
return slicer = state.slicer = new cam_slicer(widget, opts);
|
||||
}
|
||||
|
||||
async function computeShadows(progress) {
|
||||
console.time('compute shadow');
|
||||
await new OPS.shadow(state, { type: "shadow", xsilent: true }).slice(progress);
|
||||
console.timeEnd('compute shadow');
|
||||
}
|
||||
|
||||
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) {
|
||||
state.axisIndex = axisIndex = absolute ? degrees : (axisIndex || 0) + degrees;
|
||||
state.axisRotation = axisRotation = (Math.PI / 180) * axisIndex;
|
||||
widget.setAxisIndex(isIndexed ? -axisIndex : 0);
|
||||
state.tabs = tabs = [];
|
||||
for (let tab of tabW) {
|
||||
tabs.push(await updateTab(tab));
|
||||
}
|
||||
return isIndexed ? -axisIndex : 0;
|
||||
}
|
||||
|
||||
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)) {
|
||||
addPolyIndexing(slice.camLines, -axisIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (false) {
|
||||
opList.push({ op: new OPS.xray(state, { type: "xray" }) });
|
||||
}
|
||||
|
||||
// find index ops
|
||||
let activeOps = proc.ops.filter(op => !op.disabled);
|
||||
|
||||
if (isIndexed) {
|
||||
// preface op list with OpIndex
|
||||
if (activeOps.length === 0 || activeOps[0].type !== 'index') {
|
||||
opList.push({ op: new OPS.index(state, { type: "index", degrees: 0, absolute: true }) });
|
||||
opTot += opList.peek().op.weight();
|
||||
|
||||
}
|
||||
} else {
|
||||
// preface op list with OpShadow
|
||||
opList.push({ op: new OPS.shadow(state, { type: "shadow", silent: true }) });
|
||||
opTot += opList.peek().op.weight();
|
||||
}
|
||||
|
||||
// LOOP EXPANSION - duplicate next N operations M times
|
||||
let expandedOps = [];
|
||||
|
||||
for (let i=0; i<activeOps.length; i++) {
|
||||
let op = activeOps[i];
|
||||
if (op.type === 'loop') {
|
||||
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`);
|
||||
}
|
||||
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);
|
||||
}
|
||||
} else if (op.type === '|') {
|
||||
break;
|
||||
} else {
|
||||
expandedOps.push(op);
|
||||
}
|
||||
}
|
||||
|
||||
activeOps = expandedOps;
|
||||
|
||||
// determine # of steps and step weighting for progress bar
|
||||
let opos = 0;
|
||||
for (let op of activeOps) {
|
||||
if (op.type === '|') {
|
||||
break;
|
||||
}
|
||||
let opfn = OPS[op.type];
|
||||
if (opfn) {
|
||||
let opin = new opfn(state, op);
|
||||
opTot += opin.weight();
|
||||
opos++;
|
||||
let share = shared.ops[opos];
|
||||
if (!share) {
|
||||
share = shared.ops[opos] = {};
|
||||
}
|
||||
opList.push({ op: opin, share });
|
||||
}
|
||||
}
|
||||
|
||||
// drop last op if it's type index
|
||||
if (opList.peek().op.op.type === 'index') {
|
||||
opList.pop();
|
||||
}
|
||||
|
||||
// call slice() function on all ops in order
|
||||
await setAxisIndex();
|
||||
updateSlicer();
|
||||
for (let { op, share } 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;
|
||||
}
|
||||
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,
|
||||
share,
|
||||
stock,
|
||||
tool,
|
||||
zBottom,
|
||||
ztOff,
|
||||
zMax,
|
||||
zTop,
|
||||
workarea: workover
|
||||
});
|
||||
let operr;
|
||||
await op.slice((progress, message) => {
|
||||
updateProgress((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 ---
|
||||
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);
|
||||
}
|
||||
|
||||
widget.traces = polylines;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export async function cylinders(settings, widget, opts) {
|
||||
let { } = opts ?? {};
|
||||
let { array: verts } = widget.mesh.geometry.attributes.position;
|
||||
let perpTriangles = [];
|
||||
|
||||
//iterate over all triangles
|
||||
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]];
|
||||
|
||||
//calculate normal
|
||||
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();
|
||||
|
||||
// if perpindicular normal, and at least 2 Zs are the same
|
||||
if (normal.z.round(5) == 0 && !(a[2] != b[2] && b[2] != c[2])) {
|
||||
let minZ = Math.min(a[2], b[2], c[2]),
|
||||
maxZ = Math.max(a[2], b[2], c[2]);
|
||||
if (minZ == maxZ) { // all Zs are the same indicated malformed geometry
|
||||
continue;
|
||||
}
|
||||
perpTriangles.push({ ...[a, b, c], normal, minZ, maxZ, i: index });
|
||||
}
|
||||
|
||||
}
|
||||
//map where zmax, zmin -> triangleData
|
||||
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, []);
|
||||
}
|
||||
cylinderTriangles.get(hash).push(t);
|
||||
}
|
||||
|
||||
return Array.from(cylinderTriangles.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export function cylinder_find (widget, face){
|
||||
CAM.surface_prep(widget,false);
|
||||
return widget.tool.findCylinderSurface(face);
|
||||
}
|
||||
|
||||
export function cylinder_poly_find(widget, face) {
|
||||
let faces = cylinder_find(widget, face);
|
||||
let vert = widget.getGeoVertices({ unroll: true, translate: true }).map(v => v.round(4));
|
||||
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));
|
||||
let zmin = Math.min(...zs);
|
||||
let zmax = Math.max(...zs);
|
||||
let zmid = (zmin + zmax) / 2;
|
||||
let cylVerts = new Float32Array(faces.length * 9);
|
||||
let opts = {
|
||||
dedup: true,
|
||||
edges: false,
|
||||
over: true
|
||||
};
|
||||
|
||||
let i = 0
|
||||
//for each cylinder face index
|
||||
for (let index of faces) {
|
||||
let off = index * 9;
|
||||
//copy the 9 triangle vertices floats in
|
||||
for (let j = 0; j < 9; j++) {
|
||||
cylVerts[i] = vert[off + j];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
//slice the poly at the midpoint
|
||||
let poly = slicer.sliceZ(zmid, cylVerts, opts)?.polys?.at(0);
|
||||
if (!poly) throw "slicing returned no poly";
|
||||
|
||||
let circular = poly.circularity() > 0.98;
|
||||
// console.log({ zmid, cylVerts, opts, poly, circularity: poly.circularity() });
|
||||
|
||||
//throw error if not circular
|
||||
if (!circular) throw "faces must be circular";
|
||||
|
||||
//calculate circle properties
|
||||
let area = poly.area(),
|
||||
diam = Math.sqrt(area / Math.PI) * 2,
|
||||
center = poly.calcCircleCenter();
|
||||
|
||||
//find direction using normal to center point
|
||||
let point = newPoint(x1, y1, z1),
|
||||
delta = point.setZ(center.z).sub(center),
|
||||
normal = new THREE.Vector3(x1 - x2, y1 - y2, z1 - z2)
|
||||
.cross(new THREE.Vector3(x2 - x3, y2 - y3, z2 - z3)),
|
||||
dotProd = normal.dot(delta),
|
||||
interior = dotProd < 0
|
||||
|
||||
return { faces, zmin, zmax, poly, circular, area, diam, center, interior }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
|
||||
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
|
||||
|
||||
let proc = settings.process,
|
||||
stock = settings.stock || {},
|
||||
isIndexed = proc.camStockIndexed,
|
||||
track = widget.track,
|
||||
{ camZBottom } = 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();
|
||||
|
||||
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
|
||||
.map(kv => parseFloat(kv).round(5))
|
||||
.filter(z => z !== null)
|
||||
)];
|
||||
let centerDiff = diam * 0.1,
|
||||
area = (diam / 2) * (diam / 2) * Math.PI,
|
||||
circles = [],
|
||||
slices = [];
|
||||
|
||||
function onEach(slice) {
|
||||
slices.push(slice);
|
||||
}
|
||||
let opts = { each: onEach, progress: (num, total) => onProgress(num / total * 0.5, "slice") };
|
||||
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
|
||||
continue;
|
||||
}
|
||||
for (let poly of inner) {
|
||||
if (poly.points.length < 7) {
|
||||
continue;
|
||||
}
|
||||
let center = poly.calcCircleCenter();
|
||||
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;
|
||||
continue;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
onProgress(0.5 + (i / slices.length * 0.25), "recognize circles");
|
||||
}
|
||||
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) {
|
||||
// set z top if selected
|
||||
h.depth += wztop - h.z;
|
||||
h.z = wztop;
|
||||
}
|
||||
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
|
||||
})
|
||||
|
||||
// console.log("unfiltered circles",circles);
|
||||
// console.log("drills",drills);
|
||||
|
||||
drills = drills.filter(drill => drill.depth > 0);
|
||||
widget.shadowedDrills = shadowedDrills;
|
||||
widget.drills = drills;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue