partforge 0.60.0 → 0.60.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -824,6 +824,35 @@ function sourceBackedPositiveRegions(source, out, delta) {
|
|
|
824
824
|
return result;
|
|
825
825
|
}
|
|
826
826
|
|
|
827
|
+
// Crossing clustering can snap both ends of a tiny curve run onto the same pool vertex.
|
|
828
|
+
// The run then survives as a closed loop attached at one point (or as a whole one-segment
|
|
829
|
+
// contour). It encloses only geometry inside the resolver's own 2*CLUSTER_TOL cluster-
|
|
830
|
+
// diameter uncertainty, but a downstream cap triangulator may bridge that loop across the
|
|
831
|
+
// face and expose the bridge as a bogus feature edge. Positive dilation may discard these
|
|
832
|
+
// sub-resolution counter loops: they are collapsed negative boundaries, never new material.
|
|
833
|
+
// Keep erosion unchanged; its tiny surviving islands have no equivalent source-domain proof.
|
|
834
|
+
function dropSubresolutionPositiveLoops(out, delta) {
|
|
835
|
+
if (delta <= 0) return out;
|
|
836
|
+
const radius = 2 * CLUSTER_TOL;
|
|
837
|
+
const clean = (contour) => {
|
|
838
|
+
const segments = [];
|
|
839
|
+
let from = contour.start;
|
|
840
|
+
for (const seg of contour.segments) {
|
|
841
|
+
const controls = [seg.via, seg.c1, seg.c2].filter(Boolean);
|
|
842
|
+
const clusteredLoop = dist(from, seg.to) <= 1e-12
|
|
843
|
+
&& controls.every((p) => dist(from, p) <= radius);
|
|
844
|
+
if (!clusteredLoop) segments.push(seg);
|
|
845
|
+
from = seg.to;
|
|
846
|
+
}
|
|
847
|
+
return segments.length ? { start: contour.start, segments } : null;
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
return out.flatMap((rg) => {
|
|
851
|
+
const outer = clean(rg.outer);
|
|
852
|
+
return outer ? [{ outer, holes: rg.holes.map(clean).filter(Boolean) }] : [];
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
|
|
827
856
|
// Region-in / region-out offset: the engine behind Shape2D.offset on BOTH backends.
|
|
828
857
|
// Fast path: raw per-ring offsets that validate cleanly are returned as-is (lines/arcs
|
|
829
858
|
// exact). Cleanup path: anything dirty or invalid goes through resolveOffsetWinding
|
|
@@ -850,6 +879,7 @@ export function offsetRegions(regions, delta, { corners = "round" } = {}) {
|
|
|
850
879
|
if (out === null) throw err; // pinned message, unchanged, when nothing works
|
|
851
880
|
}
|
|
852
881
|
out = sourceBackedPositiveRegions(regions, out, delta);
|
|
882
|
+
out = dropSubresolutionPositiveLoops(out, delta);
|
|
853
883
|
if (out.length === 0) throw new Error("Shape2D.offset: offset collapses the shape (reduce |delta|)");
|
|
854
884
|
return out;
|
|
855
885
|
}
|
|
@@ -95,10 +95,14 @@ export function creasedNormals(g, { policies = null, featureLabels = null } = {}
|
|
|
95
95
|
if (prev === undefined) { seenEdge.set(key, t); continue; }
|
|
96
96
|
seenEdge.delete(key);
|
|
97
97
|
const dot = fn[prev * 3] * fn[t * 3] + fn[prev * 3 + 1] * fn[t * 3 + 1] + fn[prev * 3 + 2] * fn[t * 3 + 2];
|
|
98
|
-
//
|
|
99
|
-
|
|
98
|
+
// A multi-hole cap triangulation can contain an opposite-wound bridge:
|
|
99
|
+
// its two normals disagree by 180 degrees even though both triangles lie
|
|
100
|
+
// in the same plane. Gate on the unoriented supporting-plane angle first
|
|
101
|
+
// so that triangulation seam never becomes a feature line.
|
|
102
|
+
const bends = Math.abs(dot) < COPLANAR_COS;
|
|
103
|
+
const hard = bends && (triOID[prev] === triOID[t]
|
|
100
104
|
? polFor(triOID[t]).sameSurfaceLines && dot < cosFor(triOID[t])
|
|
101
|
-
:
|
|
105
|
+
: true);
|
|
102
106
|
if (hard) {
|
|
103
107
|
const ai = i * np, bj = j * np;
|
|
104
108
|
const dx = vp[ai] - vp[bj], dy = vp[ai + 1] - vp[bj + 1], dz = vp[ai + 2] - vp[bj + 2];
|