levelset: cut cleanly where a 3D surface meets the box - #127
Merged
Conversation
Reported from a real model: a gyroid built with levelset() came out visibly staircased along every edge where the sheet meets the bounding box, next to a smooth one from BOSL2's isosurface() of the same field. Manifold closes the mesh where the surface reaches the edge of the box it is given, and that closure follows the sample lattice. Anything touching the boundary gets a voxel staircase; a gyroid touches it everywhere. Fixed the way the 2D path already was: sample a box padded by 2*edge, let the ragged closure happen out there, then intersect with a cube at the bounds actually asked for, so the cut is a plane. The grid sampler now reads "outside" beyond its data rather than clamping, which would smear the boundary values outward and extend the surface instead of closing it. The tests did not catch this because every shape in them sat inside its box. The numeric form of the bug is sharp once looked for: a half-space should be exactly half the box at any resolution, and now is -- 32000 to the digit at edge 2, 1 and 0.5. Worth recording: the 2D path got this exact fix first, and 3D was not checked at the time. A boundary bug found in one dimension is worth looking for in the other. Three regression tests. 1078 passing under both engines.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Your gyroid found a real bug. Same script, before and after:
Cause
Manifold closes the mesh where the surface reaches the edge of the box it is given, and that closure follows the sample lattice. Anything touching the boundary gets a voxel staircase — and a gyroid touches it everywhere, which is why it showed up so starkly next to BOSL2's
isosurface()of the same field.Fix
Exactly what the 2D path already did: sample a box padded by
2 * edge, let the ragged closure happen out there, then intersect with a cube at the bounds actually requested. The cut becomes a plane.The grid intake needed one more thing — its sampler now reads "outside" beyond the data rather than clamping, which would smear the boundary values outward and extend the surface instead of closing it.
Why the tests missed it
Every shape in them sat inside its box. The numeric form of the bug is sharp once you look for it — a half-space should be exactly half the box at any resolution:
That is the 3D twin of the 2D half-plane test which caught this same class of bug a few hours ago. The 2D path got this fix first and I did not check 3D — worth recording, and
CLAUDE.mdnow says so: a boundary bug found in one dimension is worth looking for in the other.A sphere sitting well inside its box is unaffected (33487.2, unchanged), confirming the padding disturbs nothing that does not reach the boundary.
Three regression tests: the half-space at three resolutions, an interior sphere, and the grid intake cut at the box. 1078 pass under both engines.
Version 0.51.0 → 0.51.1.
🤖 Generated with Claude Code