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:
Stewart Allen 2022-01-23 12:43:51 -05:00
commit 86fd0e78ce
5 changed files with 75 additions and 157 deletions

View file

@ -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,

View file

@ -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;

View file

@ -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) {

View file

@ -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);
}); });
} }

View file

@ -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++) {