grid-apps-cmms/src/kiri/slicer.js

811 lines
29 KiB
JavaScript
Raw Normal View History

2020-06-03 22:39:30 -04:00
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
2017-04-18 17:09:33 -04:00
"use strict";
(function() {
if (self.kiri.slicer) return;
2020-06-03 22:39:30 -04:00
self.kiri.slicer = {
2017-04-18 17:09:33 -04:00
slice: slice,
2020-03-22 20:05:04 -04:00
sliceWidget: sliceWidget,
connectLines: connectLines
2017-04-18 17:09:33 -04:00
};
2020-04-18 15:34:59 -04:00
let KIRI = self.kiri,
2017-04-18 17:09:33 -04:00
BASE = self.base,
CONF = BASE.config,
UTIL = BASE.util,
POLY = BASE.polygons,
time = UTIL.time,
newSlice = KIRI.newSlice,
newOrderedLine = BASE.newOrderedLine;
/**
* Convenience method. Gets a Widget's points and calls slice()
*
* @param {Widget} widget
* @param {Object} options
* @param {Function} ondone callback when slicing complete
* @param {Function} onupdate callback on incremental updates
*/
function sliceWidget(widget, options, ondone, onupdate) {
slice(widget.getPoints(), widget.getBoundingBox(), options, ondone, onupdate);
}
/**
* Given an array of points as triples, a bounding box and a set of
* slicing controls, emit an array of Slice objects to the ondone()
* function. onupdate() will be called with two parameters (% completion
* and an optional message) so that the UI can report progress to the user.
*
* @param {Array} points vertex array
* @param {Bounds} bounds bounding box for points
* @param {Object} options slicing parameters
* @param {Function} ondone callback when slicing done
* @param {Function} onupdate callback to report slicing progress
*/
function slice(points, bounds, options, ondone, onupdate) {
let useFlats = options.flats,
debug = options.debug,
xray = options.xray,
2017-04-18 17:09:33 -04:00
ox = 0,
oy = 0;
let zMin = options.zmin || options.firstHeight || Math.floor(bounds.min.z),
2017-04-18 17:09:33 -04:00
zMax = options.zmax || Math.ceil(bounds.max.z),
zInc = options.height,
2018-03-06 20:57:26 -05:00
zIncMin = options.minHeight,
zIncFirst = options.firstHeight || zInc,
2017-04-18 17:09:33 -04:00
zOff = true ? zInc / 2 : 0,
2018-03-06 20:57:26 -05:00
zHeights = [], // heights for zIndexes in adaptive mode
2018-03-05 14:34:15 -05:00
zIndexes = [], // auto-detected z slicing offsets (laser/cam)
zOrdered = [], // ordered list of Z indexes
zThick = [], // ordered list of Z slice thickness (laser)
zList = {}, // map count of z index points for adaptive slicing
zFlat = {}, // map area of z index flat areas (cam)
zLines = {}, // map count of z index lines
2018-03-05 14:34:15 -05:00
zScale, // bucket span in z units
2017-04-18 17:09:33 -04:00
timeStart = time(),
slices = [],
zSum = 0.0,
buckets = [],
i, j = 0, k, p1, p2, p3, px,
CPRO = KIRI.driver.CAM.process;
if (options.add) {
zMax += zInc;
}
2020-04-16 20:39:00 -04:00
2018-03-05 17:14:18 -05:00
function countZ(z) {
z = UTIL.round(z,5);
zList[z] = (zList[z] || 0) + 1;
}
2017-04-18 17:09:33 -04:00
// gather z-index stats
// these are used for auto-slicing in laser
// and to flats detection in CAM mode
for (i = 0; i < points.length;) {
p1 = points[i++];
p2 = points[i++];
p3 = points[i++];
zSum += (Math.abs(p1.z - p2.z) + Math.abs(p2.z - p3.z) + Math.abs(p3.z - p1.z));
// laser auto-detect z slice points
2018-03-06 20:57:26 -05:00
if (zInc === 0 || zIncMin) {
2018-03-05 17:14:18 -05:00
countZ(p1.z);
countZ(p2.z);
countZ(p3.z);
2017-04-18 17:09:33 -04:00
}
// use co-flat and co-line detection to adjust slice Z
if (p1.z === p2.z && p2.z === p3.z && p1.z > bounds.min.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 (true || options.trace) {
// detect zLines (curved region tops/bottoms)
// mark these layers for ball and v mill tracing
if (p1.z === p2.z && p1.z > bounds.min.z) {
let zkey = p1.z.toFixed(5);
let zval = zLines[zkey];
zLines[zkey] = (zval || 0) + 1;
}
if (p2.z === p3.z && p2.z > bounds.min.z) {
let zkey = p2.z.toFixed(5);
let zval = zLines[zkey];
zLines[zkey] = (zval || 0) + 1;
}
if (p3.z === p1.z && p3.z > bounds.min.z) {
let zkey = p3.z.toFixed(5);
let zval = zLines[zkey];
zLines[zkey] = (zval || 0) + 1;
2017-04-18 17:09:33 -04:00
}
}
}
/**
* bucket polygons into z-bounded groups (inside or crossing)
* to reduce the search space in complex models
*/
2020-04-18 15:34:59 -04:00
let bucketCount = Math.max(1, Math.ceil(zMax / (zSum / points.length)) - 1);
2017-04-18 17:09:33 -04:00
zScale = 1 / (zMax / bucketCount);
if (bucketCount > 1) {
// create empty buckets
for (i = 0; i < bucketCount + 1; i++) buckets.push([]);
// copy triples into all matching z-buckets
for (i = 0; i < points.length;) {
p1 = points[i++];
p2 = points[i++];
p3 = points[i++];
2020-04-18 15:34:59 -04:00
let zm = Math.min(p1.z, p2.z, p3.z),
2017-04-18 17:09:33 -04:00
zM = Math.max(p1.z, p2.z, p3.z),
bm = Math.floor(zm * zScale),
bM = Math.ceil(zM * zScale);
2021-01-24 10:17:32 -05:00
if (bm < 0) bm = 0;
2017-04-18 17:09:33 -04:00
for (j = bm; j < bM; j++) {
buckets[j].push(p1);
buckets[j].push(p2);
buckets[j].push(p3);
}
}
}
2018-03-06 20:57:26 -05:00
// we need Z ordered list for laser auto or adaptive fdm slicing
if (zInc === 0 || zIncMin) {
2020-04-18 15:34:59 -04:00
for (let key in zList) {
2017-04-18 17:09:33 -04:00
if (!zList.hasOwnProperty(key)) continue;
2018-03-06 20:57:26 -05:00
zOrdered.push(parseFloat(key));
2017-04-18 17:09:33 -04:00
}
2018-03-06 20:57:26 -05:00
zOrdered.sort(function(a,b) { return a - b});
}
if (options.indices) {
zIndexes = options.indices;
zHeights = zIndexes.map(v => options.height);
} else if (useFlats) {
zIndexes.appendAll(zOrdered);
} else if (options.single) {
2019-05-24 09:06:51 -04:00
// usually for laser single slice
zIndexes.push(zMin + zInc);
} else if (zInc === 0) {
// use Z indices in auto slice mode for laser
2018-03-06 20:57:26 -05:00
// find unique z-index offsets for slicing
2020-04-18 15:34:59 -04:00
let zl = zOrdered
// if zIncMin also present, then merge adjacent
// slices less than that value
if (zIncMin) {
let last = undefined;
zl = zl.filter(v => {
if (last !== undefined && v - last < zIncMin) {
return false;
}
last = v;
return true;
});
}
2018-03-06 20:57:26 -05:00
for (i = 0; i < zl.length - 1; i++) {
2017-04-18 17:09:33 -04:00
zIndexes.push((zl[i] + zl[i+1]) / 2);
zThick.push(zl[i+1] - zl[i]);
2017-04-18 17:09:33 -04:00
}
2018-03-06 20:57:26 -05:00
} else if (zIncMin) {
// console.log('adaptive slicing', zIncMin, ':', zInc, 'from', zMin, 'to', zMax);
// FDM adaptive slicing
2020-11-20 19:55:18 -05:00
let zPos = zIncFirst,
2018-03-06 20:57:26 -05:00
zOI = 0,
zDelta,
zDivMin,
zDivMax,
zStep,
2020-04-22 11:35:17 -04:00
nextZ,
lzp = zPos;
2018-03-06 20:57:26 -05:00
// first slice is fixed
zHeights.push(zIncFirst);
2018-03-06 20:57:26 -05:00
zIndexes.push(zIncFirst / 2);
2020-11-20 19:55:18 -05:00
// console.log({zIncFirst, zOrdered})
2018-03-06 20:57:26 -05:00
while (zPos < zMax && zOI < zOrdered.length) {
nextZ = zOrdered[zOI++];
2020-11-20 19:55:18 -05:00
if (zPos >= nextZ) {
// console.log('skip',{zPos},'>=',{nextZ});
continue;
}
2018-03-06 20:57:26 -05:00
zDelta = nextZ - zPos;
2020-11-20 19:55:18 -05:00
if (zDelta < zIncMin) {
// console.log('skip',{zDelta},'<',{zIncMin});
continue;
}
2018-03-06 20:57:26 -05:00
zDivMin = Math.floor(zDelta / zIncMin);
zDivMax = Math.floor(zDelta / zInc);
2020-04-22 11:35:17 -04:00
if (zDivMax && zDivMax <= zDivMin) {
if (zDelta % zInc > 0.01) zDivMax++;
zStep = zDelta / zDivMax;
2020-04-22 11:35:17 -04:00
// console.log(`--- zDivMax <= zDivMin ---`, zStep, zDelta % zInc)
2018-03-06 20:57:26 -05:00
} else {
zStep = zDelta;
}
2020-04-22 11:35:17 -04:00
// console.log({nextZ, zPos, zDelta, zStep, zDivMin, zDivMax})
2018-03-06 20:57:26 -05:00
while (zPos < nextZ) {
zHeights.push(zStep);
2018-03-06 20:57:26 -05:00
zIndexes.push(zPos + zStep / 2);
zPos += zStep;
2020-04-22 11:35:17 -04:00
// console.log({D: zPos - lzp, zPos})
// lzp = zPos;
2018-03-06 20:57:26 -05:00
}
}
2017-04-18 17:09:33 -04:00
} else {
// console.log('fixed slicing', zInc, 'from', zMin, 'to', zMax);
// FDM fixed slicing
2017-04-18 17:09:33 -04:00
if (options.firstHeight) {
zIndexes.push(options.firstHeight / 2);
zHeights.push(options.firstHeight);
2017-04-18 17:09:33 -04:00
zMin = options.firstHeight;
}
for (i = zMin + zOff; i < zMax; i += zInc) {
zIndexes.push(i);
zHeights.push(zInc);
2017-04-18 17:09:33 -04:00
}
}
2017-04-18 17:09:33 -04:00
// create a Slice for each z offset in the zIndexes array
2020-04-18 15:34:59 -04:00
for (let i = 0; i < zIndexes.length; i++) {
let ik = zIndexes[i].toFixed(5),
onFlat = false,
onLine = false;
// ensure no slice through horizontal lines or planes
2020-04-21 17:38:34 -04:00
if (zFlat[ik]) onFlat = true;
if (zLines[ik]) onLine = true;
if (!useFlats && (onFlat || onLine)) {
zIndexes[i] -= -0.001;
2020-04-18 15:34:59 -04:00
}
2018-03-08 09:28:03 -05:00
// slice next layer and add to slices[] array
let slice = sliceZ(zIndexes[i], zHeights[i], onFlat, onLine, zThick[i]);
onupdate(i / zIndexes.length);
2017-04-18 17:09:33 -04:00
}
// connect slices into linked list for island/bridge projections
for (i=1; i<slices.length; i++) {
slices[i-1].up = slices[i];
slices[i].down = slices[i-1];
}
slices.slice_time = time() - timeStart;
// pass Slices array back to ondone function
ondone(slices);
/** ***** SLICING FUNCTIONS ***** */
/**
* 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 checkUnderOverOn(p, z, where) {
2020-04-18 15:34:59 -04:00
let delta = p.z - z;
2017-04-18 17:09:33 -04:00
if (Math.abs(delta) < CONF.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) {
2020-04-18 15:34:59 -04:00
let ip = [];
for (let i = 0; i < over.length; i++) {
for (let j = 0; j < under.length; j++) {
2017-04-18 17:09:33 -04:00
ip.push(over[i].intersectZ(under[j], z));
}
}
return ip;
}
/**
* Ensure points are unique with a cache/key algorithm
*/
function getCachedPoint(phash, p) {
2020-04-18 15:34:59 -04:00
let cached = phash[p.key];
2017-04-18 17:09:33 -04:00
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);
p2 = getCachedPoint(phash, p2);
2020-04-18 15:34:59 -04:00
let line = newOrderedLine(p1,p2);
2017-04-18 17:09:33 -04:00
line.coplanar = coplanar || false;
line.edge = edge || false;
return line;
}
/**
* process a single z-slice on a single mesh and
* add to slices array
*
* @param {number} z
2018-03-06 20:57:26 -05:00
* @param {number} [height] optional real height (fdm)
2017-04-18 17:09:33 -04:00
*/
function sliceZ(z, height, onflat, online, thick) {
2020-04-18 15:34:59 -04:00
let phash = {},
2017-04-18 17:09:33 -04:00
lines = [],
slice = newSlice(z, options.view ? options.view.newGroup() : null),
bucket = bucketCount == 1 ? points : buckets[Math.floor(z * zScale)];
2020-04-21 17:38:34 -04:00
if (!bucket) {
return;
}
2017-04-18 17:09:33 -04:00
// iterate over matching buckets for this z offset
2020-04-18 15:34:59 -04:00
for (let i = 0; i < bucket.length;) {
2017-04-18 17:09:33 -04:00
p1 = bucket[i++];
p2 = bucket[i++];
p3 = bucket[i++];
2020-04-18 15:34:59 -04:00
let where = {under: [], over: [], on: []};
2017-04-18 17:09:33 -04:00
checkUnderOverOn(p1, z, where);
checkUnderOverOn(p2, z, where);
checkUnderOverOn(p3, z, where);
if (where.under.length === 3 || where.over.length === 3) {
// does not intersect (all 3 above or below)
2017-04-18 17:09:33 -04:00
} 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 (where.under.length === 1) {
lines.push(makeZLine(phash, where.on[0], where.on[1], false, true));
}
2017-04-18 17:09:33 -04:00
} 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
2017-04-18 17:09:33 -04:00
} else if (where.under.length === 0 || where.over.length === 0) {
// does not intersect but one point is on the slice Z plane
2017-04-18 17:09:33 -04:00
} else {
// compute two point intersections and construct line
2020-04-18 15:34:59 -04:00
let line = intersectPoints(where.over, where.under, z);
2017-04-18 17:09:33 -04:00
if (line.length < 2 && where.on.length === 1) {
line.push(where.on[0]);
}
if (line.length === 2) {
lines.push(makeZLine(phash, line[0], line[1]));
} else {
console.log({msg: "invalid ips", line: line, where: where});
}
}
}
// allow empty slices in CAM swap mode (for topos w/ gaps)
// if (lines.length == 0 && !(options.swapX || options.swapY)) return;
if (lines.length == 0 && options.noEmpty) return;
2017-04-18 17:09:33 -04:00
slice.height = height;
2017-04-18 17:09:33 -04:00
slice.index = slices.length;
slice.thick = thick;
2017-04-18 17:09:33 -04:00
// de-dup and group lines
lines = removeDuplicateLines(lines);
let groups = connectLines(lines, slice.z);
// simplistic healing of bad meshes
if (options.union) {
groups = POLY.flatten(POLY.union(POLY.nest(groups), null, true), null, true);
}
if ((debug && lines.excessive) || xray) {
const dash = xray || 3;
lines.forEach((line, i) => {
const group = i % dash;
const color = [ 0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 0xff00ff ][group];
slice.output().setLayer(`xl-${group}`, color).addLine(line.p1, line.p2);
});
2017-04-18 17:09:33 -04:00
}
// identify and add tops to slice
POLY.nest(groups).forEach((poly, i) => {
slice.addTop(poly);
if (xray) {
const group = i % (xray || 3);
const color = [ 0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 0xff00ff ][group];
slice.output().setLayer(`xg-${group}`, color).addPoly(poly);
2017-04-18 17:09:33 -04:00
}
});
2017-04-18 17:09:33 -04:00
slices.push(slice);
return slice;
2017-04-18 17:09:33 -04:00
}
}
/**
* Given an array of input lines (line soup), find the path through
* joining line ends that encompasses the greatest area without self
* interesection. Eliminate used points and repeat. Unjoined lines
* are permitted and handled after all other cases are handled.
*
* @param {Line[]} input
* @param {number} [index]
* @returns {Array}
*/
function connectLines(input, z) {
2017-04-18 17:09:33 -04:00
// map points to all other points they're connected to
2020-04-18 15:34:59 -04:00
let DBUG = BASE.debug,
2017-04-18 17:09:33 -04:00
CONF = BASE.config,
pmap = {},
points = [],
output = [],
connect = [],
search = 1,
nextMod = 1,
bridge = CONF.bridgeLineGapDistance,
p1, p2;
function cachedPoint(p) {
2020-04-18 15:34:59 -04:00
let cp = pmap[p.key];
2017-04-18 17:09:33 -04:00
if (cp) return cp;
points.push(p);
pmap[p.key] = p;
p.mod = nextMod++; // unique seq ID for points
p.toString = function() { return this.mod }; // point array concat
return p;
}
function addConnected(p1, p2) {
if (!p1.group) p1.group = [ p2 ];
else p1.group.push(p2);
}
function sliceAtTerm(path, term) {
2020-04-18 15:34:59 -04:00
let idx, len = path.length;
2017-04-18 17:09:33 -04:00
for (idx = 0; idx < len-1; idx++) {
if (path[idx] === term) {
return path.slice(idx);
}
}
return path;
}
/**
* using minimal recursion, follow points through connected lines
* to form candidate output paths.
*/
function findPathsMinRecurse(point, path, paths, from) {
2020-04-18 15:34:59 -04:00
let stack = [ ];
2017-04-18 17:09:33 -04:00
if (paths.length > 10000) {
DBUG.log(`indeterminate path @ ${z} from paths=${paths.length} input=${input.length}`);
input.excessive = paths.length;
2017-04-18 17:09:33 -04:00
return;
}
for (;;) {
stack.push(point);
2020-04-18 15:34:59 -04:00
let last = point,
2017-04-18 17:09:33 -04:00
links = point.group;
path.push(point);
// use del to mark traversed path
point.del = true;
// set so point isn't used in another polygon search
point.pos = search++;
// seed path with two points to prevent redundant opposing seeks
if (path.length === 1) {
from = point;
point = links[0];
continue;
}
if (links.length > 2) {
// TODO optimize when > 2 and limit to left-most and right-most branches
// for now, pursue all possible branches
links.forEach(function(nextp) {
// do not backtrack
if (nextp === from) {
return;
}
if (nextp.del) {
paths.push(sliceAtTerm(path,nextp));
} else {
findPathsMinRecurse(nextp, path.slice(), paths, point);
}
});
break;
} else {
point = links[0] === from ? links[1] : links[0];
from = last;
// hit an open end
if (!point) {
path.open = true;
paths.push(path);
break;
}
// hit a point previously in the path (or start)
if (point.del) {
paths.push(sliceAtTerm(path,point));
break;
}
}
}
// undo temp del/used marks
2020-04-18 15:34:59 -04:00
for (let i=0; i<stack.length; i++) stack[i].del = false;
2017-04-18 17:09:33 -04:00
}
// emit a polygon if it can be cleaned and still have 2 or more points
function emit(poly) {
poly = poly.clean();
if (poly.length > 2) output.push(poly);
2017-04-18 17:09:33 -04:00
}
// given an array of paths, emit longest to shortest
// eliminating points from the paths as they are emitted
// shorter paths any point eliminated are eliminated as candidates.
function emitLongestAsPolygon(paths) {
2020-04-18 15:34:59 -04:00
let longest = null,
2017-04-18 17:09:33 -04:00
emitted = 0,
closed = 0,
open = 0;
2020-03-22 20:05:04 -04:00
paths.forEach(function(path) {
2017-04-18 17:09:33 -04:00
// use longest perimeter vs longest path?
if (!longest || path.length > longest.length) longest = path;
if (!path.open) closed++; else open++;
});
// it gets more complicated with multiple possible output paths
if (closed > 1 && open === 0) {
// add polygon to path (for area sorting)
paths.forEach(function(path) { path.poly = BASE.newPolygon().addPoints(path) });
// sort descending by area VS (length below -- better in most cases)
// paths.sort(function(a,b) { return b.poly.area() - a.poly.area() });
// sort descending by length
paths.sort(function(a,b) { return b.poly.length - a.poly.length });
// emit polygons largest to smallest
// omit polygon if it intersects previously emitted (has del points)
paths.forEach(function(path) {
if (path.length < 3) return;
2020-04-18 15:34:59 -04:00
let len = path.length, i;
2017-04-18 17:09:33 -04:00
for (i = 0; i < len; i++) if (path[i].del) return;
for (i = 0; i < len; i++) path[i].del = true;
emit(path.poly);
emitted++;
});
} else {
if (longest.open) {
connect.push(longest);
} else {
emit(BASE.newPolygon().addPoints(longest));
}
}
}
// create point map, unique point list and point group arrays
input.forEach(function(line) {
p1 = cachedPoint(line.p1.round(4));
p2 = cachedPoint(line.p2.round(4));
2017-04-18 17:09:33 -04:00
addConnected(p1,p2);
addConnected(p2,p1);
});
// first trace paths starting at dangling endpoinds (bad polygon soup)
points.forEach(function(point) {
// must not have been used and be a dangling end
if (point.pos === 0 && point.group.length === 1) {
2020-04-18 15:34:59 -04:00
let path = [],
2017-04-18 17:09:33 -04:00
paths = [];
findPathsMinRecurse(point, path, paths);
if (paths.length > 0) emitLongestAsPolygon(paths);
}
});
// for each point, find longest path back to self
points.forEach(function(point) {
// must not have been used or be at a split
if (point.pos === 0 && point.group.length === 2) {
2020-04-18 15:34:59 -04:00
let path = [],
2017-04-18 17:09:33 -04:00
paths = [];
findPathsMinRecurse(point, path, paths);
if (paths.length > 0) emitLongestAsPolygon(paths);
}
});
// return true if points are deemed "close enough" close a polygon
function close(p1,p2) {
return p1.distToSq2D(p2) <= 0.01;
}
// reconnect dangling/open polygons to closest endpoint
2020-04-18 15:34:59 -04:00
for (let i=0; i<connect.length; i++) {
2017-04-18 17:09:33 -04:00
2020-04-18 15:34:59 -04:00
let array = connect[i],
2017-04-18 17:09:33 -04:00
last = array[array.length-1],
tmp, dist, j;
if (!bridge) {
emit(BASE.newPolygon().addPoints(array).setOpen());
continue;
}
if (array.delete) continue;
2020-04-18 15:34:59 -04:00
loop: for (let merged=0;;) {
let closest = { dist:Infinity };
2017-04-18 17:09:33 -04:00
for (j=i+1; j<connect.length; j++) {
tmp = connect[j];
if (tmp.delete) continue;
dist = last.distToSq2D(tmp[0]);
if (dist < closest.dist && dist <= bridge) {
closest = {
dist: dist,
array: tmp
}
}
dist = last.distToSq2D(tmp[tmp.length-1]);
if (dist < closest.dist && dist <= bridge) {
closest = {
dist: dist,
array: tmp,
reverse: true
}
}
}
if (tmp = closest.array) {
if (closest.reverse) tmp.reverse();
tmp.delete = true;
array.appendAll(tmp);
last = array[array.length-1];
merged++;
// tail meets head (closed)
if (close(array[0], last)) {
emit(BASE.newPolygon().addPoints(array));
break loop;
}
} else {
// no more closest polys (open set)
emit(BASE.newPolygon().addPoints(array));
break loop;
}
}
}
return output;
}
/**
* eliminate duplicate lines and interior-only lines (coplanar)
*
* lines are sorted using lexicographic point keys such that
* they are comparable even if their points are reversed. hinting
* for deletion, co-planar and suspect shared edge is detectable at
* this time.
*
* @param {Line[]} lines
* @returns {Line[]}
*/
function removeDuplicateLines(lines) {
2020-04-18 15:34:59 -04:00
let output = [],
2017-04-18 17:09:33 -04:00
tmplines = [],
points = [],
pmap = {};
function cachePoint(p) {
2020-04-18 15:34:59 -04:00
let cp = pmap[p.key];
2017-04-18 17:09:33 -04:00
if (cp) return cp;
points.push(p);
pmap[p.key] = p;
return p;
}
function addLinesToPoint(point, line) {
cachePoint(point);
if (!point.group) point.group = [ line ];
else point.group.push(line);
}
// mark duplicates for deletion preserving edges
lines.sort(function (l1, l2) {
if (l1.key === l2.key) {
l1.del = !l1.edge;
l2.del = !l2.edge;
return 0;
}
return l1.key < l2.key ? -1 : 1;
});
// associate points with their lines, cull deleted
lines.forEach(function(line) {
if (!line.del) {
tmplines.push(line);
addLinesToPoint(line.p1, line);
addLinesToPoint(line.p2, line);
}
});
// merge collinear lines
points.forEach(function(point) {
if (point.group.length != 2) return;
2020-04-18 15:34:59 -04:00
let l1 = point.group[0],
2017-04-18 17:09:33 -04:00
l2 = point.group[1];
if (l1.isCollinear(l2)) {
l1.del = true;
l2.del = true;
// find new endpoints that are not shared point
2020-04-18 15:34:59 -04:00
let p1 = l1.p1 != point ? l1.p1 : l1.p2,
2017-04-18 17:09:33 -04:00
p2 = l2.p1 != point ? l2.p1 : l2.p2,
2020-06-16 11:08:50 -04:00
newline = BASE.newOrderedLine(p1,p2);
2017-04-18 17:09:33 -04:00
// remove deleted lines from associated points
p1.group.remove(l1);
p1.group.remove(l2);
p2.group.remove(l1);
p2.group.remove(l2);
// associate new line with points
p1.group.push(newline);
p2.group.push(newline);
// add new line to lines array
newline.edge = l1.edge || l2.edge;
tmplines.push(newline);
}
});
// mark duplicates for deletion
// but preserve one if it's an edge
tmplines.sort(function (l1, l2) {
if (l1.key === l2.key) {
l1.del = true;
l2.del = !l2.edge;
return 0;
}
return l1.key < l2.key ? -1 : 1;
});
// create new line array culling deleted
tmplines.forEach(function(line) {
if (!line.del) {
output.push(line);
line.p1.group = null;
line.p2.group = null;
}
});
return output;
}
})();