510 lines
17 KiB
JavaScript
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 });
|
|
});
|
|
};
|
|
});
|