Skip to content

fix: edge keys lose precision above 2.1M vertices, breaking T-junction repair and its check - #113

Open
Dgmtnz wants to merge 1 commit into
CNCKitchen:mainfrom
Dgmtnz:fix/edge-key-precision
Open

fix: edge keys lose precision above 2.1M vertices, breaking T-junction repair and its check#113
Dgmtnz wants to merge 1 commit into
CNCKitchen:mainfrom
Dgmtnz:fix/edge-key-precision

Conversation

@Dgmtnz

@Dgmtnz Dgmtnz commented Aug 7, 2026

Copy link
Copy Markdown

Where this is going: first of three, aimed at making large exports work.
This one is a standalone bug fix and stands on its own — but it also matters
for the goal, because the failure below only appears above ~2.1 M vertices,
which is exactly the range the other two open up.

What

meshRepair.js packs an edge as a * 4294967296 + b (a * 2^32 + b) into a JS number. float64 carries 53 bits of integer precision, so that is exact only up to a = 2^21 = 2,097,152 vertices. Above that, distinct edges collide onto one key.

Why it matters

In countEdgeDefects it is a false alarm on a good file. Colliding edges sum their incidence counts and trip the > 2 non-manifold test.

diag-edgekey-collision.mjs (added) builds a closed torus whose manifoldness follows from its grid topology rather than from any measurement, so a counter that disagrees is wrong by construction:

Vertices Reported non-manifold edges Actual
1,540,000 0 0
2,520,000 210,422 0
3,740,000 819,608 0

On a real export — 400 mm sphere, crystal texture, 0.35 mm / 5 M triangles — the app reported 185,146 non-manifold edges. Measuring edge defects after every pipeline stage (subdivide → regularize → displace → decimate → snap → repair) showed the mesh had zero at all of them. The file was fine; the check was wrong.

In resolveTJunctions it is a correctness bug, because that function repairs rather than measures:

  • a genuine boundary edge (count 1) that collides with another reads as count 2, so its T-junction is silently left unrepaired;
  • decoding the key back with b = k % 4294967296 returns vertex ids that were never on that edge, feeding wrong candidates to the on-segment search.

So above ~2.1 M vertices the repair pass has been quietly degrading.

Change

Both now key on exact Int32 pairs via a new IntPairMap in meshIndex.js, over a dense edge table. This also removes a second ceiling in countEdgeDefects: it counted in a JS Map, which throws past V8's ~16.7 M entry cap — reachable at ~11 M triangles.

Risk

Below 2.1 M vertices the old keys were exact, so this is a no-op there. Verified with bench-pipeline.mjs: 10/10 fingerprints unchanged, across sphere, cube-with-0.4 mm-fillets, cone (high-valence apex), holed plate (boundary loops, hard creases) and a sliver strip.

Caveats

  • Measured on Linux / Node 22 / Chromium 131. Not run on Windows, macOS, Safari or Firefox — though the failure is float64 semantics, which are engine-independent.
  • I could not run bench-decim-quality.mjs or bench-tri-estimate.mjs: they need 3DBenchy.stl, Barry Bear.stl etc., which aren't in the repo. The models above are generated (test-models/mkstl-adversarial.mjs, happy to include).

…d its check

meshRepair packs an edge as `a * 4294967296 + b` (a * 2^32 + b) into a JS
number. float64 holds that exactly only while it stays inside 2^53, i.e. up to
a = 2^21 = 2,097,152 vertices. Above that distinct edges collide onto one key.

diag-edgekey-collision.mjs (added) builds a closed torus whose manifoldness
follows from its grid topology rather than from any measurement, so a counter
that disagrees is wrong by construction:

  1.54M vertices ->       0 non-manifold edges   (below the threshold)
  2.52M vertices -> 210,422 non-manifold edges   (mesh is perfect)
  3.74M vertices -> 819,608 non-manifold edges   (mesh is perfect)

In countEdgeDefects the collision is a false alarm on a good file: a 400mm
sphere exported at 0.35mm / 5M triangles reported 185,146 non-manifold edges
that staged measurement showed were not in the mesh at any pipeline stage.

In resolveTJunctions it is a correctness bug. A genuine boundary edge (count 1)
colliding with another reads as count 2, so its T-junction is silently left
unrepaired, and decoding the key back with `b = k % 4294967296` yields vertex
ids that were never on that edge, feeding wrong candidates to the split search.

Both now key on exact Int32 pairs via a new IntPairMap in meshIndex.js, over a
dense edge table. This also removes a second ceiling in countEdgeDefects, which
counted in a JS Map and throws past V8's ~16.7M entry cap (~11M triangles).

Below 2.1M vertices the old keys were exact, so this is a no-op there: pipeline
fingerprints are unchanged across sphere, cube-with-fillets, cone, holed-plate
and sliver-strip inputs at 10 model/texture/resolution combinations.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants