From 1e69db16f81562341375ea671aec7c7b424e6927 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 6 Dec 2025 14:23:56 -0500 Subject: [PATCH] move healPolys into polygons util class and rename as 'reconnect' --- src/geo/polygons.js | 89 ++++++++++++++++++++++++++++++++++++ src/kiri/mode/cam/op-area.js | 4 +- src/kiri/mode/cam/slice.js | 87 ++--------------------------------- 3 files changed, 94 insertions(+), 86 deletions(-) diff --git a/src/geo/polygons.js b/src/geo/polygons.js index 8606e40a..01aa5292 100644 --- a/src/geo/polygons.js +++ b/src/geo/polygons.js @@ -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; diff --git a/src/kiri/mode/cam/op-area.js b/src/kiri/mode/cam/op-area.js index bd7d7051..bb39ef39 100644 --- a/src/kiri/mode/cam/op-area.js +++ b/src/kiri/mode/cam/op-area.js @@ -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)); diff --git a/src/kiri/mode/cam/slice.js b/src/kiri/mode/cam/slice.js index d63faa3e..46958187 100644 --- a/src/kiri/mode/cam/slice.js +++ b/src/kiri/mode/cam/slice.js @@ -1,10 +1,9 @@ /** Copyright Stewart Allen -- 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; -} \ No newline at end of file