move healPolys into polygons util class and rename as 'reconnect'

This commit is contained in:
Stewart Allen 2025-12-06 14:23:56 -05:00
commit 1e69db16f8
3 changed files with 94 additions and 86 deletions

View file

@ -62,6 +62,7 @@ const POLYS = {
outer, outer,
points, points,
rayIntersect, rayIntersect,
reconnect,
renest, renest,
route, route,
setWinding, setWinding,
@ -1112,4 +1113,92 @@ export function route(polys, start) {
return routed.map(r => r.poly); return routed.map(r => r.poly);
} }
function dedup(arr, same) {
const out = [];
let prev;
for (const x of arr) {
if (!prev || !same(prev, x)) out.push(x);
prev = x;
}
return out;
}
/**
* Given an array of Polygons, attempt to reconnect open polygons
* into longer chains. If the chains close, emit a closed Polygon.
* Closed polygons are ignored and passed into the output.
*
* @param {Polygon[]} polys polygons
* @param {boolean} sameZ require open polygons to share endpoints with the same Z
* @returns array of connected or closed polygons
*/
export function reconnect(polys, sameZ = true) {
for (let p of polys) {
if (p.appearsClosed()) {
p.points.pop();
p.setClosed();
}
}
if (polys.length > 1) {
let heal = 0;
// heal/rejoin open segments that share endpoints
outer: for (; ; heal++) {
let ntmp = polys, tlen = ntmp.length;
for (let i = 0; i < tlen; i++) {
let s1 = ntmp[i];
if (!s1) continue;
for (let j = i + 1; j < tlen; j++) {
let s2 = ntmp[j];
if (!s2) continue;
// require polys at same Z to heal
if (sameZ && Math.abs(s1.getZ() - s2.getZ()) > 0.01) {
continue;
}
if (!(s1.open && s2.open)) continue;
if (s1.last().isMergable2D(s2.first())) {
s1.addPoints(s2.points);
ntmp[j] = null;
continue outer;
}
if (s2.last().isMergable2D(s1.first())) {
s2.addPoints(s1.points);
ntmp[i] = null;
continue outer;
}
if (s1.first().isMergable2D(s2.first())) {
s1.reverse();
s1.addPoints(s2.points);
ntmp[j] = null;
continue outer;
}
if (s1.last().isMergable2D(s2.last())) {
s2.reverse();
s1.addPoints(s2.points);
ntmp[j] = null;
continue outer;
}
}
}
break;
}
if (heal > 0) {
// cull nulls
polys = polys.filter(o => o);
}
// close poly if head meets tail
for (let poly of polys) {
poly.points = dedup(poly.points, (a,b) => a.isMergable3D(b));
if (poly.open) {
if (poly.first().isMergable3D(poly.last())) {
poly.points.pop();
poly.open = false;
} else if (poly.first().isMergable2D(poly.last())) {
poly.open = false;
}
}
}
}
return polys;
}
export const polygons = POLYS; export const polygons = POLYS;

View file

