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

commit879ef7ccf0594bbaf0efa81db31121eb8edc73d6
parent321a0b834b
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-06 21:03
feat(paint): Prim::Frame, a plate turned inside out

A Bevel whose face is everything in its rect OUTSIDE a hole, its roll
running round the hole and falling into it. A corner of the hole is an
inside corner of the plate, a cove, which no box prim can draw and which a
ConcaveFillet beside a Bevel edge draws in the carve-wall profile rather
than the plate's roll. In the shader it is the plate branch with the hole's
SDF negated (MODE_FRAME = 17), so roll, frost, focus tint and grouped carves
are the plate's own; it is a carve host over its rect like a Bevel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 CLAUDE.md                 | 22 +++++++++++++++++
 src/backend/tessellate.rs | 61 ++++++++++++++++++++++++++++++++++++++++++++++-
 src/draw/mod.rs           |  2 +-
 src/draw/shader2d.wgsl    | 11 +++++++--
 src/scene/paint.rs        | 22 +++++++++++++++++
 src/scene/relief_shade.rs |  2 +-
 6 files changed, 115 insertions(+), 5 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 74b7d2d..3153985 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -929,6 +929,28 @@ the process can reach an X display (`DISPLAY=:0` and `XAUTHORITY=$HOME/.Xauthori
 fell back to the Intel device SILENTLY. It says so on stderr now (below, "Debug
 environment variables"); `grep -c nvidia /proc/<pid>/maps` is the check from outside.
 
+### A plate can be turned inside out: `Prim::Frame` (since 2026-10-06)
+
+`PaintCtx::frame(rect, hole, hole_radii, material, depth)` is a Bevel whose face is
+everything in `rect` OUTSIDE `hole`, its rolled edge running round the hole and falling
+INTO it. A corner of the hole is therefore an inside corner of the plate — a **cove**,
+rounded at the hole's radius in the DE's corner family — which no box prim can draw: box
+radii round only convex corners, and `ConcaveFillet` shades with the carve WALL profile,
+not a plate's roll, so a fillet beside a Bevel edge changes profile at the join.
+
+- **The shader is the plate branch, unchanged** (`MODE_FRAME` = 17): the hole's SDF is
+  negated (`gd = -gd0`), so the depth into the face is the distance outside the box and
+  the outward gradient points into the hole, and everything after is `MODE_PLATE`'s —
+  roll, crest, frost, focus tint and CSG carves.
+- **It is a carve host over `rect`**, like a Bevel: carves inside it group into its draw
+  (the feature offset is rebased for mode 17 as for 1 and 14). Its own outer edges are
+  NOT rolled — lay them past the window or under something.
+- **The legacy banded path fills only below the hole**, square: it has no inside-out SDF.
+
+The first consumer is the designer's playbar, a shelf of the window's bottom edge whose
+top meets each side lip in a cove. `a_frame_is_an_inside_out_plate_that_hosts_its_carves`
+is the test.
+
 ### A row can lead to a page, and a side swipe turns it (since 2026-10-02)
 
 `context_menu::set_row_page(idx)`, called after `show` like `set_row_slider`, makes a
diff --git a/src/backend/tessellate.rs b/src/backend/tessellate.rs
index f3ddac5..be38fa1 100644
--- a/src/backend/tessellate.rs
+++ b/src/backend/tessellate.rs
@@ -1327,7 +1327,7 @@ fn prim_kind(p: &crate::scene::paint::Prim) -> &'static str {
         P::Arc { .. } => "Arc", P::ArcShaded { .. } => "ArcShaded",
         P::Vector { .. } => "Vector", P::Circle { .. } => "Circle",
         P::Sphere { .. } => "Sphere", P::Droplet { .. } => "Droplet",
-        P::DropletScrim { .. } => "DropletScrim",
+        P::DropletScrim { .. } => "DropletScrim", P::Frame { .. } => "Frame",
         P::ConcaveFillet { .. } => "ConcaveFillet",
         P::Groove { .. } => "Groove", P::Lattice { .. } => "Lattice", P::Grout { .. } => "Grout", P::Fill { .. } => "Fill",
         P::CarveUnion { .. } => "CarveUnion", P::Glow { .. } => "Glow",
@@ -1483,6 +1483,7 @@ pub fn tessellate_display_list(
         ) || matches!(
             &item.prim,
             crate::scene::paint::Prim::Bevel { material, .. }
+            | crate::scene::paint::Prim::Frame { material, .. }
             | crate::scene::paint::Prim::Plate { material, .. }
             | crate::scene::paint::Prim::Droplet { material, .. }
                 if material.fill(PlateRole::Nested)[3] < 0.0
@@ -1616,6 +1617,23 @@ pub fn tessellate_display_list(
                 plate = Some(p);
                 made_plate = Some(*rect);
             }
+            Prim::Frame { rect, hole, hole_radii, material, depth } if shader_plates => {
+                // The Bevel branch turned inside out (shader MODE_FRAME): the
+                // cover quad is the face's bound, the SDF box is the HOLE,
+                // and the shader reads the distance outside it as the
+                // plate's depth. Nominal corner radii, as a Bevel's. A carve
+                // host over `rect`, like any filled plate.
+                let color = material.fill(PlateRole::Nested);
+                let mat = material.finish.to_array();
+                verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, color));
+                let mut p = plate_push_raised(hole, *hole_radii, *depth, scale, plate_light, mat, false, None);
+                let [fz, fw] = material.frost.pack(scale);
+                p.host[2] = fz;
+                p.host[3] = fw;
+                p.mode = 17.0; // MODE_FRAME
+                plate = Some(p);
+                made_plate = Some(*rect);
+            }
             Prim::Plate { rect, radii, material, depth, shape } if shader_plates => {
                 let color = material.fill(PlateRole::Nested);
                 let mat = material.finish.to_array();
@@ -1928,6 +1946,17 @@ pub fn tessellate_display_list(
                 push_rounded_rect_vertices_corners(rect.x, rect.y, rect.width, rect.height, corners, sw, sh, color, no, None, &mut verts);
                 push_plate_bevel_vertices(rect.x, rect.y, rect.width, rect.height, radii.0, *depth, sw, sh, color, no, &mut verts);
             }
+            // The legacy banded path has no inside-out SDF: the face square,
+            // and only below the hole, so the coves' rows are left to what
+            // is beneath (A/B comparison path only).
+            Prim::Frame { rect, hole, material, .. } => {
+                let color = material.fill(PlateRole::Nested);
+                let y0 = rect.y.max(hole.y + hole.height);
+                let y1 = rect.y + rect.height;
+                if y1 > y0 {
+                    verts.extend(quad_vertices(rect.x, y0, rect.width, y1 - y0, sw, sh, color));
+                }
+            }
             Prim::Plate { rect, radii, material, depth, .. } => {
                 let color = material.fill(PlateRole::Nested);
                 if *depth < 0.0 {
@@ -3055,3 +3084,33 @@ mod near_roll_fallback_tests {
         );
     }
 }
+
+#[cfg(test)]
+mod frame_tests {
+    use crate::scene::layout::Rect;
+    use crate::scene::paint::PaintCtx;
+
+    /// A frame is a plate turned inside out: its batch carries the HOLE as
+    /// the SDF box, in mode 17, its cover quad is the face's bound, and it
+    /// hosts the carves inside that bound as a Bevel does.
+    #[test]
+    fn a_frame_is_an_inside_out_plate_that_hosts_its_carves() {
+        if !crate::layout::bevel_shader() {
+            eprintln!("skipping: the shader plates are off in this configuration");
+            return;
+        }
+        let (w, h, scale) = (400.0f32, 200.0f32, 1.0f32);
+        let face = Rect { x: -10.0, y: 100.0, width: 420.0, height: 110.0 };
+        let hole = Rect { x: 0.0, y: -100.0, width: 400.0, height: 224.0 };
+        let mut pc = PaintCtx::new();
+        let material = crate::scene::Material::opaque([0.3, 0.3, 0.35, 1.0]);
+        pc.frame(face, hole, (0.0, 0.0, 20.0, 20.0), &material, 8.0);
+        pc.recess(Rect { x: 40.0, y: 150.0, width: 30.0, height: 20.0 }, (4.0, 4.0, 4.0, 4.0), 3.0);
+        let dl = pc.finish();
+        let (_, batches, _, features) = super::tessellate_display_list(&dl, w, h, scale);
+        let plate = batches.iter().find_map(|b| b.plate.filter(|p| p.mode == 17.0)).expect("a mode-17 batch");
+        assert_eq!(plate.rect, [200.0, 12.0, 200.0, 112.0], "the SDF box is the hole");
+        assert_eq!(plate.radii[2], 20.0, "the hole's bottom corners are the coves");
+        assert_eq!(features.len(), 1, "the carve inside the face groups into the frame");
+    }
+}
diff --git a/src/draw/mod.rs b/src/draw/mod.rs
index 6f9a583..4154786 100644
--- a/src/draw/mod.rs
+++ b/src/draw/mod.rs
@@ -70,7 +70,7 @@ pub fn batch_push_constants(batch: &Batch2D, clip_shape: f32, feature_base: usiz
         pc[20..24].copy_from_slice(&p.material);
         pc[24..28].copy_from_slice(&p.host);
         pc[28..32].copy_from_slice(&p.specular_tint);
-        if p.mode == 1.0 || p.mode == 14.0 {
+        if p.mode == 1.0 || p.mode == 14.0 || p.mode == 17.0 {
             // Rebase the feature offset onto this frame's UBO slot (a plate's
             // CSG carves, or a union carve's boxes).
             pc[24] += feature_base as f32;
diff --git a/src/draw/shader2d.wgsl b/src/draw/shader2d.wgsl
index 4b36d80..c98c6cb 100644
--- a/src/draw/shader2d.wgsl
+++ b/src/draw/shader2d.wgsl
@@ -158,6 +158,7 @@ const MODE_LATTICE: i32 = 13;     // periodic well field: nearest-cell carve, on
 const MODE_UNION: i32 = 14;       // union of feature boxes carved/raised as one wall
 const MODE_GROUT: i32 = 15;       // flat colour outside a periodic field of rounded cells
 const MODE_FIELD: i32 = 16;       // sunken well ending in a flush run: one outline (Prim::Field)
+const MODE_FRAME: i32 = 17;       // MODE_PLATE inside out: the face is OUTSIDE the box (Prim::Frame)
 // Fillet modes rejoin the shared free-carve path as their flat equivalents.
 const FILLET_TO_STEP: i32 = 4;    // 6 -> RECESS, 7 -> BOSS
 
@@ -636,11 +637,17 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
         );
     }
 
-    let gd = rr_sdf_grad(frag, rrect_clip.p_rect, rrect_clip.p_radii);
+    // MODE_FRAME is MODE_PLATE turned inside out: the box is the plate's
+    // HOLE, so the field is negated — the distance outside the box is the
+    // depth into the face, and the outward gradient points into the hole.
+    // Everything after is the plate's own branch: the roll falls into the
+    // hole, and a corner of the hole is a cove in the face.
+    let gd0 = rr_sdf_grad(frag, rrect_clip.p_rect, rrect_clip.p_radii);
+    let gd = select(gd0, -gd0, mode == MODE_FRAME);
     let d = -gd.z; // positive inside the plate, in px
     let t = max(rrect_clip.p_light.w, 0.001);
 
-    if (mode == MODE_PLATE) {
+    if (mode == MODE_PLATE || mode == MODE_FRAME) {
         let aa = clamp(d + 0.5, 0.0, 1.0); // 1px silhouette anti-aliasing
         if (aa <= 0.0) {
             discard;
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index c034ece..2e3089c 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -626,6 +626,18 @@ pub enum Prim {
     /// tint, so the relief still reads. Shader-plates path only; the legacy
     /// banded tessellation ignores it.
     Bevel { rect: Rect, radii: Radii, material: Material, depth: f32, tint: [f32; 3] },
+    /// A [`Prim::Bevel`] turned inside out: the plate's face is everything in
+    /// `rect` OUTSIDE `hole`, and its rolled edge runs round the hole's
+    /// outline, falling INTO the hole. So a corner of the hole is an inside
+    /// corner of the plate — a cove, rounded at the hole's radius in the DE's
+    /// corner family — which a box can only round convex. A band of a
+    /// window's edge with an opening above it (the designer's playbar shelf):
+    /// its top edge and both coves are ONE outline, one profile evaluation,
+    /// with no join to stack two shadings at. `rect` bounds the face (its
+    /// own edges are not rolled — lay them past the window or under
+    /// something), and it is a carve host like a Bevel: carves inside `rect`
+    /// group into it. Shader-plates path; the legacy path fills `rect`.
+    Frame { rect: Rect, hole: Rect, hole_radii: Radii, material: Material, depth: f32 },
     /// A recess carved into whatever is already painted underneath — the inverse of
     /// `Bevel`. Emits ONLY the shaded edges, never a fill, so the surface below shows
     /// through the middle: a relief cut into the root plate rather than a plate laid on
@@ -1283,6 +1295,9 @@ impl PaintCtx {
             Prim::Bevel { rect, radii, material, depth, tint } => {
                 self.bevel_tinted(rect, radii, &material, depth, tint)
             }
+            Prim::Frame { rect, hole, hole_radii, material, depth } => {
+                self.frame(rect, hole, hole_radii, &material, depth)
+            }
             Prim::Recess { rect, radii, depth, edges, tint } => match tint {
                 Some(t) => self.recess_tinted(rect, radii, depth, t),
                 None => self.recess_edges(rect, radii, depth, edges),
@@ -1405,6 +1420,13 @@ impl PaintCtx {
         self.bevel_tinted(rect, radii, material, depth, [1.0, 1.0, 1.0]);
     }
 
+    /// A plate whose face is `rect` outside `hole` — see [`Prim::Frame`].
+    pub fn frame(&mut self, rect: Rect, hole: Rect, hole_radii: Radii, material: &Material, depth: f32) {
+        let rect = self.apply_offset(rect);
+        let hole = self.apply_offset(hole);
+        self.push(Prim::Frame { rect, hole, hole_radii, material: *material, depth });
+    }
+
     /// `bevel` with a specular tint — see `Prim::Bevel::tint`.
     pub fn bevel_tinted(&mut self, rect: Rect, radii: Radii, material: &Material, depth: f32, tint: [f32; 3]) {
         let rect = self.apply_offset(rect);
diff --git a/src/scene/relief_shade.rs b/src/scene/relief_shade.rs
index 4d53b10..85dad8a 100644
--- a/src/scene/relief_shade.rs
+++ b/src/scene/relief_shade.rs
@@ -310,7 +310,7 @@ mod tests {
     #[test]
     fn a_grouped_carve_takes_no_shade_line() {
         let plate = WGSL
-            .split("if (mode == MODE_PLATE) {")
+            .split("if (mode == MODE_PLATE || mode == MODE_FRAME) {")
             .nth(1)
             .and_then(|rest| rest.split("if (mode == MODE_ROLL) {").next())
             .expect("the plate branch");