GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
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");