Skip to content

levelset: 2D contours and bounded isovalue ranges - #126

Merged
revarbat merged 1 commit into
mainfrom
levelset-2d-ranges
Aug 27, 2026
Merged

levelset: 2D contours and bounded isovalue ranges#126
revarbat merged 1 commit into
mainfrom
levelset-2d-ranges

Conversation

@revarbat

Copy link
Copy Markdown
Member
// bounded range -> a shell
levelset(function(x,y,z) sqrt(x*x+y*y+z*z), bounds=[[-30,-30,-30],[30,30,30]],
         isovalue=[15,20], edge=1);

// 2D -- bounds with 2-vector corners gives a section
levelset(function(x,y) sqrt(x*x+y*y), bounds=[[-30,-30],[30,30]],
         isovalue=[15,20], edge=0.5);      // an annulus

Ranges

isovalue is now a band: a scalar v means [-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 no invert.

One LevelSet pass either way. A bounded range could be difference(levelset(hi), levelset(lo)) — that works and gives the same answer — but it meshes twice and then booleans.

2D

CrossSection has no contour extraction, only construction from polygons, so this is marching squares. Three details carry the correctness:

  • Saddles (cases 5/10) resolved by the cell-centre average. Picking wrong changes the topology — two touching blobs versus one pinched shape — not just the shape.
  • Vertices keyed on edge identity, never on float coordinates, so loops close exactly rather than nearly.
  • Padding with an outside ring so no contour runs off the box.

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 bounds was 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:

                before-clip      after-clip
edge=1          872.76           800.000000000
edge=0.5        836.21           800.000000000
edge=0.25       818.06           800.000000000     (analytic 800)

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

got analytic
disc 1256.48 1256.64
annulus 549.782 549.779 — and 2 contours, a real hole
two blobs 401.811 402.124 — and 2 contours
half-plane 800.000000000 800
asymmetric rect bbox x±5, y±15 pins i→x, j→y

Nineteen new tests. 1075 pass under both engines.

Version 0.50.1 → 0.51.0.

🤖 Generated with Claude Code

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.
@revarbat
revarbat merged commit 6a4fab3 into main Aug 27, 2026
3 checks passed
@revarbat
revarbat deleted the levelset-2d-ranges branch August 27, 2026 16:57
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.

1 participant