grid-apps-cmms/src2/kiri-mode/cam/slicer.js
2025-07-04 22:18:18 -04:00

510 lines
17 KiB
JavaScript

/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { base, util } from '../../geo/base.js';
import { broker } from '../../moto/broker.js';
import { decode, decodePointArray, encode, encodePointArray } from '../../kiri/codec.js';
import { newLine, newOrderedLine } from '../../geo/line.js';
import { newPoint } from '../../geo/point.js';
import { newSlice } from '../../kiri/slice.js';
import { polygons as POLY } from '../../geo/polygons.js';
import { slicer } from '../../geo/slicer.js';
const { sliceConnect, sliceDedup } = slicer;
const { config } = base;
const timing = false;
const begin = function() {
if (timing) console.time(...arguments);
};
const end = function() {
if (timing) console.timeEnd(...arguments);
};
export class Slicer {
constructor(widget, options = { zlist: true, zline: true, lines: false }) {
this.minions = self.kiri_worker.minions;
this.options = {};
this.setOptions(options);
if (widget) {
this.setPoints(widget.getGeoVertices({ unroll: true, translate: true }));
this.computeFeatures();
}
}
get threaded() {
return this.options.threaded;
}
// zList = generate list of z vertices
// zline = generate list of z vertices with coplanar lines
// trace = find z coplanar trace lines
// flatoff = amount to offset z when slicing on detected flats
// each = call for each slice generated from an interval
setOptions(options) {
Object.assign(this.options, options || {});
return this.options;
}
setPoints(points) {
this.bounds = null;
this.points = points;
this.zFlat = {}; // accumulated flat area at z height
this.zLine = {}; // count of z coplanar lines
this.zList = {}; // count of z values for auto slicing
return this;
}
// gather z-index stats
// these are used for auto-slicing in laser
// and to flats detection in CAM mode
computeFeatures(options) {
begin('compute features');
const opt = this.setOptions(options);
const bounds = this.bounds = new THREE.Box3();
const points = this.points;
const zFlat = this.zFlat;
const zLine = this.zLine;
const zList = this.zList;
function countZ(z) {
z = z.round(5);
zList[z] = (zList[z] || 0) + 1;
}
let p1 = newPoint(0,0,0),
p2 = newPoint(0,0,0),
p3 = newPoint(0,0,0);
// for (let i = 0, il = points.length; i < il; i++) {
// points[i] = points[i].round(5);
// }
for (let i = 0, il = points.length; i < il; ) {
p1.set(points[i++], points[i++], points[i++]);
p2.set(points[i++], points[i++], points[i++]);
p3.set(points[i++], points[i++], points[i++]);
// update bounds
bounds.expandByPoint(p1);
bounds.expandByPoint(p2);
bounds.expandByPoint(p3);
// count occurrences of z values for auto slicing
if (opt.zlist) {
countZ(p1.z);
countZ(p2.z);
countZ(p3.z);
}
// use co-flat and co-line detection to adjust slice Z
if (p1.z === p2.z && p2.z === p3.z) {
// detect zFlat faces to avoid slicing directly on them
let zkey = p1.z.toFixed(5),
area = Math.abs(util.area2(p1,p2,p3)) / 2;
if (!zFlat[zkey]) {
zFlat[zkey] = area;
} else {
zFlat[zkey] += area;
}
} else if (opt.zline) {
// detect zLine (curved region tops/bottoms)
// in cam used for ball and v mill tracing
if (p1.z === p2.z) {
let zkey = p1.z.toFixed(5);
let zval = zLine[zkey];
zLine[zkey] = (zval || 0) + 1;
}
if (p2.z === p3.z) {
let zkey = p2.z.toFixed(5);
let zval = zLine[zkey];
zLine[zkey] = (zval || 0) + 1;
}
if (p3.z === p1.z) {
let zkey = p3.z.toFixed(5);
let zval = zLine[zkey];
zLine[zkey] = (zval || 0) + 1;
}
}
}
end('compute features');
// console.log({ bounds, zFlat, zLine, zList });
return this;
}
/**
* Given an array of z-slice positions, generate a list of polygons
* representing the intersection of the mesh with the given z-slice
* positions.
*
* @param {number[]} zs - array of z-slice positions
* @param {Object} options - slicing parameters
* @param {number} [options.flatoff=0.01] - z offset when slicing on a flat surface
* @param {boolean} [options.progress=false] - callback with slice progress
* @param {function} [options.each] - callback with each slice result
* @returns {Promise<Object[]>} - list of slice objects with z and tops (polygons)
*/
async slice(zs, options) {
const opt = this.setOptions(options);
// slice through points at given Z and return polygons
if (!Array.isArray(zs)) {
throw "slice zs parameter must be ab array";
}
// sort Z and offset when on a flat
const flatoff = util.numOrDefault(opt.flatoff, 0.01);
zs = zs.sort((a,b) => a-b).map(z => {
if (!flatoff) {
return z;
}
const znorm = z.toFixed(5);
return this.zFlat[znorm] ? z + flatoff : z;
});
// compute buckets from z slice list
const { points } = this;
const plen = points.length;
const zlen = zs.length;
const ppz = plen / zlen;
const count = 25;//Math.ceil(ppz / 10000);
const step = Math.ceil(zlen / count);
const buckets = [];
// console.log({ points, zs, count, step });
for (let c = 0, b = 0; c < count; c++) {
if (b >= zlen) break;
buckets.push({
zs: zs.slice(b, b + step),
index: []
});
b += step;
}
let p1 = newPoint(0,0,0),
p2 = newPoint(0,0,0),
p3 = newPoint(0,0,0),
ep = 0.001;
begin("create buckets");
for (let i = 0, il = points.length; i < il; ) {
p1.set(points[i++], points[i++], points[i++]);
p2.set(points[i++], points[i++], points[i++]);
p3.set(points[i++], points[i++], points[i++]);
let zmin = Math.min(p1.z, p2.z, p3.z);
let zmax = Math.max(p1.z, p2.z, p3.z);
for (let bucket of buckets) {
const { zs, index } = bucket;
const min = Math.min(...zs);
const max = Math.max(...zs);
if (zmin < min && zmax < min) {
if (Math.abs(zmin - min) > ep && Math.abs(zmax - min) > ep) {
continue;
}
}
if (zmin > max && zmax > max) {
if (Math.abs(zmin - max) > ep && Math.abs(zmax - max) > ep) {
continue;
}
}
index.push(p1.x, p1.y, p1.z, p2.x, p2.y, p2.z, p3.x, p3.y, p3.z);
}
}
end("create buckets");
// console.log({ zs, zlen, count, step, buckets, flatoff });
begin("slicing");
const { minions } = this;
const threaded = minions && minions.running;
const sliceFn = (threaded ? this.sliceBucketMinion : this.sliceBucket).bind(this);
const track = { count: 0, total: zs.length };
if (threaded) minions.broadcast("cam_slice_init", {});
const promises = []
for (let bucket of buckets) {
promises.push(sliceFn(bucket, { ...opt, minZ: this.bounds.min.z }, slice => {
track.count++;
if (opt.progress) {
opt.progress(track.count, track.total);
}
// console.log({ oneach: slice, ...track });
}));
}
const data = (await Promise.all(promises)).flat();
data.sort((a,b) => b.z - a.z).forEach((rec, i) => {
rec.tops = rec.polys;
rec.slice = newSlice(rec.z).addTops(rec.tops);
if (opt.each) {
opt.each(rec, i, data.length);
}
});
if (threaded) minions.broadcast("cam_slice_cleanup");
end("slicing");
return data;
}
async sliceBucketMinion(bucket, opt, oneach) {
const slices = (await new Promise(resolve => {
this.minions.queue({
cmd: "cam_slice",
opt: Object.clone(opt),
bucket,
}, resolve);
})).slices;
for (let slice of slices) {
slice.polys = decode(slice.polys);
if (slice.lines) {
slice.lines = decodePointArray(slice.lines)
.group(2)
.map(arr => {
return newLine(arr[0], arr[1])
});
}
oneach(slice);
}
// console.log(bucket, slices);
return slices;
}
async sliceBucket(bucket, opt, oneach) {
const { zs, index } = bucket;
const slices = [];
for (let z of zs) {
const slice = this.sliceZ(z, index, opt);
if (slice) {
slices.push(slice);
}
oneach(slice);
}
return slices;
}
sliceZ(z, indices, opt) {
let links = opt.links !== false,
dedup = opt.dedup !== false,
edges = opt.edges || false,
over = opt.over || z <= opt.minZ,
debug = opt.debug,
phash = {},
lines = [];
// const { points } = this,
const points = indices,
p1 = newPoint(0,0,0),
p2 = newPoint(0,0,0),
p3 = newPoint(0,0,0);
for (let index = 0; index < points.length; ) {
p1.set(points[index++], points[index++], points[index++]);
p2.set(points[index++], points[index++], points[index++]);
p3.set(points[index++], points[index++], points[index++]);
let where = {under: [], over: [], on: []};
checkOverUnderOn(p1, z, where);
checkOverUnderOn(p2, z, where);
checkOverUnderOn(p3, z, where);
if (where.under.length === 3 || where.over.length === 3) {
// does not intersect (all 3 above or below)
} else if (where.on.length === 2) {
// one side of triangle is on the Z plane and 3rd is below
// drop lines with 3rd above because that leads to ambiguities
// with complex nested polygons on flat surface
if ((over && where.over.length === 1) || (!over && where.under.length === 1)) {
lines.push(makeZLine(phash, where.on[0], where.on[1], false, true));
}
} else if (where.on.length === 3) {
// triangle is coplanar with Z
// we drop these because this face is attached to 3 others
// that will satisfy the if above (line) with 2 points
} else if (where.under.length === 0 || where.over.length === 0) {
// does not intersect but one point is on the slice Z plane
} else if (!edges) {
// compute two point intersections and construct line
let line = intersectPoints(where.over, where.under, z);
if (line.length < 2 && where.on.length === 1) {
line.push(where.on[0].clone());
}
if (line.length === 2) {
lines.push(makeZLine(phash, line[0], line[1]));
} else {
console.log({msg: "invalid ips", line: line, where: where});
}
}
}
if (dedup) {
// console.log({ z, dedup: lines, points });
lines = sliceDedup(lines, debug);
}
return lines.length ? { z,
lines: opt.lines !== false ? lines : undefined,
polys: links ? POLY.nest(sliceConnect(lines, opt, debug)) : undefined
} : null;
}
interval(step, options) {
let opt = options || {},
bounds = this.bounds,
boff = opt.boff || opt.off || 0, // bottom offset
toff = opt.toff || opt.off || 0, // top offset
zmin = opt.min !== undefined ? opt.min : bounds.min.z + boff,
zmax = (opt.max || bounds.max.z) - toff,
steps = (zmax - zmin) / step,
rem = steps % 1 != 0 ? 0 : 1,
count = Math.floor(steps) + rem,
array = [];
if (opt.fit) {
count++;
step = (zmax - zmin) / count;
}
if (opt.down) {
for (let i=0; i<count; i++) {
array.push(zmax);
zmax -= step;
}
} else {
for (let i=0; i<count; i++) {
array.push(zmin);
zmin += step;
}
}
if (opt.fit) {
array.push(opt.down ? zmax : zmin);
}
if (opt.flats && opt.off) {
let add = [];
Object.keys(this.zFlat).forEach(z => {
z = parseFloat(z);
add.push(z + opt.off);
if (z > zmin) {
add.push(z - opt.off);
}
});
// add over and under all flats by 'off'
array.appendAll(add).sort((a,b) => {
return opt.down ? b-a : a-b;
});
}
// filter duplicate values
array = array.map(v => v.round(5)).filter((e,i,a) => i < 1 || a[i-1] !== a[i]);
// return array.map(v => Math.abs(parseFloat(v.toFixed(5))));
return array.map(v => parseFloat(v.toFixed(5)));
}
}
/**
* given a point, append to the correct
* 'where' objec tarray (on, over or under)
*
* @param {Point} p
* @param {number} z offset
* @param {Obejct} where
*/
function checkOverUnderOn(p, z, where) {
let delta = p.z - z;
if (Math.abs(delta) < config.precision_slice_z) { // on
where.on.push(p);
} else if (delta < 0) { // under
where.under.push(p);
} else { // over
where.over.push(p);
}
}
/**
* Given a point over and under a z offset, calculate
* and return the intersection point on that z plane
*
* @param {Point} over
* @param {Point} under
* @param {number} z offset
* @returns {Point} intersection point
*/
function intersectPoints(over, under, z) {
let ip = [];
for (let i = 0; i < over.length; i++) {
for (let j = 0; j < under.length; j++) {
ip.push(over[i].intersectZ(under[j], z));
}
}
return ip;
}
/**
* Ensure points are unique with a cache/key algorithm
*/
function getCachedPoint(phash, p) {
let cached = phash[p.key];
if (!cached) {
phash[p.key] = p;
return p;
}
return cached;
}
/**
* Given two points and hints about their edges,
* return a new Line object with points sorted
* lexicographically by key. This allows for future
* line de-duplication and joins.
*
* @param {Object} phash
* @param {Point} p1
* @param {Point} p2
* @param {boolean} [coplanar]
* @param {boolean} [edge]
* @returns {Line}
*/
function makeZLine(phash, p1, p2, coplanar, edge) {
p1 = getCachedPoint(phash, p1.clone());
p2 = getCachedPoint(phash, p2.clone());
let line = newOrderedLine(p1,p2);
line.coplanar = coplanar || false;
line.edge = edge || false;
return line;
}
Slicer.checkOverUnderOn = checkOverUnderOn;
Slicer.intersectPoints = intersectPoints;
broker.subscribe("minion.started", msg => {
const { funcs, cache, reply, log } = msg;
funcs.cam_slice_init = () => {
cache.slicer = new Slicer();
};
funcs.cam_slice_cleanup = () => {
delete cache.slicer;
};
funcs.cam_slice = (data, seq) => {
const { bucket, opt } = data;
// log({ slice: bucket, opt });
cache.slicer.sliceBucket(bucket, opt, slice => {
// console.log({ slice });
}).then(data => {
data.forEach(rec => {
rec.polys = encode(rec.polys);
if (rec.lines) {
const points = rec.lines.map(l => [l.p1, l.p2]).flat();
rec.lines = encodePointArray(points);
}
});
reply({ seq, slices: data });
});
};
});