@ -28,7 +28,7 @@ class OpArea extends CamOp {
let { tool, mode, down, over, follow, expand, outline, smooth } = op; let { tool, mode, down, over, follow, expand, outline, smooth } = op;
let { ov_topz, ov_botz, direction, rename } = op; let { ov_topz, ov_botz, direction, rename } = op;
let { settings, widget, tabs, color } = state; let { settings, widget, tabs, color } = state;
let { addSlices, setToolDiam, cutTabs, healPolys, shadowAt, workarea } = state; let { addSlices, setToolDiam, cutTabs, shadowAt, workarea } = state;
let areaTool = new Tool(settings, tool); let areaTool = new Tool(settings, tool);
let smoothVal = (smooth ?? 0) / 10; let smoothVal = (smooth ?? 0) / 10;
@ -68,7 +68,7 @@ class OpArea extends CamOp {
// connect open poly edge segments into closed loops (when possible) // connect open poly edge segments into closed loops (when possible)
// surface and edge selections produce open polygons by default // surface and edge selections produce open polygons by default
polys = POLY.nest(healPolys(polys, false)); polys = POLY.nest(POLY.reconnect(polys, false));
// gather surface selections // gather surface selections
let vert = widget.getGeoVertices({ unroll: true, translate: true }).map(v => v.round(4)); let vert = widget.getGeoVertices({ unroll: true, translate: true }).map(v => v.round(4));

View file

@ -1,10 +1,9 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { base, util } from '../../../geo/base.js'; import { util } from '../../../geo/base.js';
import { newLine } from '../../../geo/line.js'; import { newLine } from '../../../geo/line.js';
import { newPoint } from '../../../geo/point.js'; import { newPoint } from '../../../geo/point.js';
import { newPolygon } from '../../../geo/polygon.js'; import { newPolygon } from '../../../geo/polygon.js';
import { newSlopeFromAngle } from '../../../geo/slope.js';
import { polygons as POLY } from '../../../geo/polygons.js'; import { polygons as POLY } from '../../../geo/polygons.js';
import { setSliceTracker } from '../../core/slice.js'; import { setSliceTracker } from '../../core/slice.js';
import { ops as OPS } from './ops.js'; import { ops as OPS } from './ops.js';
@ -152,7 +151,6 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
cutPolys, cutPolys,
cutTabs, cutTabs,
dark, dark,
healPolys,
isIndexed, isIndexed,
ops: opList, ops: opList,
setAxisIndex, setAxisIndex,
@ -730,7 +728,7 @@ function cutTabs(tabs, offset) {
hi.appendAll(POLY.setZ(poly.cut(tab.off, true), tab.top)); hi.appendAll(POLY.setZ(poly.cut(tab.off, true), tab.top));
} }
if (hi.length) { if (hi.length) {
let heal = healPolys([ ...lo, ...hi ], false); let heal = POLY.reconnect([ ...lo, ...hi ], false);
POLY.setWinding(heal, poly.isClockwise()); POLY.setWinding(heal, poly.isClockwise());
out.push(...heal); out.push(...heal);
} else { } else {
@ -743,7 +741,7 @@ function cutTabs(tabs, offset) {
function cutPolys(polys, offset) { function cutPolys(polys, offset) {
let noff = []; let noff = [];
offset.forEach(op => noff.appendAll( op.cut(POLY.union(polys, 0, true), true) )); offset.forEach(op => noff.appendAll( op.cut(POLY.union(polys, 0, true), true) ));
return healPolys(noff); return POLY.reconnect(noff);
} }
function contourPolys(widget, polys) { function contourPolys(widget, polys) {
@ -758,82 +756,3 @@ function contourPolys(widget, polys) {
} }
} }
} }
function dedup(arr, same) {
const out = [];
let prev;
for (const x of arr) {
if (!prev || !same(prev, x)) out.push(x);
prev = x;
}
return out;
}
function healPolys(noff, sameZ = true) {
for (let p of noff) {
if (p.appearsClosed()) {
p.points.pop();
p.setClosed();
}
}
if (noff.length > 1) {
let heal = 0;
// heal/rejoin open segments that share endpoints
outer: for (; ; heal++) {
let ntmp = noff, tlen = ntmp.length;
for (let i = 0; i < tlen; i++) {
let s1 = ntmp[i];
if (!s1) continue;
for (let j = i + 1; j < tlen; j++) {
let s2 = ntmp[j];
if (!s2) continue;
// require polys at same Z to heal
if (sameZ && Math.abs(s1.getZ() - s2.getZ()) > 0.01) {
continue;
}
if (!(s1.open && s2.open)) continue;
if (s1.last().isMergable2D(s2.first())) {
s1.addPoints(s2.points);
ntmp[j] = null;
continue outer;
}
if (s2.last().isMergable2D(s1.first())) {
s2.addPoints(s1.points);
ntmp[i] = null;
continue outer;
}
if (s1.first().isMergable2D(s2.first())) {
s1.reverse();
s1.addPoints(s2.points);
ntmp[j] = null;
continue outer;
}
if (s1.last().isMergable2D(s2.last())) {
s2.reverse();
s1.addPoints(s2.points);
ntmp[j] = null;
continue outer;
}
}
}
break;
}
if (heal > 0) {
// cull nulls
noff = noff.filter(o => o);
}
// close poly if head meets tail
for (let poly of noff) {
poly.points = dedup(poly.points, (a,b) => a.isMergable3D(b));
if (poly.open) {
if (poly.first().isMergable3D(poly.last())) {
poly.points.pop();
poly.open = false;
} else if (poly.first().isMergable2D(poly.last())) {
poly.open = false;
}
}
}
}
return noff;
}