levelset: 2D contours and bounded isovalue ranges - #126
Merged
Conversation
Ranges first, since 2D shares them. isovalue is now a band: a scalar v means [-INF, v], a range means between, and [lo, INF] is BOSL2's "at or above" idiom -- so code forwarding BOSL2's own isovalue argument gets BOSL2's own semantics with no translation and no invert. One LevelSet pass either way; a bounded range could be difference(levelset(hi), levelset(lo)) but that meshes twice and then booleans. 2D is marching squares, because CrossSection has no contour extraction -- only construction from polygons. Saddle cases resolved by the cell-centre average, since picking wrong changes the topology rather than just the shape. Vertices keyed on edge identity, so loops close exactly instead of nearly. Dimension is decided by bounds, not guessed from the field. Two bugs found by measuring rather than by the tests passing: The 2D grid intake never ran. The array was parsed as 3D before bounds was read, so every 2D grid was rejected before reaching the 2D path. Parsing is now deferred until the dimension is known. Padding inflated anything touching the box edge. A half-plane came out 0.9 * spacing too large along every side it touched -- an O(h) error where a closed contour is O(h^2) -- because the contour landed out in the pad ring. Clipping to bounds afterwards makes it exact and resolution-independent: 800 to 1e-13 at every spacing tried. Verified against analytic areas: disc 1256.48 vs 1256.64, annulus 549.782 vs 549.779 with a real hole (2 contours), two blobs staying two, and an asymmetric field pinning i->x, j->y. Nineteen new tests, 1075 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.
Ranges
isovalueis now a band: a scalarvmeans[-INF, v](at or below — the distance-field reading), a range means between, and[lo, INF]is BOSL2's "at or above" idiom. Code forwarding BOSL2's own isovalue argument therefore gets BOSL2's own semantics, with no translation and noinvert.One
LevelSetpass either way. A bounded range could bedifference(levelset(hi), levelset(lo))— that works and gives the same answer — but it meshes twice and then booleans.2D
CrossSectionhas no contour extraction, only construction from polygons, so this is marching squares. Three details carry the correctness:Dimension comes from
bounds, not from guessing at the field.Two bugs measurement caught that passing tests would not have
The 2D grid intake never ran. The array was parsed as 3D before
boundswas read, so every 2D grid was rejected before the 2D path existed. Parsing is now deferred until the dimension is known.Padding inflated anything touching the box edge. A half-plane came out 0.9 × spacing too large along every side it touched — an O(h) error, where a closed contour is O(h²) — because the contour landed out in the pad ring:
I only found it because the disc converged beautifully while the half-plane didn't — the shape of the error, O(h) versus O(h²), is what pointed at the boundary rather than at the algorithm.
Verified against analytic values
Nineteen new tests. 1075 pass under both engines.
Version 0.50.1 → 0.51.0.
🤖 Generated with Claude Code