checkpoint kiri module migration work
This commit is contained in:
parent
e341b98849
commit
7d22cc1029
201 changed files with 41262 additions and 85 deletions
510
src2/kiri-mode/cam/slicer.js
Normal file
510
src2/kiri-mode/cam/slicer.js
Normal file
|
|
@ -0,0 +1,510 @@
|
|||
/** 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 });
|
||||
});
|
||||
};
|
||||
});
|
||||
Loading…
Reference in a new issue