git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commitce42b4fca30ae97420a0e95975444473383987bd
parent675de1b832
authorLucas Galante <[email protected]>
date2026-09-14 09:59
fix(relief): mitre the union carve's outer corners like the lattice's

Each box's wall band now runs between the box shrunk and grown by half
the run at the box's own radius, and the union takes the box the pixel
is deepest in — so an outer corner keeps its radius instead of sweeping
at radius + run/2, and interior walls still vanish. Height field
mirrored; the test pins a hair of wall inside the mitred corner arc
where an offset contour was flat.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

 src/scene/heightfield.rs | 28 +++++++++++++++++++++-------
 src/scene/paint.rs       |  3 +++
 src/vk/shader2d.wgsl     | 31 +++++++++++++++++++++----------
 3 files changed, 45 insertions(+), 17 deletions(-)

diff --git a/src/scene/heightfield.rs b/src/scene/heightfield.rs
index 4242ae1..46faa50 100644
--- a/src/scene/heightfield.rs
+++ b/src/scene/heightfield.rs
@@ -361,17 +361,25 @@ impl HeightField {
                             -carve_drop * prof.carve_height(1.0 - frac)
                         }
                         MODE_UNION => {
-                            // Nearest box of the run — the union SDF — through
-                            // one profile (see the shader's MODE_UNION).
-                            let mut best = f32::MAX;
+                            // Mitred per box (band between the box shrunk and
+                            // grown by t/2 at its own radius), union = the box
+                            // the point is deepest in (see the shader's
+                            // MODE_UNION). One profile.
+                            let hw = 0.5 * t;
+                            let mut best = f32::MIN;
                             for feat in features.iter().skip(f_off).take(f_cnt) {
-                                let fd = rr_sdf(pt, [feat[0], feat[1], feat[2], feat[3]], [feat[4], feat[5], feat[6], feat[7]], shape, t);
-                                best = best.min(fd);
+                                let radii = [feat[4], feat[5], feat[6], feat[7]];
+                                let inner = [feat[0], feat[1], (feat[2] - hw).max(0.5), (feat[3] - hw).max(0.5)];
+                                let outer = [feat[0], feat[1], feat[2] + hw, feat[3] + hw];
+                                let d_in = rr_sdf(pt, inner, radii, shape, t);
+                                let d_out = rr_sdf(pt, outer, radii, shape, t);
+                                let frac = (d_in / (d_in - d_out).max(1e-3)).clamp(0.0, 1.0);
+                                best = best.max((0.5 - frac) * t);
                             }
-                            if best == f32::MAX {
+                            if best == f32::MIN {
                                 continue;
                             }
-                            let u = (-best / t + 0.5).clamp(0.0, 1.0);
+                            let u = (best / t + 0.5).clamp(0.0, 1.0);
                             let sign = if p.radii[0] > 0.5 { 1.0 } else { -1.0 };
                             sign * carve_drop * prof.carve_height(u)
                         }
@@ -627,6 +635,12 @@ mod tests {
         // outside the top bar's lower edge (y = 30), on the wall.
         let wall = at(55, 32);
         assert!(wall < 0.0 && wall > -drop, "wall {wall}");
+        // Mitred outer corner: the band's outer contour is the bar grown by
+        // t/2 at the bar's own radius 4, so (72.5, 8.5) — 1.1 px inside that
+        // contour's corner arc — carries a hair of wall, where an offset
+        // contour (radius 8, the point 4.5 px from the bar) would be flat.
+        let corner = at(72, 8);
+        assert!(corner < 0.0 && corner > -0.5 * drop, "mitred corner {corner}");
     }
 
     #[test]
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index 58ea8d9..e724202 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -617,6 +617,9 @@ pub enum Prim {
     /// vanishes wherever it runs inside another, and an inside corner is a
     /// sharp mitre (round it with [`Prim::ConcaveFillet`] if it must be
     /// concave-rounded — the union has no radius there by construction).
+    /// Outer corners are mitred like [`Prim::Lattice`]'s: the wall band runs
+    /// between each box shrunk and grown by half the run at the box's own
+    /// radius, so a corner keeps its radius instead of sweeping wider.
     ///
     /// The boxes ride the frame's plate-feature buffer (the same slots CSG
     /// carves use, 64 per frame), so a union costs one draw plus one slot
diff --git a/src/vk/shader2d.wgsl b/src/vk/shader2d.wgsl
index 35c4225..5acedff 100644
--- a/src/vk/shader2d.wgsl
+++ b/src/vk/shader2d.wgsl
@@ -711,23 +711,34 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
         eff = MODE_RECESS;
     } else if (mode == MODE_UNION) {
         // MODE_UNION: the boxes in the feature run p_host.xy = [offset,
-        // count] are one shape — the pixel's distance is to the NEAREST box
-        // (the union SDF, min over the run), with that box's gradient, so a
-        // box's wall vanishes inside another and the outline is evaluated
-        // once. p_radii.x = 1 raises the union (boss) instead of carving it.
+        // count] are one shape. Each box's wall is MITRED like the lattice's:
+        // the band runs between the box shrunk by t/2 and the box grown by
+        // t/2, both at the box's own corner radius, and the pixel's position
+        // is its fraction across that band (an offset band would round the
+        // outer corners at radius + t/2). The union takes the box the pixel
+        // is deepest in — max over the run of the band coordinate, with that
+        // box's gradient — so a box's wall vanishes inside another and the
+        // outline is evaluated once. p_radii.x = 1 raises the union (boss)
+        // instead of carving it.
         let u_off = u32(rrect_clip.p_host.x);
         let u_cnt = u32(rrect_clip.p_host.y);
-        var best = 1e9;
+        let hw = 0.5 * t;
+        var best = -1e9;
         var bgrad = vec2f(0.0, -1.0);
         for (var i = 0u; i < u_cnt; i = i + 1u) {
             let feat = plate_features.items[u_off + i];
-            let fg = rr_sdf_grad(frag, feat.rect, feat.radii);
-            if (fg.z < best) {
-                best = fg.z;
-                bgrad = fg.xy;
+            let inner = vec4f(feat.rect.xy, max(feat.rect.zw - vec2f(hw), vec2f(0.5)));
+            let outer = vec4f(feat.rect.xy, feat.rect.zw + vec2f(hw));
+            let gi = rr_sdf_grad(frag, inner, feat.radii);
+            let go = rr_sdf_grad(frag, outer, feat.radii);
+            let band = max(gi.z - go.z, 1e-3);
+            let fdi = (0.5 - clamp(gi.z / band, 0.0, 1.0)) * t;
+            if (fdi > best) {
+                best = fdi;
+                bgrad = gi.xy;
             }
         }
-        fd = -best;
+        fd = best;
         fgd = bgrad;
         eff = select(MODE_RECESS, MODE_BOSS, rrect_clip.p_radii.x > 0.5);
     } else if (mode == MODE_FILLET_DOWN || mode == MODE_FILLET_UP) {