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,
points,
rayIntersect,
reconnect,
renest,
route,
setWinding,
@ -1112,4 +1113,92 @@ export function route(polys, start) {
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;

View file

@ -28,7 +28,7 @@ class OpArea extends CamOp {
let { tool, mode, down, over, follow, expand, outline, smooth } = op;
let { ov_topz, ov_botz, direction, rename } = op;
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 smoothVal = (smooth ?? 0) / 10;
@ -68,7 +68,7 @@ class OpArea extends CamOp {
// connect open poly edge segments into closed loops (when possible)
// 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
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 */
import { base, util } from '../../../geo/base.js';
import { util } from '../../../geo/base.js';
import { newLine } from '../../../geo/line.js';
import { newPoint } from '../../../geo/point.js';
import { newPolygon } from '../../../geo/polygon.js';
import { newSlopeFromAngle } from '../../../geo/slope.js';
import { polygons as POLY } from '../../../geo/polygons.js';
import { setSliceTracker } from '../../core/slice.js';
import { ops as OPS } from './ops.js';
@ -152,7 +151,6 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
cutPolys,
cutTabs,
dark,
healPolys,
isIndexed,
ops: opList,
setAxisIndex,
@ -730,7 +728,7 @@ function cutTabs(tabs, offset) {
hi.appendAll(POLY.setZ(poly.cut(tab.off, true), tab.top));
}
if (hi.length) {
let heal = healPolys([ ...lo, ...hi ], false);
let heal = POLY.reconnect([ ...lo, ...hi ], false);
POLY.setWinding(heal, poly.isClockwise());
out.push(...heal);
} else {
@ -743,7 +741,7 @@ function cutTabs(tabs, offset) {
function cutPolys(polys, offset) {
let noff = [];
offset.forEach(op => noff.appendAll( op.cut(POLY.union(polys, 0, true), true) ));
return healPolys(noff);
return POLY.reconnect(noff);
}
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;
}