danger. major redo of core fdm slicer line connection algorithm. seems to handle non-manifold parts better. seems faster on torture tests. too good to be true?
This commit is contained in:
parent
d40be9cdc0
commit
86fd0e78ce
5 changed files with 75 additions and 157 deletions
|
|
@ -479,7 +479,7 @@
|
||||||
// default # of decimal places in generated gcode
|
// default # of decimal places in generated gcode
|
||||||
gcode_decimals : 4,
|
gcode_decimals : 4,
|
||||||
// heal disjoint polygons in slicing (experimental)
|
// heal disjoint polygons in slicing (experimental)
|
||||||
bridgeLineGapDistance : 0.0001,
|
bridgeLineGapDistance : 0.5,
|
||||||
// Bounds default margin nearTo
|
// Bounds default margin nearTo
|
||||||
// Polygon.offset mindist2 offset precision
|
// Polygon.offset mindist2 offset precision
|
||||||
precision_offset : 0.05,
|
precision_offset : 0.05,
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
function Line(p1, p2, key) {
|
function Line(p1, p2, key) {
|
||||||
if (!key) key = [p1.key, p2.key].join('-');
|
if (!key) key = [p1.key, p2.key].join(';');
|
||||||
this.p1 = p1;
|
this.p1 = p1;
|
||||||
this.p2 = p2;
|
this.p2 = p2;
|
||||||
this.key = key;
|
this.key = key;
|
||||||
|
|
|
||||||
|
|
@ -272,7 +272,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function flatten(polys, to, crush) {
|
function flatten(polys, to, crush) {
|
||||||
if (!to) to = [];
|
to = to || [];
|
||||||
polys.forEach(function(poly) {
|
polys.forEach(function(poly) {
|
||||||
poly.flattenTo(to);
|
poly.flattenTo(to);
|
||||||
if (crush) poly.inner = null;
|
if (crush) poly.inner = null;
|
||||||
|
|
@ -399,7 +399,7 @@
|
||||||
for (j=i+1; j<out.length; j++) {
|
for (j=i+1; j<out.length; j++) {
|
||||||
if (!out[j]) continue;
|
if (!out[j]) continue;
|
||||||
union = out[i].union(out[j], minarea, all);
|
union = out[i].union(out[j], minarea, all);
|
||||||
if (union) {
|
if (union && union.length) {
|
||||||
out[i] = null;
|
out[i] = null;
|
||||||
out[j] = null;
|
out[j] = null;
|
||||||
if (all) {
|
if (all) {
|
||||||
|
|
|
||||||
|
|
@ -720,18 +720,18 @@
|
||||||
|
|
||||||
if (slice.xray) {
|
if (slice.xray) {
|
||||||
if (slice.lines) {
|
if (slice.lines) {
|
||||||
let dash = 3;
|
let dash = 5;
|
||||||
slice.lines.forEach((line, i) => {
|
slice.lines.forEach((line, i) => {
|
||||||
const group = i % dash;
|
const group = i % dash;
|
||||||
const color = [ 0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 0xff00ff ][group];
|
const color = [ 0xff0000, 0x00aa00, 0x0000ff, 0xaaaa00, 0xff00ff ][group];
|
||||||
slice.output().setLayer(`xl-${group}`, color).addLine(line.p1, line.p2);
|
slice.output().setLayer(`xl-${group}`, color).addLine(line.p1, line.p2);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (slice.groups)
|
if (slice.groups)
|
||||||
POLY.nest(slice.groups).forEach((poly, i) => {
|
POLY.nest(slice.groups).forEach((poly, i) => {
|
||||||
slice.addTop(poly);
|
slice.addTop(poly);
|
||||||
const group = i % 3;
|
const group = i % 5;
|
||||||
const color = [ 0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 0xff00ff ][group];
|
const color = [ 0xff0000, 0x00aa00, 0x0000ff, 0xaaaa00, 0xff00ff ][group];
|
||||||
slice.output().setLayer(`xg-${group}`, color).addPoly(poly);
|
slice.output().setLayer(`xg-${group}`, color).addPoly(poly);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -566,9 +566,6 @@
|
||||||
if (cp) return cp;
|
if (cp) return cp;
|
||||||
points.push(p);
|
points.push(p);
|
||||||
pmap[p.key] = p;
|
pmap[p.key] = p;
|
||||||
p.pos = 0;
|
|
||||||
p.mod = nextMod++; // unique seq ID for points
|
|
||||||
p.toString = function() { return this.mod }; // point array concat
|
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -577,201 +574,122 @@
|
||||||
else p1.group.push(p2);
|
else p1.group.push(p2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sliceAtTerm(path, term) {
|
function perimeter(array) {
|
||||||
let idx, len = path.length;
|
let p = BASE.newPolygon().addPoints(array);
|
||||||
for (idx = 0; idx < len-1; idx++) {
|
// console.log(p);
|
||||||
if (path[idx] === term) {
|
return p.perimeter();
|
||||||
return path.slice(idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* using minimal recursion, follow points through connected lines
|
* follow points through connected lines to form candidate output paths
|
||||||
* to form candidate output paths.
|
|
||||||
*/
|
*/
|
||||||
function findPathsMinRecurse(point, path, paths, from) {
|
function findNextPath(point, current, branches, depth = 1) {
|
||||||
let stack = [ ];
|
let path = [];
|
||||||
if (paths.length > 10000) {
|
if (current) {
|
||||||
console.log(`indeterminate path @ ${z} from paths=${paths.length} input=${input.length}`);
|
current.push(path);
|
||||||
input.excessive = paths.length;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
for (;;) {
|
for (;;) {
|
||||||
stack.push(point);
|
// prevent point re-use
|
||||||
|
|
||||||
let last = point,
|
|
||||||
links = point.group;
|
|
||||||
|
|
||||||
path.push(point);
|
|
||||||
// use del to mark traversed path
|
|
||||||
point.del = true;
|
point.del = true;
|
||||||
// set so point isn't used in another polygon search
|
// add point to path
|
||||||
point.pos = search++;
|
path.push(point);
|
||||||
// seed path with two points to prevent redundant opposing seeks
|
|
||||||
if (path.length === 1) {
|
let links = point.group.filter(p => !p.del);
|
||||||
from = point;
|
|
||||||
|
// no need to recurse at the start
|
||||||
|
if (links.length === 2) {
|
||||||
point = links[0];
|
point = links[0];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (links.length > 2) {
|
// if fork in the road, follow all paths to their end
|
||||||
// TODO optimize when > 2 and limit to left-most and right-most branches
|
// and find the longest path
|
||||||
// for now, pursue all possible branches
|
let root = !current, nc;
|
||||||
links.forEach(function(nextp) {
|
if (links.length > 1) {
|
||||||
// do not backtrack
|
// if (debug) console.log('fork!', {links: links.length, depth, root});
|
||||||
if (nextp === from) {
|
if (root) {
|
||||||
return;
|
current = [ path ];
|
||||||
}
|
branches = [ ];
|
||||||
if (nextp.del) {
|
}
|
||||||
paths.push(sliceAtTerm(path,nextp));
|
for (let p of links) {
|
||||||
} else {
|
branches.push(nc = current.slice());
|
||||||
findPathsMinRecurse(nextp, path.slice(), paths, point);
|
let rpath = findNextPath(p, nc, branches, depth + 1);
|
||||||
}
|
// allow point re-use in other path searches
|
||||||
|
for (let p of rpath) p.del = false;
|
||||||
|
}
|
||||||
|
let flat = branches.map(b => b.flat()).sort((a,b) => {
|
||||||
|
return perimeter(b) - perimeter(a);
|
||||||
|
return b.length - a.length;
|
||||||
});
|
});
|
||||||
break;
|
let npath = flat[0];
|
||||||
|
for (let p of npath) p.del = true;
|
||||||
|
// if (debug) console.log({root, branches, flat, path, npath});
|
||||||
|
return root ? npath : path;
|
||||||
} else {
|
} else {
|
||||||
point = links[0] === from ? links[1] : links[0];
|
path.open = point.group.length === 1;
|
||||||
from = last;
|
// choose next (unused) point
|
||||||
// hit an open end
|
point = links[0];
|
||||||
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
|
// hit an open end or branch
|
||||||
for (let i=0; i<stack.length; i++) {
|
if (!point || point.del) {
|
||||||
stack[i].del = false;
|
// console.log({open: path.open, path: path.slice()});
|
||||||
// stack[i].pos = 0;
|
return path;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// emit a polygon if it can be cleaned and still have 2 or more points
|
// emit a polygon if it can be cleaned and still have 2 or more points
|
||||||
function emit(poly) {
|
function emit(poly) {
|
||||||
poly = poly.clean();
|
poly = poly.clean();
|
||||||
if (debug) console.log({emit: poly});
|
if (poly.length > 2) output.push(poly);
|
||||||
if (poly.length > 2) {
|
if (debug) console.log('xray',poly.xray());
|
||||||
output.push(poly);
|
|
||||||
} else if (debug) {
|
|
||||||
console.log({clean_to_zero: poly});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// given an array of paths, emit longest to shortest
|
// given an array of paths, emit longest to shortest
|
||||||
// eliminating points from the paths as they are emitted
|
// eliminating points from the paths as they are emitted
|
||||||
// shorter paths any point eliminated are eliminated as candidates.
|
// shorter paths any point eliminated are eliminated as candidates.
|
||||||
function emitLongestAsPolygon(paths) {
|
function emitPath(path) {
|
||||||
let longest = null,
|
if (path.open) {
|
||||||
emitted = 0,
|
// if (debug) console.log('emit OPEN', path.length);
|
||||||
closed = 0,
|
connect.push(path);
|
||||||
open = 0;
|
|
||||||
|
|
||||||
paths.forEach(function(path) {
|
|
||||||
// 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;
|
|
||||||
let len = path.length, i;
|
|
||||||
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 {
|
} else {
|
||||||
if (longest.open) {
|
// if (debug) console.log('emit CLOSED', path.length);
|
||||||
connect.push(longest);
|
emit(BASE.newPolygon().addPoints(path), debug);
|
||||||
} else {
|
|
||||||
// mark points so they don't get re-used
|
|
||||||
for (let p of longest) p.del = true;
|
|
||||||
emit(BASE.newPolygon().addPoints(longest), debug);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// create point map, unique point list and point group arrays
|
// create point map, unique point list and point group arrays
|
||||||
input.forEach(function(line) {
|
input.forEach(function(line) {
|
||||||
p1 = cachedPoint(line.p1.round(4));
|
p1 = cachedPoint(line.p1.round(3));
|
||||||
p2 = cachedPoint(line.p2.round(4));
|
p2 = cachedPoint(line.p2.round(3));
|
||||||
addConnected(p1,p2);
|
addConnected(p1,p2);
|
||||||
addConnected(p2,p1);
|
addConnected(p2,p1);
|
||||||
});
|
});
|
||||||
|
|
||||||
// order points leftmost to right-most
|
// console.log({points, forks: points.filter(p => p.group.length !== 2)});
|
||||||
// points = points.sort((a,b) => {
|
|
||||||
// return a.x - b.x;
|
|
||||||
// });
|
|
||||||
|
|
||||||
// first trace paths starting at dangling endpoinds (bad polygon soup)
|
// first trace paths starting at undeleted points with connections
|
||||||
for (let point of points) {
|
for (let point of points) {
|
||||||
// must not have been used and be a dangling end
|
// must not have been used and be a dangling end
|
||||||
if (point.pos === 0 && point.group.length === 1) {
|
if (!point.del && point.group.length) {
|
||||||
let path = [];
|
let path = findNextPath(point);
|
||||||
let paths = [];
|
if (path) emitPath(path);
|
||||||
findPathsMinRecurse(point, path, paths);
|
|
||||||
if (paths.length > 0) emitLongestAsPolygon(paths);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i=0; i<2; i++) {
|
if (debug) console.log({
|
||||||
// for each point, find longest path back to self
|
used: points.filter(p => p.del),
|
||||||
for (let point of points) {
|
free: points.filter(p => !p.del)
|
||||||
// must not have been used or be at a split
|
});
|
||||||
if (point.pos === 0 && point.group.length === 2) {
|
|
||||||
let path = [];
|
|
||||||
let paths = [];
|
|
||||||
findPathsMinRecurse(point, path, paths);
|
|
||||||
if (paths.length > 0) {
|
|
||||||
emitLongestAsPolygon(paths, i>0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (debug) console.log({
|
|
||||||
used: points.filter(p => p.del),
|
|
||||||
free: points.filter(p => !p.del)
|
|
||||||
});
|
|
||||||
|
|
||||||
// prepare points for re-use
|
|
||||||
points = points.filter(p => !p.del);
|
|
||||||
for (let p of points) {
|
|
||||||
p.pos = 0;
|
|
||||||
}
|
|
||||||
if (points.length < 2) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (debug) console.log({output});
|
|
||||||
|
|
||||||
// return true if points are deemed "close enough" close a polygon
|
// return true if points are deemed "close enough" close a polygon
|
||||||
function close(p1,p2) {
|
function close(p1,p2) {
|
||||||
return p1.distToSq2D(p2) <= 0.01;
|
return p1.distToSq2D(p2) <= 0.01;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (debug && connect.length) console.log({connect});
|
||||||
|
|
||||||
// reconnect dangling/open polygons to closest endpoint
|
// reconnect dangling/open polygons to closest endpoint
|
||||||
for (let i=0; i<connect.length; i++) {
|
for (let i=0; i<connect.length; i++) {
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue