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

commit4cbf672a99af611724322cc27081e5dfd7f6977e
parentb3fc35af79
authorLucas Galante <[email protected]>
date2026-08-25 14:52
7b fix: plate_spec double-spanned every window corner

The spec's radii are FINAL on-screen values — a window corner already
wears the full silhouette span. Prim::Plate speaks the older convention:
NOMINAL radii, span applied downstream by plate_push_raised
(scale_corners = true), which the unmigrated hand-rolled plates
(cce-cloud, the test-interface gallery shim) still rely on. plate_spec
fed the final values straight through, so the five Plate-group clients
(files, terminal, system-interface, authenticator, data-editor) drew
window corners at span squared — 12 → ~100 logical at corner_shape 4.5.
The plate arc pulled away from the compositor clip, the black window
background showed through as a corner crescent, and corners stopped
matching the desktop grid.

Those five apps had been correct BEFORE 7b-2 (nominal in, spanned once
by the push); the "arc correction" the A/B eyeball accepted was the
regression itself. It surfaced as a user report — corners visibly
rounder than the grid — and was pinned by measuring a live corner
diagonal: clip and grid arc at the expected 0.202·span depth (~14 device
px), plate arc ~2x deeper, the circle-vs-superellipse ratio a red
herring against the span-squared superellipse. The four scalar-group
clients tessellate without a downstream span; their spanned scalar was
and remains the genuine correction and is untouched here.

Fix: plate_spec pre-divides all four radii by corner_span_factor() so
the push's multiply reconstructs the spec's exact values — one function,
no Prim change, no per-app edits, and interior corners now land at
exactly plate_corner_radius() instead of being quietly spanned. Unit
test plate_spec_emission_round_trips_the_span guards the round-trip for
any configured corner_shape. RFC 7b notes corrected, and the stale
"PROPOSED, not started" Phase 7 header updated to the real state.

Clients need a REBUILD to pick this up (toolkit change; see the
WORKSPACE.md note) — the five Plate-group apps ship it with their next
install.

Co-Authored-By: Claude Opus 5 <[email protected]>

 docs/rfc-core-rebuild.md | 25 ++++++++++++++++++++---
 src/scene/paint.rs       | 52 +++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 73 insertions(+), 4 deletions(-)

diff --git a/docs/rfc-core-rebuild.md b/docs/rfc-core-rebuild.md
index 350a021..6861390 100644
--- a/docs/rfc-core-rebuild.md
+++ b/docs/rfc-core-rebuild.md
@@ -2347,7 +2347,7 @@ Constraint respected: **each crate still builds standalone** — the new core is
     phase-4 flip reaches them first; no standalone pointer-to-id
     signature sweep.
 
-- **Phase 7 — Plate unification: backplate becomes a ROLE of Plate (PROPOSED, not started).**
+- **Phase 7 — Plate unification: backplate becomes a ROLE of Plate (7a/7b/7c stages DONE 2026-08-25; open questions below).**
   Finish what 6as/6at began. The `Backplate` and `Plate` container widgets are deleted and the
   `is_backplate`/`is_movable_backplate`/`is_plate` flags are folded, but "backplate" survives as
   a second vocabulary for what is now one concept — a lit base surface (`Prim::Plate`). What
@@ -2495,6 +2495,23 @@ Constraint respected: **each crate still builds standalone** — the new core is
       design. Verified: seven apps diff ONLY within 120px corner squares; the eighth
       (system-interface, hardcoded r=12 → silhouette, the largest jump) also shifts the
       perimeter roll's edge gradient, eyeball-confirmed as the arc correction.
+
+      **CORRECTION (2026-08-25, post-7c): the Plate-group half of that story was
+      inverted — a double-span, since fixed.** `plate_spec` fed the spec's FINAL
+      radii into `Prim::Plate`, whose contract is NOMINAL radii spanned downstream
+      by `plate_push_raised(scale_corners = true)` — so the five Plate-group
+      clients drew window corners at span² (12 → ~100 logical at n=4.5). Those
+      apps had been CORRECT all along (nominal in, spanned once by the push); the
+      "arc correction" the A/B eyeball accepted was the regression itself, caught
+      when the user reported corners rounder than the desktop grid. Measured on a
+      live corner diagonal: clip/grid arc at the expected 0.202·span depth, plate
+      arc ~2× deeper. The four scalar-group clients (graph, fonts, color-editor,
+      text-editor) tessellate without a downstream span, so for them the spanned
+      scalar was and remains the genuine correction. Fix: `plate_spec` pre-divides
+      by `corner_span_factor()` so the push's multiply reconstructs the spec's
+      exact values; unit test `plate_spec_emission_round_trips_the_span` guards
+      it. The same double-span reached DemoApp via 7b-1 — its "AE≠0 expected"
+      diff bundled the genuine correction WITH the overshoot.
       Authenticator is values-only verification (never launch it in a shadow — it claims
       the PolicyKit D-Bus name). SKIPPED deliberately: cce-cloud (overlay popup windows —
       whether they share the decorated-window silhouette is an open question for 7c) and
@@ -2546,8 +2563,10 @@ Constraint respected: **each crate still builds standalone** — the new core is
         appear). It lands with the first non-designer detach implementation, as does the
         cce-cloud overlay-silhouette question from 7b-2.
 
-  Not started; no code moves before 7a's alias sweep is reviewed, and stages land in order —
-  7b is the value (one paint path, plates compositional), 7c is the payoff feature.
+  All numbered stages DONE (2026-08-25). Still open: detached-window CSD packaging (lands
+  with the first non-designer detach), the cce-cloud overlay-silhouette question, and
+  designer pane spec-OBJECT emission (focus tint / widget-driven bevel styling are not yet
+  spec fields).
 
 Order rationale: each phase is independently valuable and reversible, and no phase requires the
 next to compile. Phase 0 can land immediately regardless of the rest.
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index ae6c1e1..7247df7 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -1012,8 +1012,25 @@ impl PaintCtx {
 
     /// Emit the plate a [`PlateSpec`] describes: role-resolved per-corner
     /// radii and role-encoded frost (RFC Phase 7b).
+    ///
+    /// The spec's radii are FINAL on-screen values (a window corner already
+    /// wears the full silhouette span), but `Prim::Plate` speaks the older
+    /// convention — NOMINAL radii, span applied downstream by
+    /// `plate_push_raised(scale_corners = true)`, which the unmigrated
+    /// hand-rolled plates (cce-cloud, the test-interface gallery shim) still
+    /// rely on. So divide the span back out here and let the push multiply
+    /// reconstruct the spec's exact values.
+    ///
+    /// Feeding the final radii straight through double-spanned every window
+    /// corner (12 → ~100 logical at corner_shape 4.5): the plate arc pulled
+    /// away from the compositor's clip, the black window background showed
+    /// through as a corner crescent, and the corners stopped matching the
+    /// desktop grid — the original 7b-2 report of this looking like "the arc
+    /// correction" was the regression itself.
     pub fn plate_spec(&mut self, spec: &PlateSpec) {
-        self.plate(spec.rect, spec.radii(), spec.fill(), spec.depth);
+        let f = crate::layout::corner_span_factor();
+        let (tl, tr, br, bl) = spec.radii();
+        self.plate(spec.rect, (tl / f, tr / f, br / f, bl / f), spec.fill(), spec.depth);
     }
 
     pub fn arc(&mut self, cx: f32, cy: f32, radius: f32, thickness: f32, start: f32, end: f32, color: [f32; 4]) {
@@ -1275,6 +1292,39 @@ mod tests {
         Rect { x, y, width: w, height: h }
     }
 
+    /// The emission round-trip: `plate_spec` pre-divides by the span factor so
+    /// `plate_push_raised(scale_corners = true)` lands each corner at exactly
+    /// the spec's final radius. Guards the double-span regression (7b-2), and
+    /// holds for any configured corner_shape because both sides use the same
+    /// factor.
+    #[test]
+    fn plate_spec_emission_round_trips_the_span() {
+        let spec = PlateSpec {
+            rect: r(0.0, 0.0, 400.0, 300.0),
+            color: [0.1, 0.2, 0.3, 0.8],
+            blur: false,
+            window_corners: (true, true, false, false),
+            depth: 4.0,
+        };
+        let mut pc = PaintCtx::new();
+        pc.plate_spec(&spec);
+        let f = crate::layout::corner_span_factor();
+        let emitted = pc
+            .finish()
+            .items
+            .iter()
+            .find_map(|it| match &it.prim {
+                Prim::Plate { radii, .. } => Some(radii.clone()),
+                _ => None,
+            })
+            .expect("plate_spec emits a Prim::Plate");
+        let want = spec.radii();
+        let got = (emitted.0 * f, emitted.1 * f, emitted.2 * f, emitted.3 * f);
+        for (g, w) in [(got.0, want.0), (got.1, want.1), (got.2, want.2), (got.3, want.3)] {
+            assert!((g - w).abs() < 1e-3, "span round-trip drifted: {g} vs {w}");
+        }
+    }
+
     #[test]
     fn emits_in_order_unclipped() {
         let mut ctx = PaintCtx::new();