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

src/backend/tessellate.rs (144.7K)

   1 //! The tessellator: a `DisplayList` (or the legacy primitive tuples) turned
   2 //! into `Vertex` batches with their push constants, plus the vertex helpers
   3 //! apps call directly. Platform-neutral — it produces the data the renderer
   4 //! draws and knows nothing of the window system; moved out of
   5 //! `window_runner` so another shell can share it.
   6 
   7 use crate::widget::WidgetHostExt;
   8 use crate::widget::WidgetHost;
   9 use crate::draw::Batch2D;
  10 
  11 /// A droplet spec resolved against a concrete rect: the push-constant fields
  12 /// that define its SILHOUETTE, in logical px.
  13 ///
  14 /// Shared by [`crate::scene::paint::Prim::Droplet`] and
  15 /// [`crate::scene::paint::Prim::DropletScrim`] so the lit drop and the vignette
  16 /// drawn inside it can never disagree about the shape — the whole reason the
  17 /// scrim rides the droplet's shader path instead of approximating the outline
  18 /// with a rounded rect.
  19 struct DropletGeom {
  20     hx: f32,
  21     hy: f32,
  22     sag: f32,
  23     br: f32,
  24     bw: f32,
  25     k: f32,
  26     sr: f32,
  27     ar: f32,
  28     band: f32,
  29     bow: f32,
  30     /// How far the contact shadow reaches below/beside the box (0 when the
  31     /// spec has no shadow). The lit drop's cover quad grows by this; a scrim
  32     /// never draws outside the silhouette and ignores it.
  33     sh_reach: f32,
  34 }
  35 
  36 fn droplet_geom(rect: &crate::scene::layout::Rect, spec: &crate::scene::paint::DropletSpec) -> DropletGeom {
  37     let hx = rect.width * 0.5;
  38     let hy = rect.height * 0.5;
  39     let sag = spec.sag.clamp(0.0, 0.9) * rect.height;
  40     // belly <= 0 disables the belly outright (the oval-dewdrop default) — the
  41     // shader skips the smin when the radius is 0.
  42     let (br, bw) = if spec.belly > 0.0 {
  43         let br = (spec.belly.min(1.0) * rect.height).min(hy).min(hx);
  44         (br, ((hx - br).max(0.0) * spec.belly_w.clamp(0.0, 1.0)).max(1.0))
  45     } else {
  46         (0.0, 0.0)
  47     };
  48     let k = (spec.blend.max(0.0) * rect.height).max(1.0);
  49     let sheet_hy = hy - sag * 0.5;
  50     // Bottom (sheet_r) and top (attach) corner radii: when the pair overfills
  51     // the sheet height, scale both down proportionally — 0.5 + 0.5 is the
  52     // fully continuous egg.
  53     let mut sr = (spec.sheet_r.clamp(0.0, 1.0) * rect.height).min(hx);
  54     let mut ar = (spec.attach.clamp(0.0, 1.0) * rect.height).min(hx);
  55     let sheet_h = (2.0 * sheet_hy).max(0.0);
  56     if sr + ar > sheet_h && sr + ar > 0.0 {
  57         let f = sheet_h / (sr + ar);
  58         sr *= f;
  59         ar *= f;
  60     }
  61     let band = (spec.band.max(0.05) * rect.height).max(1.0);
  62     // Bottom-bow edge rise; the shader derives the arc radius from it per drop
  63     // (R = hx^2/2*rise).
  64     let bow = (spec.bow.clamp(0.0, 0.5) * rect.height).min(hy * 0.9);
  65     let sh_reach = if spec.shadow > 0.0 { (0.18 * rect.height).max(2.0) } else { 0.0 };
  66     DropletGeom { hx, hy, sag, br, bw, k, sr, ar, band, bow, sh_reach }
  67 }
  68 
  69 /// The tessellated display list's batches, scissors and rounded clips scaled
  70 /// to physical px.
  71 pub(crate) fn dl_batches_2d(dl_batches: &[DlBatch], scale_f32: f32) -> Vec<Batch2D> {
  72     dl_batches
  73         .iter()
  74         .map(|batch| Batch2D {
  75             scissor: batch.scissor.map(|clip| {
  76                 (
  77                     (clip.x * scale_f32).max(0.0) as u32,
  78                     (clip.y * scale_f32).max(0.0) as u32,
  79                     (clip.width * scale_f32) as u32,
  80                     (clip.height * scale_f32) as u32,
  81                 )
  82             }),
  83             clip_rrect: batch
  84                 .clip_rrect
  85                 .map(|c| [c[0] * scale_f32, c[1] * scale_f32, c[2] * scale_f32, c[3] * scale_f32, c[4] * scale_f32]),
  86             start: batch.start,
  87             end: batch.end,
  88             plate: batch.plate,
  89             blur_behind: batch.blur_behind,
  90         })
  91         .collect()
  92 }
  93 
  94 #[repr(C)]
  95 #[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
  96 pub struct Vertex {
  97     pub position: [f32; 2],
  98     pub color: [f32; 4],
  99     pub clip_circle: [f32; 3], // [cx, cy, r]
 100 }
 101 
 102 pub fn quad_vertices(x: f32, y: f32, w: f32, h: f32, sw: f32, sh: f32, c: [f32; 4]) -> [Vertex; 6] {
 103     let x0 = (x / sw) * 2.0 - 1.0;
 104     let y0 = 1.0 - (y / sh) * 2.0;
 105     let x1 = ((x + w) / sw) * 2.0 - 1.0;
 106     let y1 = 1.0 - ((y + h) / sh) * 2.0;
 107     [
 108         Vertex { position: [x0, y0], color: c, clip_circle: [0.0, 0.0, 0.0] },
 109         Vertex { position: [x1, y0], color: c, clip_circle: [0.0, 0.0, 0.0] },
 110         Vertex { position: [x0, y1], color: c, clip_circle: [0.0, 0.0, 0.0] },
 111         Vertex { position: [x1, y0], color: c, clip_circle: [0.0, 0.0, 0.0] },
 112         Vertex { position: [x1, y1], color: c, clip_circle: [0.0, 0.0, 0.0] },
 113         Vertex { position: [x0, y1], color: c, clip_circle: [0.0, 0.0, 0.0] },
 114     ]
 115 }
 116 
 117 pub fn quad_vertices_with_clip(
 118     x: f32, y: f32, w: f32, h: f32,
 119     sw: f32, sh: f32,
 120     color: [f32; 4],
 121     clip_circle: [f32; 3],
 122 ) -> [Vertex; 6] {
 123     let x0 = (x / sw) * 2.0 - 1.0;
 124     let y0 = 1.0 - (y / sh) * 2.0;
 125     let x1 = ((x + w) / sw) * 2.0 - 1.0;
 126     let y1 = 1.0 - ((y + h) / sh) * 2.0;
 127     [
 128         Vertex { position: [x0, y0], color, clip_circle },
 129         Vertex { position: [x1, y0], color, clip_circle },
 130         Vertex { position: [x0, y1], color, clip_circle },
 131         Vertex { position: [x1, y0], color, clip_circle },
 132         Vertex { position: [x1, y1], color, clip_circle },
 133         Vertex { position: [x0, y1], color, clip_circle },
 134     ]
 135 }
 136 
 137 /// A quad whose four corners each carry their own color, Gouraud-interpolated across both
 138 /// triangles by the shader (`@location(0) color` has no `flat` qualifier). Corner order is
 139 /// TL, TR, BR, BL. Keep the alpha equal on all four: negative alpha is the blur sentinel,
 140 /// so a gradient that crossed zero would tear the triangle in half.
 141 pub fn quad_vertices_shaded(
 142     x: f32, y: f32, w: f32, h: f32,
 143     sw: f32, sh: f32,
 144     c_tl: [f32; 4], c_tr: [f32; 4], c_br: [f32; 4], c_bl: [f32; 4],
 145     clip_circle: [f32; 3],
 146 ) -> [Vertex; 6] {
 147     let x0 = (x / sw) * 2.0 - 1.0;
 148     let y0 = 1.0 - (y / sh) * 2.0;
 149     let x1 = ((x + w) / sw) * 2.0 - 1.0;
 150     let y1 = 1.0 - ((y + h) / sh) * 2.0;
 151     [
 152         Vertex { position: [x0, y0], color: c_tl, clip_circle },
 153         Vertex { position: [x1, y0], color: c_tr, clip_circle },
 154         Vertex { position: [x0, y1], color: c_bl, clip_circle },
 155         Vertex { position: [x1, y0], color: c_tr, clip_circle },
 156         Vertex { position: [x1, y1], color: c_br, clip_circle },
 157         Vertex { position: [x0, y1], color: c_bl, clip_circle },
 158     ]
 159 }
 160 
 161 pub fn quad_vertices_clipped(
 162     x: f32, y: f32, w: f32, h: f32,
 163     surface_w: f32, surface_h: f32,
 164     color: [f32; 4],
 165     clip: (f32, f32, f32, f32),
 166     clip_circle: [f32; 3],
 167 ) -> Vec<Vertex> {
 168     let (cx0, cy0, cx1, cy1) = clip;
 169     let ix0 = x.max(cx0);
 170     let iy0 = y.max(cy0);
 171     let ix1 = (x + w).min(cx1);
 172     let iy1 = (y + h).min(cy1);
 173     if ix1 <= ix0 || iy1 <= iy0 {
 174         return Vec::new();
 175     }
 176     quad_vertices_with_clip(ix0, iy0, ix1 - ix0, iy1 - iy0, surface_w, surface_h, color, clip_circle).to_vec()
 177 }
 178 
 179 pub fn line_vertices(
 180     x1: f32, y1: f32, x2: f32, y2: f32,
 181     thickness: f32,
 182     sw: f32, sh: f32,
 183     c: [f32; 4]
 184 ) -> [Vertex; 6] {
 185     let dx = x2 - x1;
 186     let dy = y2 - y1;
 187     let len = (dx * dx + dy * dy).sqrt();
 188     if len < 0.001 {
 189         return quad_vertices(x1 - thickness/2.0, y1 - thickness/2.0, thickness, thickness, sw, sh, c);
 190     }
 191     let ux = dx / len;
 192     let uy = dy / len;
 193     let nx = -uy;
 194     let ny = ux;
 195     
 196     let half_t = thickness * 0.5;
 197     let p0x = x1 + nx * half_t;
 198     let p0y = y1 + ny * half_t;
 199     let p1x = x1 - nx * half_t;
 200     let p1y = y1 - ny * half_t;
 201     let p2x = x2 - nx * half_t;
 202     let p2y = y2 - ny * half_t;
 203     let p3x = x2 + nx * half_t;
 204     let p3y = y2 + ny * half_t;
 205 
 206     let ndc_p0x = (p0x / sw) * 2.0 - 1.0;
 207     let ndc_p0y = 1.0 - (p0y / sh) * 2.0;
 208     let ndc_p1x = (p1x / sw) * 2.0 - 1.0;
 209     let ndc_p1y = 1.0 - (p1y / sh) * 2.0;
 210     let ndc_p2x = (p2x / sw) * 2.0 - 1.0;
 211     let ndc_p2y = 1.0 - (p2y / sh) * 2.0;
 212     let ndc_p3x = (p3x / sw) * 2.0 - 1.0;
 213     let ndc_p3y = 1.0 - (p3y / sh) * 2.0;
 214 
 215     let clip_circle = [0.0, 0.0, 0.0];
 216     [
 217         Vertex { position: [ndc_p0x, ndc_p0y], color: c, clip_circle },
 218         Vertex { position: [ndc_p1x, ndc_p1y], color: c, clip_circle },
 219         Vertex { position: [ndc_p2x, ndc_p2y], color: c, clip_circle },
 220         Vertex { position: [ndc_p0x, ndc_p0y], color: c, clip_circle },
 221         Vertex { position: [ndc_p2x, ndc_p2y], color: c, clip_circle },
 222         Vertex { position: [ndc_p3x, ndc_p3y], color: c, clip_circle },
 223     ]
 224 }
 225 
 226 #[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
 227 pub enum LineCap {
 228     Arrow,
 229     Round,
 230     Flat,
 231 }
 232 
 233 pub fn vector_vertices(
 234     x1: f32, y1: f32, x2: f32, y2: f32,
 235     thickness: f32,
 236     sw: f32, sh: f32,
 237     c: [f32; 4],
 238     line_cap: LineCap,
 239 ) -> Vec<Vertex> {
 240     let mut verts = Vec::new();
 241     let dx = x2 - x1;
 242     let dy = y2 - y1;
 243     let len = (dx * dx + dy * dy).sqrt();
 244     if len < 0.001 {
 245         return quad_vertices(x1 - thickness/2.0, y1 - thickness/2.0, thickness, thickness, sw, sh, c).to_vec();
 246     }
 247     
 248     match line_cap {
 249         LineCap::Arrow => {
 250             let ux = dx / len;
 251             let uy = dy / len;
 252             let nx = -uy;
 253             let ny = ux;
 254             
 255             let arrow_len = (thickness * 3.0).max(10.0).min(len);
 256             let arrow_width = (thickness * 2.5).max(8.0);
 257             
 258             let line_x2 = x2 - ux * arrow_len;
 259             let line_y2 = y2 - uy * arrow_len;
 260             
 261             if len > arrow_len {
 262                 verts.extend_from_slice(&line_vertices(x1, y1, line_x2, line_y2, thickness, sw, sh, c));
 263             }
 264             
 265             let bx = line_x2;
 266             let by = line_y2;
 267             
 268             let w1x = bx + nx * (arrow_width * 0.5);
 269             let w1y = by + ny * (arrow_width * 0.5);
 270             let w2x = bx - nx * (arrow_width * 0.5);
 271             let w2y = by - ny * (arrow_width * 0.5);
 272             
 273             let ndc_tip_x = (x2 / sw) * 2.0 - 1.0;
 274             let ndc_tip_y = 1.0 - (y2 / sh) * 2.0;
 275             let ndc_w1x = (w1x / sw) * 2.0 - 1.0;
 276             let ndc_w1y = 1.0 - (w1y / sh) * 2.0;
 277             let ndc_w2x = (w2x / sw) * 2.0 - 1.0;
 278             let ndc_w2y = 1.0 - (w2y / sh) * 2.0;
 279             
 280             let clip_circle = [0.0, 0.0, 0.0];
 281             verts.push(Vertex { position: [ndc_tip_x, ndc_tip_y], color: c, clip_circle });
 282             verts.push(Vertex { position: [ndc_w1x, ndc_w1y], color: c, clip_circle });
 283             verts.push(Vertex { position: [ndc_w2x, ndc_w2y], color: c, clip_circle });
 284         }
 285         LineCap::Round => {
 286             push_feathered_line_vertices(x1, y1, x2, y2, thickness, sw, sh, c, &mut verts);
 287             let clip_circle = [0.0, 0.0, 0.0];
 288             verts.extend(circle_vertices(x2, y2, thickness / 2.0, sw, sh, c, 16, clip_circle));
 289         }
 290         LineCap::Flat => {
 291             push_feathered_line_vertices(x1, y1, x2, y2, thickness, sw, sh, c, &mut verts);
 292         }
 293     }
 294 
 295     verts
 296 }
 297 
 298 /// `line_vertices` with a half-px alpha ramp along each long edge (the arc
 299 /// tessellator's poor-man's AA) — diagonal strokes resolve smoothly instead of
 300 /// stair-stepping. Axis-aligned strokes keep the crisp single-quad path:
 301 /// feathering a pixel-snapped hairline would only blur it.
 302 fn push_feathered_line_vertices(
 303     x1: f32, y1: f32, x2: f32, y2: f32,
 304     thickness: f32,
 305     sw: f32, sh: f32,
 306     c: [f32; 4],
 307     out: &mut Vec<Vertex>,
 308 ) {
 309     let dx = x2 - x1;
 310     let dy = y2 - y1;
 311     let len = (dx * dx + dy * dy).sqrt();
 312     if len < 0.001 || dx.abs() < 0.01 || dy.abs() < 0.01 {
 313         out.extend_from_slice(&line_vertices(x1, y1, x2, y2, thickness, sw, sh, c));
 314         return;
 315     }
 316     let (nx, ny) = (-dy / len, dx / len);
 317     let f = 0.5f32.min(thickness * 0.25);
 318     let half = thickness * 0.5;
 319     // (offset at band start, offset at band end, alpha at start, alpha at end)
 320     let bands = [
 321         (-half - f, -half + f, 0.0, c[3]),
 322         (-half + f, half - f, c[3], c[3]),
 323         (half - f, half + f, c[3], 0.0),
 324     ];
 325     for &(oa, ob, aa, ab) in &bands {
 326         let ca = [c[0], c[1], c[2], aa];
 327         let cb = [c[0], c[1], c[2], ab];
 328         let p = |x: f32, y: f32, o: f32| -> [f32; 2] {
 329             [((x + nx * o) / sw) * 2.0 - 1.0, 1.0 - ((y + ny * o) / sh) * 2.0]
 330         };
 331         let clip_circle = [0.0, 0.0, 0.0];
 332         let (a1, b1) = (p(x1, y1, oa), p(x1, y1, ob));
 333         let (a2, b2) = (p(x2, y2, oa), p(x2, y2, ob));
 334         out.push(Vertex { position: a1, color: ca, clip_circle });
 335         out.push(Vertex { position: b1, color: cb, clip_circle });
 336         out.push(Vertex { position: b2, color: cb, clip_circle });
 337         out.push(Vertex { position: a1, color: ca, clip_circle });
 338         out.push(Vertex { position: b2, color: cb, clip_circle });
 339         out.push(Vertex { position: a2, color: ca, clip_circle });
 340     }
 341 }
 342 
 343 pub fn rounded_rect_vertices_corners(
 344     x: f32, y: f32, ww: f32, h: f32,
 345     r: f32,
 346     sw: f32, sh: f32,
 347     color: [f32; 4],
 348     clip_circle: [f32; 3],
 349     corners: (bool, bool, bool, bool),
 350     clip_rect: Option<(f32, f32, f32, f32)>,
 351 ) -> Vec<Vertex> {
 352     let mut verts = Vec::new();
 353     let radii = crate::widget::CornerRadii::new(
 354         if corners.0 { r } else { 0.0 },
 355         if corners.1 { r } else { 0.0 },
 356         if corners.2 { r } else { 0.0 },
 357         if corners.3 { r } else { 0.0 },
 358     );
 359     push_rounded_rect_vertices_corners(x, y, ww, h, radii, sw, sh, color, clip_circle, clip_rect, &mut verts);
 360     verts
 361 }
 362 
 363 /// Sample of the unit superellipse |x|^n + |y|^n = 1 at circle parameter θ —
 364 /// the (cos θ, sin θ) replacement the corner fans use. Exactly the circle at
 365 /// n = 2; higher `corner_shape` exponents give the DE's continuous-curvature
 366 /// corners, so widget silhouettes follow the same corner family as the
 367 /// SDF-lit plates. `e` is 2/n, hoisted by callers. Tangent points at the
 368 /// quadrant ends are pinned to the exact axis points (see below), so fans
 369 /// tile exactly against the body rects and edge strips.
 370 #[inline]
 371 fn superellipse_pt(theta: f32, e: f32) -> (f32, f32) {
 372     let (s, c) = theta.sin_cos();
 373     // f32 sin/cos are not exactly 0 at a quadrant end (sin(PI) is -8.7e-8),
 374     // and the fractional power magnifies that noise by orders of magnitude:
 375     // at corner_shape 4.5 it is 7e-4, which on the desktop grid's 138 px
 376     // cell corners put the fan's tangent vertex 0.1 px short of the body
 377     // quad's edge. The fan's edge then tilts away from the quad's, and the
 378     // row of pixel centres between them is covered by neither: a stray
 379     // gap-coloured line 32 px long at every cell's left edge, and a lone
 380     // gap pixel on the bottom row where the arc meets the body. Snap the
 381     // ends to the exact axis points so fans tile against the rects.
 382     let axis = |v: f32| -> f32 {
 383         if v.abs() < 1e-6 {
 384             0.0
 385         } else if v.abs() > 1.0 - 1e-6 {
 386             v.signum()
 387         } else {
 388             v.signum() * v.abs().powf(e)
 389         }
 390     };
 391     (axis(c), axis(s))
 392 }
 393 
 394 /// Feathered glow ([`Prim::Glow`]): the rounded rect's interior fills at the
 395 /// color's alpha and concentric outline rings fade it to zero across `reach`
 396 /// px outside the boundary. Alpha rides the VERTICES, so the GPU interpolates
 397 /// a per-pixel-smooth falloff between rings — stacked translucent layers band
 398 /// visibly; this cannot. Ring alphas sit on a quadratic ease-out, giving the
 399 /// vignette profile piecewise-linearly with kinks below visibility at glow
 400 /// alphas. Corners sample [`superellipse_pt`], so a glow's silhouette sits in
 401 /// the same corner family as the cells, nodes, and plates it highlights.
 402 pub fn push_glow_vertices(
 403     x: f32, y: f32, ww: f32, h: f32,
 404     radius: f32, reach: f32,
 405     sw: f32, sh: f32,
 406     color: [f32; 4],
 407     clip_circle: [f32; 3],
 408     out: &mut Vec<Vertex>,
 409 ) {
 410     if ww <= 0.0 || h <= 0.0 || color[3].abs() <= 0.0005 || sw <= 0.0 || sh <= 0.0 {
 411         return;
 412     }
 413     let r0 = radius.clamp(0.0, ww.min(h) * 0.5);
 414     let ctl = (x + r0, y + r0);
 415     let ctr = (x + ww - r0, y + r0);
 416     let cbr = (x + ww - r0, y + h - r0);
 417     let cbl = (x + r0, y + h - r0);
 418     const K: usize = 10;
 419     use std::f32::consts::PI;
 420     let corner_e = 2.0 / crate::layout::corner_shape();
 421     // One outline ring `off` px outside the boundary, clockwise from the
 422     // top-left arc; every ring shares the layout, so strips never twist.
 423     let ring = |off: f32| -> Vec<[f32; 2]> {
 424         let r = (r0 + off).max(0.0);
 425         let mut pts = Vec::with_capacity(4 * (K + 1));
 426         let corners = [
 427             (ctl, PI, 1.5 * PI),
 428             (ctr, 1.5 * PI, 2.0 * PI),
 429             (cbr, 0.0, 0.5 * PI),
 430             (cbl, 0.5 * PI, PI),
 431         ];
 432         for ((cx, cy), a0, a1) in corners {
 433             for k in 0..=K {
 434                 let a = a0 + (a1 - a0) * (k as f32 / K as f32);
 435                 let (ux, uy) = superellipse_pt(a, corner_e);
 436                 pts.push([cx + r * ux, cy + r * uy]);
 437             }
 438         }
 439         pts
 440     };
 441     let to_v = |p: [f32; 2], a: f32| Vertex {
 442         position: [(p[0] / sw) * 2.0 - 1.0, 1.0 - (p[1] / sh) * 2.0],
 443         color: [color[0], color[1], color[2], a],
 444         clip_circle,
 445     };
 446 
 447     let rings: Vec<(Vec<[f32; 2]>, f32)> = [0.0f32, 0.35, 0.7, 1.0]
 448         .iter()
 449         .map(|&t| (ring(reach * t), color[3] * (1.0 - t) * (1.0 - t)))
 450         .collect();
 451     let n = rings[0].0.len();
 452 
 453     // Interior: a fan from the rect center over the innermost ring (a rounded
 454     // rect is convex, so the fan covers it exactly), uniform core alpha.
 455     let center = [x + ww * 0.5, y + h * 0.5];
 456     for i in 0..n {
 457         let p1 = rings[0].0[i];
 458         let p2 = rings[0].0[(i + 1) % n];
 459         out.push(to_v(center, color[3]));
 460         out.push(to_v(p1, color[3]));
 461         out.push(to_v(p2, color[3]));
 462     }
 463     // The feather: strips between consecutive rings, each vertex carrying its
 464     // ring's alpha.
 465     for w in rings.windows(2) {
 466         let (inner, ia) = (&w[0].0, w[0].1);
 467         let (outer, oa) = (&w[1].0, w[1].1);
 468         for i in 0..n {
 469             let a1 = inner[i];
 470             let a2 = inner[(i + 1) % n];
 471             let b1 = outer[i];
 472             let b2 = outer[(i + 1) % n];
 473             out.push(to_v(a1, ia));
 474             out.push(to_v(b1, oa));
 475             out.push(to_v(a2, ia));
 476             out.push(to_v(a2, ia));
 477             out.push(to_v(b1, oa));
 478             out.push(to_v(b2, oa));
 479         }
 480     }
 481 }
 482 
 483 pub fn push_rounded_rect_vertices_corners(
 484     x: f32, y: f32, ww: f32, h: f32,
 485     radii: crate::widget::CornerRadii,
 486     sw: f32, sh: f32,
 487     color: [f32; 4],
 488     clip_circle: [f32; 3],
 489     clip_rect: Option<(f32, f32, f32, f32)>,
 490     out: &mut Vec<Vertex>,
 491 ) {
 492     let corner_e = 2.0 / crate::layout::corner_shape();
 493     let mut r_tl = radii.top_left.max(0.0);
 494     let mut r_tr = radii.top_right.max(0.0);
 495     let mut r_br = radii.bottom_right.max(0.0);
 496     let mut r_bl = radii.bottom_left.max(0.0);
 497 
 498     // Simple scale clamping
 499     let sum_top = r_tl + r_tr;
 500     if sum_top > ww {
 501         let f = ww / sum_top;
 502         r_tl *= f;
 503         r_tr *= f;
 504     }
 505     let sum_bottom = r_bl + r_br;
 506     if sum_bottom > ww {
 507         let f = ww / sum_bottom;
 508         r_bl *= f;
 509         r_br *= f;
 510     }
 511     let sum_left = r_tl + r_bl;
 512     if sum_left > h {
 513         let f = h / sum_left;
 514         r_tl *= f;
 515         r_bl *= f;
 516     }
 517     let sum_right = r_tr + r_br;
 518     if sum_right > h {
 519         let f = h / sum_right;
 520         r_tr *= f;
 521         r_br *= f;
 522     }
 523 
 524     let clamp_x = |val: f32| -> f32 {
 525         if let Some((cx0, _, cx1, _)) = clip_rect {
 526             val.max(cx0).min(cx1)
 527         } else {
 528             val
 529         }
 530     };
 531     let clamp_y = |val: f32| -> f32 {
 532         if let Some((_, cy0, _, cy1)) = clip_rect {
 533             val.max(cy0).min(cy1)
 534         } else {
 535             val
 536         }
 537     };
 538 
 539     let push_quad = |verts: &mut Vec<Vertex>, qx: f32, qy: f32, qw: f32, qh: f32| {
 540         let x0 = clamp_x(qx);
 541         let y0 = clamp_y(qy);
 542         let x1 = clamp_x(qx + qw);
 543         let y1 = clamp_y(qy + qh);
 544         
 545         if x1 <= x0 || y1 <= y0 {
 546             return;
 547         }
 548 
 549         let ndc_x0 = (x0 / sw) * 2.0 - 1.0;
 550         let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
 551         let ndc_x1 = (x1 / sw) * 2.0 - 1.0;
 552         let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
 553         
 554         verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
 555         verts.push(Vertex { position: [ndc_x1, ndc_y0], color, clip_circle });
 556         verts.push(Vertex { position: [ndc_x0, ndc_y1], color, clip_circle });
 557         verts.push(Vertex { position: [ndc_x1, ndc_y0], color, clip_circle });
 558         verts.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
 559         verts.push(Vertex { position: [ndc_x0, ndc_y1], color, clip_circle });
 560     };
 561 
 562     let has_corners = r_tl > 0.1 || r_tr > 0.1 || r_br > 0.1 || r_bl > 0.1;
 563     if !has_corners {
 564         push_quad(out, x, y, ww, h);
 565         return;
 566     }
 567 
 568     // Body rectangles
 569     let mid_x0 = r_tl.max(r_bl);
 570     let mid_x1 = ww - r_tr.max(r_br);
 571     if mid_x1 > mid_x0 {
 572         push_quad(out, x + mid_x0, y, mid_x1 - mid_x0, h);
 573     }
 574     if h > r_tl + r_bl {
 575         push_quad(out, x, y + r_tl, mid_x0, h - r_tl - r_bl);
 576     }
 577     if h > r_tr + r_br {
 578         push_quad(out, x + mid_x1, y + r_tr, ww - mid_x1, h - r_tr - r_br);
 579     }
 580 
 581     // Corner rendering. The fans are FEATHERED: the fan body stops half a
 582     // pixel short of the silhouette and a strip fades from opaque at
 583     // silhouette-0.5 to transparent at silhouette+0.5, so the arc
 584     // anti-aliases instead of rasterizing a hard staircase — invisible on
 585     // HiDPI widget buffers, glaring on the desktop grid's world-scale
 586     // cells. Perceived size is unchanged (the 50%-coverage line stays on
 587     // the exact silhouette). Radii too small to feather keep the hard fan.
 588     let segments = 16;
 589     let fade = [color[0], color[1], color[2], 0.0];
 590     let to_ndc = |px: f32, py: f32| -> [f32; 2] {
 591         [(px / sw) * 2.0 - 1.0, 1.0 - (py / sh) * 2.0]
 592     };
 593     let push_corner = |out: &mut Vec<Vertex>, cx: f32, cy: f32, r: f32, start: f32, end: f32| {
 594         let feather = r > 1.5;
 595         let r_fan = if feather { r - 0.5 } else { r };
 596         let r_out = r + 0.5;
 597         for i in 0..segments {
 598             let theta1 = start + (i as f32) * (end - start) / (segments as f32);
 599             let theta2 = start + ((i + 1) as f32) * (end - start) / (segments as f32);
 600 
 601             let (c1, s1) = superellipse_pt(theta1, corner_e);
 602             let (c2, s2) = superellipse_pt(theta2, corner_e);
 603             let p0 = to_ndc(clamp_x(cx), clamp_y(cy));
 604             let p1 = to_ndc(clamp_x(cx + r_fan * c1), clamp_y(cy + r_fan * s1));
 605             let p2 = to_ndc(clamp_x(cx + r_fan * c2), clamp_y(cy + r_fan * s2));
 606 
 607             out.push(Vertex { position: p0, color, clip_circle });
 608             out.push(Vertex { position: p1, color, clip_circle });
 609             out.push(Vertex { position: p2, color, clip_circle });
 610 
 611             if feather {
 612                 let q1 = to_ndc(clamp_x(cx + r_out * c1), clamp_y(cy + r_out * s1));
 613                 let q2 = to_ndc(clamp_x(cx + r_out * c2), clamp_y(cy + r_out * s2));
 614                 out.push(Vertex { position: p1, color, clip_circle });
 615                 out.push(Vertex { position: q1, color: fade, clip_circle });
 616                 out.push(Vertex { position: q2, color: fade, clip_circle });
 617                 out.push(Vertex { position: p1, color, clip_circle });
 618                 out.push(Vertex { position: q2, color: fade, clip_circle });
 619                 out.push(Vertex { position: p2, color, clip_circle });
 620             }
 621         }
 622     };
 623 
 624     // Top-Left
 625     if r_tl > 0.1 {
 626         push_corner(out, x + r_tl, y + r_tl, r_tl, std::f32::consts::PI, 1.5 * std::f32::consts::PI);
 627         if mid_x0 > r_tl {
 628             push_quad(out, x + r_tl, y, mid_x0 - r_tl, r_tl);
 629         }
 630     }
 631 
 632     // Top-Right
 633     if r_tr > 0.1 {
 634         push_corner(out, x + ww - r_tr, y + r_tr, r_tr, 1.5 * std::f32::consts::PI, 2.0 * std::f32::consts::PI);
 635         if ww - mid_x1 > r_tr {
 636             push_quad(out, x + mid_x1, y, ww - mid_x1 - r_tr, r_tr);
 637         }
 638     }
 639 
 640     // Bottom-Right
 641     if r_br > 0.1 {
 642         push_corner(out, x + ww - r_br, y + h - r_br, r_br, 0.0, 0.5 * std::f32::consts::PI);
 643         if ww - mid_x1 > r_br {
 644             push_quad(out, x + mid_x1, y + h - r_br, ww - mid_x1 - r_br, r_br);
 645         }
 646     }
 647 
 648     // Bottom-Left
 649     if r_bl > 0.1 {
 650         push_corner(out, x + r_bl, y + h - r_bl, r_bl, 0.5 * std::f32::consts::PI, std::f32::consts::PI);
 651         if mid_x0 > r_bl {
 652             push_quad(out, x + r_bl, y + h - r_bl, mid_x0 - r_bl, r_bl);
 653         }
 654     }
 655 }
 656 
 657 pub fn rounded_rect_vertices(
 658     x: f32, y: f32, ww: f32, h: f32,
 659     r: f32,
 660     sw: f32, sh: f32,
 661     color: [f32; 4],
 662     clip_circle: [f32; 3],
 663 ) -> Vec<Vertex> {
 664     let mut verts = Vec::new();
 665     push_rounded_rect_vertices_corners(x, y, ww, h, crate::widget::CornerRadii::uniform(r), sw, sh, color, clip_circle, None, &mut verts);
 666     verts
 667 }
 668 
 669 pub fn push_rounded_rect_vertices(
 670     x: f32, y: f32, ww: f32, h: f32,
 671     r: f32,
 672     sw: f32, sh: f32,
 673     color: [f32; 4],
 674     clip_circle: [f32; 3],
 675     out: &mut Vec<Vertex>,
 676 ) {
 677     push_rounded_rect_vertices_corners(x, y, ww, h, crate::widget::CornerRadii::uniform(r), sw, sh, color, clip_circle, None, out);
 678 }
 679 
 680 pub fn plate_bevel_vertices(
 681     x: f32, y: f32, ww: f32, h: f32,
 682     r: f32,
 683     t: f32,
 684     sw: f32, sh: f32,
 685     base_color: [f32; 4],
 686     clip_circle: [f32; 3],
 687 ) -> Vec<Vertex> {
 688     let mut verts = Vec::new();
 689     push_plate_bevel_vertices(x, y, ww, h, r, t, sw, sh, base_color, clip_circle, &mut verts);
 690     verts
 691 }
 692 
 693 pub fn push_plate_bevel_vertices(
 694     x: f32, y: f32, ww: f32, h: f32,
 695     r: f32,
 696     t: f32,
 697     sw: f32, sh: f32,
 698     base_color: [f32; 4],
 699     clip_circle: [f32; 3],
 700     out: &mut Vec<Vertex>,
 701 ) {
 702     push_bevel_edge_vertices(x, y, ww, h, r, t, sw, sh, base_color, clip_circle, 1.0, out);
 703 }
 704 
 705 /// The bevel edge shading, with the light direction selectable: `light_sign` is `1.0`
 706 /// for a raised plate (edges facing `light_source_position` are lit) and `-1.0` for a
 707 /// recess (those same edges fall into shadow instead, and the far edges catch the
 708 /// light). Negating the whole light vector flips every edge and every corner segment
 709 /// consistently, because both the flat-edge factors and the arc-normal dot product
 710 /// below are linear in it.
 711 pub fn push_bevel_edge_vertices(
 712     x: f32, y: f32, ww: f32, h: f32,
 713     r: f32,
 714     t: f32,
 715     sw: f32, sh: f32,
 716     base_color: [f32; 4],
 717     clip_circle: [f32; 3],
 718     light_sign: f32,
 719     out: &mut Vec<Vertex>,
 720 ) {
 721     push_bevel_edge_vertices_radii(
 722         x, y, ww, h, (r, r, r, r), t, sw, sh, base_color, clip_circle, light_sign, out,
 723     );
 724 }
 725 
 726 /// As [`push_bevel_edge_vertices`], but with a per-corner radius (TL, TR, BR, BL) so the
 727 /// lip can follow a shape whose corners differ — a recess carved along the top of a
 728 /// rounded plate needs the plate's radius on its top corners and square ones where it
 729 /// meets the content below. A uniform radius there would either square off the plate's
 730 /// arc (painting a notch outside it) or wrongly round the inner corners.
 731 pub fn push_bevel_edge_vertices_radii(
 732     x: f32, y: f32, ww: f32, h: f32,
 733     radii: (f32, f32, f32, f32),
 734     t: f32,
 735     sw: f32, sh: f32,
 736     base_color: [f32; 4],
 737     clip_circle: [f32; 3],
 738     light_sign: f32,
 739     out: &mut Vec<Vertex>,
 740 ) {
 741     push_bevel_edge_vertices_banded(
 742         x, y, ww, h, radii, t, sw, sh, base_color, clip_circle, light_sign,
 743         default_bevel_bands(t), (true, true, true, true), EdgeKind::Rim, out,
 744     );
 745 }
 746 
 747 /// What kind of height change an edge represents. The two shade differently because they
 748 /// are different shapes, and using one where the other belongs is what makes a bevel read
 749 /// as a drawn line instead of a surface.
 750 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
 751 pub enum EdgeKind {
 752     /// The surface *ends* here: a quarter-round rolling from face-on at the inner edge of
 753     /// the lip to fully in-plane at the outer boundary, where it drops away. The shading
 754     /// therefore peaks exactly at the boundary and dies inward. This is a plate's outer
 755     /// perimeter.
 756     Rim,
 757     /// The surface *continues* at a different height: one plateau steps down to another.
 758     /// A height field that falls monotonically across the transition has its normal tilted
 759     /// toward the low side the whole way, steepest in the middle and flat at both ends —
 760     /// so the shading is a bump straddling the boundary, not a band butted against it.
 761     /// Hanging the band on one side instead leaves the seam the eye reads as a drawn line.
 762     Step,
 763 }
 764 
 765 /// Shading across an edge at signed distance `d` from the boundary (positive = toward the
 766 /// shape's interior), for a transition of width `t`. Returns the light term as a fraction
 767 /// of full tilt.
 768 #[inline]
 769 fn bevel_profile(kind: EdgeKind, d: f32, t: f32) -> f32 {
 770     if t <= 0.0 {
 771         return 0.0;
 772     }
 773     match kind {
 774         // Normal rotates from in-plane (d = 0) to face-on (d = t): sine of what tilt is
 775         // left. A linear ramp here reads as a flat 45° chamfer instead of a roll.
 776         EdgeKind::Rim => ((1.0 - (d / t).clamp(0.0, 1.0)) * std::f32::consts::FRAC_PI_2).sin(),
 777         // Symmetric bump over [-t/2, +t/2], zero at both ends so the transition blends into
 778         // both plateaus with no seam.
 779         EdgeKind::Step => {
 780             let s = (d / t + 0.5).clamp(0.0, 1.0);
 781             (s * std::f32::consts::PI).sin()
 782         }
 783     }
 784 }
 785 
 786 /// The light-independent curvature term at signed distance `d` — the second depth cue,
 787 /// on top of the directional one. Curvature shading is what ambient light does: convex
 788 /// surface catches it from everywhere (bright), concave is self-occluded (dark). Because
 789 /// it does not rotate with the light, it survives exactly where the directional term
 790 /// dies — walls parallel to the light vector — so no edge ever vanishes entirely.
 791 ///
 792 /// `high_sign` is +1 when the rect interior is the HIGH side of the transition and -1
 793 /// when it is the low side (a recess). Geometry, not lighting: it does not flip with
 794 /// `light_sign`... except that for these 2.5D shapes the two are the same number, since
 795 /// a raised shape is lit like a plateau and shaded like one.
 796 #[inline]
 797 fn bevel_curvature(kind: EdgeKind, d: f32, t: f32, high_sign: f32) -> f32 {
 798     if t <= 0.0 {
 799         return 0.0;
 800     }
 801     match kind {
 802         // A rim is convex everywhere, tightest right at the silhouette: a bright crest
 803         // line hugging the boundary and dying fast inward. This is the line that makes
 804         // glass read as glass — the edge catches ambient light all the way around, even
 805         // (dimmer, via the gain asymmetry below) on the side facing away from the light.
 806         EdgeKind::Rim => {
 807             let u = (d / t).clamp(0.0, 1.0);
 808             let f = 1.0 - u;
 809             CREST_RATIO * f * f * f
 810         }
 811         // An S-curve step is convex on its high half (the shoulder) and concave on its
 812         // low half (the fillet, where the wall meets the floor): antisymmetric, zero at
 813         // the ends (no seam against either plateau) and at the midpoint.
 814         EdgeKind::Step => {
 815             let s = (d / t + 0.5).clamp(0.0, 1.0);
 816             let outer_is_high = -high_sign; // d < 0 is outside the rect
 817             // sin(2πs) is positive on the outer half — the shoulder when the outside is
 818             // the high side — and negative on the inner (fillet) half.
 819             AO_RATIO * outer_is_high * (s * std::f32::consts::TAU).sin()
 820         }
 821     }
 822 }
 823 
 824 /// Crest amplitude as a fraction of `bevel_depth` — how much brighter a rim's silhouette
 825 /// line is than flat surface under even light. Must stay clearly below ~0.7 (the
 826 /// projection of a 135° light onto an axis edge), or it cancels the directional shadow
 827 /// on the dark side and the rim goes flat there instead of showing a faint bright line
 828 /// over a shadowed roll.
 829 const CREST_RATIO: f32 = 0.4;
 830 /// Shoulder/fillet amplitude as a fraction of `bevel_depth`.
 831 const AO_RATIO: f32 = 0.6;
 832 /// Per-sign overlay gains. These are asymmetric the opposite way from intuition: on the
 833 /// dark bases this DE runs, white-over blending (`b + a(1-b)`) moves the pixel far more
 834 /// per unit alpha than black-over (`b(1-a)`) — a dark surface has little brightness for
 835 /// black to take away. The old subtractive shading effectively crushed shadow sides to
 836 /// black in linear space; the black overlay needs a high gain to keep shadows reading
 837 /// at all, while white needs damping to keep highlights from blowing out.
 838 const LIGHT_GAIN: f32 = 0.7;
 839 const DARK_GAIN: f32 = 3.0;
 840 
 841 /// A shading value (already scaled by `bevel_depth`) as the two overlay passes: the lit
 842 /// pass is translucent white, the shadow pass translucent black. Painting the
 843 /// *modulation* instead of a resolved surface color is what lets relief primitives compose — a step
 844 /// crossing a rim shades the rim's gradient instead of stamping a flat band over it, a
 845 /// lip on a translucent plate no longer doubles its opacity, and a recess needs no
 846 /// knowledge of the surface color it carves.
 847 ///
 848 /// Why two passes with fixed RGB rather than one signed color: a primitive whose value
 849 /// crosses zero inside a band would interpolate white→black through mid-gray at
 850 /// non-negligible alpha — on a dark base a *brightening* artifact right where the
 851 /// shading should vanish. With per-pass alphas clamped at the crossing, each pass fades
 852 /// to zero there and the hue can never be wrong. Alphas also stay non-negative on every
 853 /// vertex, which the renderer requires (negative alpha is the blur sentinel).
 854 #[inline]
 855 fn overlay_light(v: f32) -> [f32; 4] {
 856     [1.0, 1.0, 1.0, (v.max(0.0) * LIGHT_GAIN).min(1.0)]
 857 }
 858 #[inline]
 859 fn overlay_dark(v: f32) -> [f32; 4] {
 860     [0.0, 0.0, 0.0, ((-v).max(0.0) * DARK_GAIN).min(1.0)]
 861 }
 862 
 863 /// The signed distance range an edge's shading occupies, relative to the boundary.
 864 #[inline]
 865 fn bevel_span(kind: EdgeKind, t: f32) -> (f32, f32) {
 866     match kind {
 867         EdgeKind::Rim => (0.0, t),
 868         EdgeKind::Step => (-0.5 * t, 0.5 * t),
 869     }
 870 }
 871 
 872 /// How many gradient bands to slice a lip of thickness `t` into. Vertex colors interpolate
 873 /// linearly, so each band is a chord of the shading curve; one band per ~1.25px keeps the
 874 /// error under a shade step without emitting geometry finer than the display resolves.
 875 /// The cap rose with the curvature term: a step now has two features across its width
 876 /// (shoulder and fillet), so it needs double the samples a single bump did.
 877 fn default_bevel_bands(t: f32) -> usize {
 878     ((t / 1.25).ceil() as usize).clamp(1, 12)
 879 }
 880 
 881 /// As [`push_bevel_edge_vertices_radii`], with the band count forced and the walls
 882 /// selectable — for callers that want a coarser or finer roll-off than thickness alone
 883 /// implies, or that are shading a step rather than a closed shape.
 884 ///
 885 /// `edges` is (top, right, bottom, left). Suppressing a wall matters for a region that
 886 /// runs flush to the surface's own edge: a full-width menubar sunk into the top of a plate
 887 /// is a *plateau one step down*, not a trough, so its only real wall is the one facing the
 888 /// content. Drawing the other three would carve a lip along the plate's outer edge, where
 889 /// the plate's own roll already lives, and the two would fight.
 890 pub fn push_bevel_edge_vertices_banded(
 891     x: f32, y: f32, ww: f32, h: f32,
 892     radii: (f32, f32, f32, f32),
 893     t: f32,
 894     sw: f32, sh: f32,
 895     base_color: [f32; 4],
 896     clip_circle: [f32; 3],
 897     light_sign: f32,
 898     bands: usize,
 899     edges: (bool, bool, bool, bool),
 900     kind: EdgeKind,
 901     out: &mut Vec<Vertex>,
 902 ) {
 903     // Floored for the same reason as `plate_push_raised`'s cap: a negative
 904     // extent must degrade to no ring, not panic in `clamp`.
 905     let cap = (ww.min(h) * 0.5).max(0.0);
 906     let (tl, tr, br, bl) = (
 907         radii.0.clamp(0.0, cap),
 908         radii.1.clamp(0.0, cap),
 909         radii.2.clamp(0.0, cap),
 910         radii.3.clamp(0.0, cap),
 911     );
 912     let t = t.clamp(0.0, cap);
 913     if t <= 0.0 {
 914         return;
 915     }
 916     let bands = bands.max(1);
 917 
 918     let rad = crate::layout::light_source_position();
 919     let lx = rad.cos() * light_sign;
 920     let ly = -rad.sin() * light_sign;
 921     let depth = crate::layout::bevel_depth();
 922 
 923     // `base_color` is no longer painted: shading is an overlay (see `overlay_color`), so
 924     // the surface below shows through with its own gradients and translucency intact.
 925     let _ = base_color;
 926     // Shading (directional + curvature, scaled by bevel_depth) at signed distance `d`,
 927     // for an edge whose outward flat normal is `dir`. A `Step` band runs negative — it
 928     // straddles the boundary into the plateau outside the rect, which is exactly what
 929     // removes the seam.
 930     let value = |dot: f32, d: f32| {
 931         depth * (bevel_profile(kind, d, t) * dot + bevel_curvature(kind, d, t, light_sign))
 932     };
 933     // The (up to two) overlay color pairs for a band running from value `v0` to `v1`:
 934     // one white pair and/or one black pair, each pass fading to zero alpha wherever the
 935     // value has the other sign. Both fire only when the band straddles the terminator.
 936     let passes = |v0: f32, v1: f32| -> [Option<([f32; 4], [f32; 4])>; 2] {
 937         [
 938             (v0 > 0.0 || v1 > 0.0).then(|| (overlay_light(v0), overlay_light(v1))),
 939             (v0 < 0.0 || v1 < 0.0).then(|| (overlay_dark(v0), overlay_dark(v1))),
 940         ]
 941     };
 942     let (span_lo, span_hi) = bevel_span(kind, t);
 943 
 944     // Each flat edge spans between its two adjoining corner radii, not a single uniform
 945     // inset — that is what lets the corners differ. At a square corner there is no arc to
 946     // cover the t×t patch where two edges meet, so the horizontal edges claim it (they run
 947     // the full span) and the vertical ones inset by `t`; overlapping them instead would
 948     // double-blend that patch, which shows as a dark notch on a translucent surface.
 949     let (left_top, left_bot) = (if tl > 0.0 { tl } else { t }, if bl > 0.0 { bl } else { t });
 950     let (right_top, right_bot) = (if tr > 0.0 { tr } else { t }, if br > 0.0 { br } else { t });
 951     let top_w = ww - tl - tr;
 952     let bottom_w = ww - bl - br;
 953     let left_h = h - left_top - left_bot;
 954     let right_h = h - right_top - right_bot;
 955 
 956     for k in 0..bands {
 957         let d0 = span_lo + (span_hi - span_lo) * (k as f32 / bands as f32);
 958         let d1 = span_lo + (span_hi - span_lo) * ((k + 1) as f32 / bands as f32);
 959         let bw = d1 - d0;
 960 
 961         // Top: outward normal (0,-1); the gradient runs downward, into the surface.
 962         if top_w > 0.0 && edges.0 {
 963             let (v0, v1) = (value(-ly, d0), value(-ly, d1));
 964             for (c0, c1) in passes(v0, v1).into_iter().flatten() {
 965                 out.extend_from_slice(&quad_vertices_shaded(
 966                     x + tl, y + d0, top_w, bw, sw, sh, c0, c0, c1, c1, clip_circle,
 967                 ));
 968             }
 969         }
 970         // Bottom: outward normal (0,1); gradient runs upward.
 971         if bottom_w > 0.0 && edges.2 {
 972             let (v0, v1) = (value(ly, d0), value(ly, d1));
 973             for (c0, c1) in passes(v0, v1).into_iter().flatten() {
 974                 out.extend_from_slice(&quad_vertices_shaded(
 975                     x + bl, y + h - d1, bottom_w, bw, sw, sh, c1, c1, c0, c0, clip_circle,
 976                 ));
 977             }
 978         }
 979         // Left: outward normal (-1,0); gradient runs rightward.
 980         if left_h > 0.0 && edges.3 {
 981             let (v0, v1) = (value(-lx, d0), value(-lx, d1));
 982             for (c0, c1) in passes(v0, v1).into_iter().flatten() {
 983                 out.extend_from_slice(&quad_vertices_shaded(
 984                     x + d0, y + left_top, bw, left_h, sw, sh, c0, c1, c1, c0, clip_circle,
 985                 ));
 986             }
 987         }
 988         // Right: outward normal (1,0); gradient runs leftward.
 989         if right_h > 0.0 && edges.1 {
 990             let (v0, v1) = (value(lx, d0), value(lx, d1));
 991             for (c0, c1) in passes(v0, v1).into_iter().flatten() {
 992                 out.extend_from_slice(&quad_vertices_shaded(
 993                     x + ww - d1, y + right_top, bw, right_h, sw, sh, c1, c0, c0, c1, clip_circle,
 994                 ));
 995             }
 996         }
 997     }
 998 
 999     // A corner arc belongs to both of its adjoining walls, so it is drawn only when both
1000     // are — otherwise a suppressed wall would still get a quarter of a lip.
1001     let corners = [
1002         (x + tl, y + tl, tl, std::f32::consts::PI, 1.5 * std::f32::consts::PI, edges.0 && edges.3), // Top-Left
1003         (x + ww - tr, y + tr, tr, 1.5 * std::f32::consts::PI, 2.0 * std::f32::consts::PI, edges.0 && edges.1), // Top-Right
1004         (x + ww - br, y + h - br, br, 0.0, 0.5 * std::f32::consts::PI, edges.2 && edges.1), // Bottom-Right
1005         (x + bl, y + h - bl, bl, 0.5 * std::f32::consts::PI, std::f32::consts::PI, edges.2 && edges.3), // Bottom-Left
1006     ];
1007 
1008     for &(cx, cy, r, start_angle, end_angle, enabled) in &corners {
1009         // A square corner has no arc to sweep — the flat edges already met there.
1010         if r <= 0.0 || !enabled {
1011             continue;
1012         }
1013         // The corner is a quarter of a torus: shading varies along the sweep (the normal
1014         // swings through 90° of the light) *and* across the lip (the roll-off). Both come
1015         // out of the vertex colors, so one quad per (segment × band) cell is enough — no
1016         // faceting, unlike the 16 flat wedges this replaced.
1017         let segments = ((r * 0.75) as usize).clamp(8, 48);
1018         let ct = t.min(r);
1019         for j in 0..segments {
1020             let theta0 = start_angle + (j as f32) * (end_angle - start_angle) / (segments as f32);
1021             let theta1 = start_angle + ((j + 1) as f32) * (end_angle - start_angle) / (segments as f32);
1022             let (cos0, sin0) = (theta0.cos(), theta0.sin());
1023             let (cos1, sin1) = (theta1.cos(), theta1.sin());
1024             for k in 0..bands {
1025                 let d0 = span_lo + (span_hi - span_lo) * (k as f32 / bands as f32);
1026                 let d1 = span_lo + (span_hi - span_lo) * ((k + 1) as f32 / bands as f32);
1027                 // Inward along the corner's radius is the same signed distance as inward
1028                 // from a flat edge, so the arc scales the span the same way.
1029                 let (r0, r1) = (r - ct * (d0 / t), r - ct * (d1 / t));
1030                 let p = |rho: f32, c: f32, s: f32| -> [f32; 2] {
1031                     [
1032                         ((cx + rho * c) / sw) * 2.0 - 1.0,
1033                         1.0 - ((cy + rho * s) / sh) * 2.0,
1034                     ]
1035                 };
1036                 // Outer/inner × the two sweep ends; each vertex gets its own value, and
1037                 // the cell is drawn once per overlay pass that has any coverage.
1038                 let vals = [
1039                     value(cos0 * lx + sin0 * ly, d0),
1040                     value(cos1 * lx + sin1 * ly, d0),
1041                     value(cos1 * lx + sin1 * ly, d1),
1042                     value(cos0 * lx + sin0 * ly, d1),
1043                 ];
1044                 let geo = [
1045                     p(r0, cos0, sin0),
1046                     p(r0, cos1, sin1),
1047                     p(r1, cos1, sin1),
1048                     p(r1, cos0, sin0),
1049                 ];
1050                 let mut cells: [Option<fn(f32) -> [f32; 4]>; 2] = [None, None];
1051                 if vals.iter().any(|&v| v > 0.0) {
1052                     cells[0] = Some(overlay_light);
1053                 }
1054                 if vals.iter().any(|&v| v < 0.0) {
1055                     cells[1] = Some(overlay_dark);
1056                 }
1057                 for f in cells.into_iter().flatten() {
1058                     let c: Vec<Vertex> = (0..4)
1059                         .map(|i| Vertex { position: geo[i], color: f(vals[i]), clip_circle })
1060                         .collect();
1061                     out.extend_from_slice(&[c[0], c[1], c[2], c[0], c[2], c[3]]);
1062                 }
1063             }
1064         }
1065     }
1066 }
1067 
1068 /// How strong the face gradient is, as a fraction of `bevel_depth` at the corner nearest
1069 /// the light. Deliberately well below the edge amplitude: the face is a plane, not a
1070 /// roll — it only *leans* toward the light.
1071 const FACE_RATIO: f32 = 0.35;
1072 
1073 /// The face lighting of a plate: a single diagonal luminance gradient across the whole
1074 /// surface, brightest at the corner facing `light_source_position` and darkest at the
1075 /// opposite one. This is the difference between an object and a sticker: a real surface
1076 /// under directional light is never uniform, and a perfectly flat fill makes the eye
1077 /// read the (much smaller) edge shading as frame decoration rather than shape.
1078 ///
1079 /// Emitted as the same two-pass white/black overlays as the relief primitives (see
1080 /// [`overlay_light`]/[`overlay_dark`]): fixed RGB per pass, per-corner alphas clamped at
1081 /// the terminator, bilinear across the quad. The quad is square — its corners poke past
1082 /// a rounded plate's arcs — but the compositor clips the window surface to the same
1083 /// radius, so the overhang never reaches the screen.
1084 pub fn push_plate_face_vertices(
1085     x: f32, y: f32, ww: f32, h: f32,
1086     sw: f32, sh: f32,
1087     clip_circle: [f32; 3],
1088     out: &mut Vec<Vertex>,
1089 ) {
1090     let rad = crate::layout::light_source_position();
1091     let (lx, ly) = (rad.cos(), -rad.sin());
1092     let amp = crate::layout::bevel_depth() * FACE_RATIO;
1093     // Corner value = how much its outward diagonal faces the light.
1094     let inv = std::f32::consts::FRAC_1_SQRT_2;
1095     let v_tl = amp * inv * (-lx - ly);
1096     let v_tr = amp * inv * (lx - ly);
1097     let v_br = amp * inv * (lx + ly);
1098     let v_bl = amp * inv * (-lx + ly);
1099     let vs = [v_tl, v_tr, v_br, v_bl];
1100     if vs.iter().any(|&v| v > 0.0) {
1101         out.extend_from_slice(&quad_vertices_shaded(
1102             x, y, ww, h, sw, sh,
1103             overlay_light(v_tl), overlay_light(v_tr), overlay_light(v_br), overlay_light(v_bl),
1104             clip_circle,
1105         ));
1106     }
1107     if vs.iter().any(|&v| v < 0.0) {
1108         out.extend_from_slice(&quad_vertices_shaded(
1109             x, y, ww, h, sw, sh,
1110             overlay_dark(v_tl), overlay_dark(v_tr), overlay_dark(v_br), overlay_dark(v_bl),
1111             clip_circle,
1112         ));
1113     }
1114 }
1115 
1116 pub fn push_plate_solid_border_vertices(
1117     x: f32, y: f32, ww: f32, h: f32,
1118     radii: crate::widget::CornerRadii,
1119     t: f32,
1120     sw: f32, sh: f32,
1121     color: [f32; 4],
1122     clip_circle: [f32; 3],
1123     out: &mut Vec<Vertex>,
1124 ) {
1125     let mut r_tl = radii.top_left.max(0.0);
1126     let mut r_tr = radii.top_right.max(0.0);
1127     let mut r_br = radii.bottom_right.max(0.0);
1128     let mut r_bl = radii.bottom_left.max(0.0);
1129 
1130     // Simple scale clamping
1131     let sum_top = r_tl + r_tr;
1132     if sum_top > ww {
1133         let f = ww / sum_top;
1134         r_tl *= f;
1135         r_tr *= f;
1136     }
1137     let sum_bottom = r_bl + r_br;
1138     if sum_bottom > ww {
1139         let f = ww / sum_bottom;
1140         r_bl *= f;
1141         r_br *= f;
1142     }
1143     let sum_left = r_tl + r_bl;
1144     if sum_left > h {
1145         let f = h / sum_left;
1146         r_tl *= f;
1147         r_bl *= f;
1148     }
1149     let sum_right = r_tr + r_br;
1150     if sum_right > h {
1151         let f = h / sum_right;
1152         r_tr *= f;
1153         r_br *= f;
1154     }
1155 
1156     out.extend_from_slice(&quad_vertices_with_clip(x + r_tl, y, ww - r_tl - r_tr, t, sw, sh, color, clip_circle));
1157     out.extend_from_slice(&quad_vertices_with_clip(x, y + r_tl, t, h - r_tl - r_bl, sw, sh, color, clip_circle));
1158     out.extend_from_slice(&quad_vertices_with_clip(x + r_bl, y + h - t, ww - r_bl - r_br, t, sw, sh, color, clip_circle));
1159     out.extend_from_slice(&quad_vertices_with_clip(x + ww - t, y + r_tr, t, h - r_tr - r_br, sw, sh, color, clip_circle));
1160 
1161     let segments = 16;
1162     let corner_e = 2.0 / crate::layout::corner_shape();
1163 
1164     // Corner strokes as annulus strips between the outer superellipse (radius
1165     // r) and its inner scaled copy (r - t): at 1px thickness the scaled inner
1166     // curve is indistinguishable from the true parallel curve, and at
1167     // corner_shape 2 this is exactly the circular arc annulus. NOT
1168     // push_arc_background_vertices — that stays circular for genuine arcs.
1169     //
1170     // Both edges of the annulus are FEATHERED, like the fill fan's corners
1171     // (push_rounded_rect_vertices_corners) and push_feathered_line_vertices:
1172     // each fades over `f` either side of its true curve, so the 50%-coverage
1173     // lines stay on the exact silhouette and the stroke reads at the same
1174     // weight, but the arc anti-aliases instead of rasterizing a staircase
1175     // beside the SDF-smooth face it outlines. The straight edges stay crisp
1176     // quads (a pixel-snapped hairline would only blur). A corner too tight to
1177     // fit the inner fade keeps the hard annulus.
1178     let f = 0.5f32.min(t * 0.25);
1179     let fade = [color[0], color[1], color[2], 0.0];
1180     let corner = |cx: f32, cy: f32, r: f32, start: f32, end: f32, out: &mut Vec<Vertex>| {
1181         let r_in = (r - t).max(0.0);
1182         // (inner radius, outer radius, inner alpha colour, outer alpha colour)
1183         let bands: &[(f32, f32, [f32; 4], [f32; 4])] = if r_in - f > 0.0 {
1184             &[
1185                 (r_in - f, r_in + f, fade, color),
1186                 (r_in + f, r - f, color, color),
1187                 (r - f, r + f, color, fade),
1188             ]
1189         } else {
1190             &[(r_in, r, color, color)]
1191         };
1192         let ndc = |px: f32, py: f32| [(px / sw) * 2.0 - 1.0, 1.0 - (py / sh) * 2.0];
1193         for i in 0..segments {
1194             let t1 = start + (i as f32) * (end - start) / segments as f32;
1195             let t2 = start + ((i + 1) as f32) * (end - start) / segments as f32;
1196             let (c1, s1) = superellipse_pt(t1, corner_e);
1197             let (c2, s2) = superellipse_pt(t2, corner_e);
1198             for &(ra, rb, ca, cb) in bands {
1199                 if rb - ra <= 0.0 {
1200                     continue;
1201                 }
1202                 let o1 = ndc(cx + rb * c1, cy + rb * s1);
1203                 let o2 = ndc(cx + rb * c2, cy + rb * s2);
1204                 let i1 = ndc(cx + ra * c1, cy + ra * s1);
1205                 let i2 = ndc(cx + ra * c2, cy + ra * s2);
1206                 out.push(Vertex { position: o1, color: cb, clip_circle });
1207                 out.push(Vertex { position: o2, color: cb, clip_circle });
1208                 out.push(Vertex { position: i1, color: ca, clip_circle });
1209                 out.push(Vertex { position: o2, color: cb, clip_circle });
1210                 out.push(Vertex { position: i2, color: ca, clip_circle });
1211                 out.push(Vertex { position: i1, color: ca, clip_circle });
1212             }
1213         }
1214     };
1215 
1216     if r_tl > 0.1 {
1217         corner(x + r_tl, y + r_tl, r_tl, std::f32::consts::PI, 1.5 * std::f32::consts::PI, out);
1218     }
1219     if r_tr > 0.1 {
1220         corner(x + ww - r_tr, y + r_tr, r_tr, 1.5 * std::f32::consts::PI, 2.0 * std::f32::consts::PI, out);
1221     }
1222     if r_br > 0.1 {
1223         corner(x + ww - r_br, y + h - r_br, r_br, 0.0, 0.5 * std::f32::consts::PI, out);
1224     }
1225     if r_bl > 0.1 {
1226         corner(x + r_bl, y + h - r_bl, r_bl, 0.5 * std::f32::consts::PI, std::f32::consts::PI, out);
1227     }
1228 }
1229 
1230 pub fn push_plate_solid_border_vertices_legacy(
1231     x: f32, y: f32, ww: f32, h: f32,
1232     r: f32,
1233     t: f32,
1234     sw: f32, sh: f32,
1235     color: [f32; 4],
1236     clip_circle: [f32; 3],
1237     out: &mut Vec<Vertex>,
1238 ) {
1239     let radii = crate::widget::CornerRadii::uniform(r);
1240     push_plate_solid_border_vertices(x, y, ww, h, radii, t, sw, sh, color, clip_circle, out);
1241 }
1242 
1243 /// A contiguous run of vertices sharing one scissor rect (Phase 3 single paint path) and one
1244 /// rounded-rect clip. `scissor` is a logical-pixel clip (`None` = unclipped); `clip_rrect` is
1245 /// the paint walk's `[cx, cy, bx, by, r]` rounded clip in logical px (`None` = unclipped),
1246 /// applied as per-draw push-constant state; `start..end` indexes the flat vertex buffer.
1247 pub struct DlBatch {
1248     pub scissor: Option<crate::scene::layout::Rect>,
1249     pub clip_rrect: Option<[f32; 5]>,
1250     pub start: u32,
1251     pub end: u32,
1252     /// When set, this batch is one SDF-lit plate cover quad (see
1253     /// [`crate::draw::PlatePush`]; already in physical px). Never merged.
1254     pub plate: Option<crate::draw::PlatePush>,
1255     /// A blur-behind plate (negative-alpha color): before drawing this batch
1256     /// the renderer snapshots the swapchain-so-far into its snapshot image, so
1257     /// the blur samples everything painted beneath the plate — not just the 3D
1258     /// scene backdrop. Never merged.
1259     pub blur_behind: bool,
1260 }
1261 
1262 /// An image draw from the display list: `at` is the vertex index it sorts
1263 /// before (its position in the tessellated stream); `clip` is the item's
1264 /// paint-walk clip. Logical coordinates throughout.
1265 pub struct DlImage {
1266     pub image: u32,
1267     pub rect: crate::scene::layout::Rect,
1268     pub alpha: f32,
1269     pub at: u32,
1270     pub clip: Option<crate::scene::layout::Rect>,
1271 }
1272 
1273 /// Tessellate a `scene::paint::DisplayList`'s geometry into a flat vertex buffer plus per-clip draw
1274 /// batches, reusing the same tessellators as the legacy path so vertices are identical. `Text`
1275 /// prims are skipped here — text is still rendered via the app's `text_areas()` path. `sw`/`sh` are
1276 /// logical surface dimensions (as everywhere else); `scale` is the HiDPI factor, needed because an
1277 /// item's circular clip rides the vertices in PHYSICAL pixels. Consecutive prims sharing a clip are
1278 /// merged into one batch (the circle clip is per-vertex, so it never splits batches).
1279 /// `CCE_PLATE_DEBUG=1` — trace which carves group into their host plate as exact
1280 /// CSG features and which fall back to the standalone overlay shading.
1281 ///
1282 /// The two paths do NOT look the same: a grouped carve is part of the plate's
1283 /// single height field, so its wall meets the plate's rolled perimeter as a real
1284 /// junction, while the fallback approximates that with the host-box fade. Six
1285 /// conditions decide it, three of them dynamic (draw order, neighbouring plates,
1286 /// whether another carve already claimed the host's feature run), so the SAME
1287 /// widget can render either way depending on what is around it — and it does so
1288 /// silently. That has already shipped as a bug once: a hovered button's opaque
1289 /// fill used to sever every later button from the root plate they carve into,
1290 /// which is why `plate_stack` is a stack (see its comment below).
1291 ///
1292 /// Off by default and read once; the classification below runs only when set.
1293 /// Prim discriminant name, for `CCE_PLATE_DEBUG` reporting only.
1294 fn prim_kind(p: &crate::scene::paint::Prim) -> &'static str {
1295     use crate::scene::paint::Prim as P;
1296     match p {
1297         P::Quad { .. } => "Quad", P::RoundedRect { .. } => "RoundedRect",
1298         P::Border { .. } => "Border", P::Bevel { .. } => "Bevel",
1299         P::Recess { .. } => "Recess", P::Boss { .. } => "Boss",
1300         P::Ridge { .. } => "Ridge", P::Trough { .. } => "Trough", P::Field { .. } => "Field", P::Plate { .. } => "Plate",
1301         P::Arc { .. } => "Arc", P::ArcShaded { .. } => "ArcShaded",
1302         P::Vector { .. } => "Vector", P::Circle { .. } => "Circle",
1303         P::Sphere { .. } => "Sphere", P::Droplet { .. } => "Droplet",
1304         P::DropletScrim { .. } => "DropletScrim", P::Frame { .. } => "Frame",
1305         P::ConcaveFillet { .. } => "ConcaveFillet",
1306         P::Groove { .. } => "Groove", P::Lattice { .. } => "Lattice", P::Grout { .. } => "Grout", P::Fill { .. } => "Fill",
1307         P::CarveUnion { .. } => "CarveUnion", P::Glow { .. } => "Glow",
1308         P::Text { .. } => "Text", P::Image { .. } => "Image",
1309     }
1310 }
1311 
1312 fn plate_debug() -> bool {
1313     static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1314     *ON.get_or_init(|| std::env::var("CCE_PLATE_DEBUG").is_ok_and(|v| v != "0"))
1315 }
1316 
1317 /// Debug builds make one kind of fallback LOUD without `CCE_PLATE_DEBUG`: a
1318 /// carve that could group (full ring, untinted) failing to while a still-open
1319 /// plate encloses it and the carve's shaded region reaches that plate's
1320 /// perimeter roll. There the grouped and overlay paths shade the junction
1321 /// differently, and the rejection is one of the dynamic rules — so the SAME
1322 /// widget can flip looks frame to frame with nothing on stderr. Not an
1323 /// assert/panic: every rejection is conservative-CORRECT (the audit that
1324 /// shipped CCE_PLATE_DEBUG found no misgrouping; a later plate overlapping the
1325 /// carve genuinely must be shaded over, not under) — it is the frame-to-frame
1326 /// LOOK that flips, so the right loudness is an unmissable warning, not a
1327 /// crash. The ubiquitous quiet case stays quiet by construction: ordinary
1328 /// geometry closing every grouping window empties `plate_stack`, so no
1329 /// enclosing OPEN plate exists and this never runs — that is draw-order
1330 /// design, not a flip.
1331 ///
1332 /// Returns the dynamic rule to report, or `None` when the fallback is not the
1333 /// loud case. Pure so the classification is unit-testable; `later_plates` are
1334 /// the open plates emitted after the enclosing host.
1335 #[cfg(debug_assertions)]
1336 fn near_roll_fallback_reason(
1337     carve: &crate::scene::layout::Rect,
1338     depth: f32,
1339     host: &crate::scene::layout::Rect,
1340     roll: f32,
1341     later_plates: &[crate::scene::layout::Rect],
1342     budget_full: bool,
1343 ) -> Option<&'static str> {
1344     // The carve's shaded region — the overlay path's cover-quad inflation.
1345     let infl = depth * 0.5 + 2.0;
1346     let (sx0, sy0) = (carve.x - infl, carve.y - infl);
1347     let (sx1, sy1) = (carve.x + carve.width + infl, carve.y + carve.height + infl);
1348     // "Near the roll" = the shaded region leaves the host rect deflated by the
1349     // host's own roll width on any side.
1350     let near = sx0 < host.x + roll
1351         || sy0 < host.y + roll
1352         || sx1 > host.x + host.width - roll
1353         || sy1 > host.y + host.height - roll;
1354     if !near {
1355         return None;
1356     }
1357     // The dynamic rules, in the order the grouping guard tests them.
1358     if budget_full {
1359         return Some("the feature budget is full");
1360     }
1361     if later_plates
1362         .iter()
1363         .any(|o| sx0 < o.x + o.width && sx1 > o.x && sy0 < o.y + o.height && sy1 > o.y)
1364     {
1365         return Some("a later plate overlaps the carve's shaded region");
1366     }
1367     Some("the host's feature run is closed (another plate appended features since)")
1368 }
1369 
1370 /// Print a near-roll fallback warning once per distinct message — a carve in a
1371 /// steady layout would otherwise repeat it every frame.
1372 #[cfg(debug_assertions)]
1373 fn plate_carve_warn_once(msg: String) {
1374     use std::sync::{Mutex, OnceLock};
1375     static SEEN: OnceLock<Mutex<std::collections::HashSet<String>>> = OnceLock::new();
1376     let seen = SEEN.get_or_init(|| Mutex::new(std::collections::HashSet::new()));
1377     if seen.lock().unwrap().insert(msg.clone()) {
1378         eprintln!("{msg}");
1379     }
1380 }
1381 
1382 pub fn tessellate_display_list(
1383     dl: &crate::scene::paint::DisplayList,
1384     sw: f32,
1385     sh: f32,
1386     scale: f32,
1387 ) -> (Vec<Vertex>, Vec<DlBatch>, Vec<DlImage>, Vec<[f32; 12]>) {
1388     use crate::scene::material::PlateRole;
1389     use crate::scene::paint::{Cap, Prim};
1390     let mut verts: Vec<Vertex> = Vec::new();
1391     let mut batches: Vec<DlBatch> = Vec::new();
1392     let mut images: Vec<DlImage> = Vec::new();
1393     // Carves CSG'd into plates (see Frame2D::plate_features), plus the plate
1394     // they group into: the most recent Plate/Bevel batch, provided only Text
1395     // and Image prims (which draw through separate paths anyway) intervene.
1396     let mut features: Vec<[f32; 12]> = Vec::new();
1397     // Open carve-host plates, in emission order (innermost candidates last).
1398     // A STACK, not a single slot: a sibling plate emitted between a root plate
1399     // and its later carves (a hovered button's opaque fill among transparent
1400     // ones) must not sever those carves from the root plate they are carved
1401     // into — that severing rendered every button after the hovered one
1402     // through the visually-different overlay fallback. Ordinary geometry
1403     // still closes every open plate (the draw-order rule below).
1404     let mut plate_stack: Vec<(usize, crate::scene::layout::Rect)> = Vec::new();
1405     // Which plate last appended a carve feature: a plate's features are
1406     // addressed as one contiguous [offset, count] run (PlatePush::host), so a
1407     // plate may only receive MORE features while no other plate has appended
1408     // any since.
1409     let mut last_feature_plate: Option<usize> = None;
1410     // `CCE_PLATE_DEBUG` bookkeeping — see `plate_debug`.
1411     let dbg_plates = plate_debug();
1412     let mut dbg_grouped = 0usize;
1413     let mut dbg_fell_back: Vec<String> = Vec::new();
1414     let mut dbg_opened = 0usize;
1415     // Which prim kind closed a still-open grouping window, and how many plates
1416     // it closed — the answer to "why was there no enclosing plate?".
1417     let mut dbg_closed_by: std::collections::BTreeMap<&'static str, usize> =
1418         std::collections::BTreeMap::new();
1419 
1420     // SDF-lit plate path (shader2d's plate branch) vs the legacy banded vertex
1421     // shading, plus the frame-constant lighting inputs it pushes per plate.
1422     let shader_plates = crate::layout::bevel_shader();
1423     // Light and material come from `scene::relief_shade`, which is also what
1424     // cce-relief predicts pixels with — one definition, so the editor cannot
1425     // draw a different material than the renderer applies.
1426     let plate_light = crate::scene::relief_shade::light_vector();
1427     // [shading strength (1.0 at the default bevel_depth), specular strength,
1428     // shininess, curvature/AO strength] — the DE's finish, for the CARVES,
1429     // which shade whatever is beneath them and so take the host's. A prim
1430     // that carries a Material (Plate, Bevel, Sphere, Droplet) pushes its own
1431     // `material.finish` instead. Curvature is kept near the raised path's
1432     // crest amplitude: the recess shoulder's brightening lands on the same
1433     // pixels as its specular line, and the two stack — at 0.5 the step read
1434     // several times hotter than a plate roll.
1435     let plate_mat = crate::scene::material::Finish::from_style().to_array();
1436 
1437     for item in &dl.items {
1438         let mut start = verts.len() as u32;
1439         let mut plate: Option<crate::draw::PlatePush> = None;
1440         // A frosted flat fill promoted to a zero-depth plate batch (below):
1441         // it carries a recipe like any plate, but it is ordinary geometry to
1442         // the carve grouping — it opens no host and closes the open ones.
1443         let mut promoted = false;
1444         let mut made_plate: Option<crate::scene::layout::Rect> = None;
1445         // Blur-behind marker: a prim whose FILL alpha is negative asks the
1446         // renderer to snapshot the frame-so-far before it draws. Every
1447         // fill-bearing prim counts — the shader's a<0 branch runs for all of
1448         // them, and a variant missing here still frosts, but against the
1449         // stale scene backdrop instead of the frame: a flat tint with no
1450         // content and no blur, which is how the Dropdown popover (Border)
1451         // and the menubar panels (Quad) shipped visibly unfrosted while the
1452         // context menu (Plate) worked.
1453         let mut blur_behind = matches!(
1454             &item.prim,
1455             crate::scene::paint::Prim::Quad { color, .. }
1456             | crate::scene::paint::Prim::RoundedRect { color, .. } if color[3] < 0.0
1457         ) || matches!(
1458             &item.prim,
1459             crate::scene::paint::Prim::Bevel { material, .. }
1460             | crate::scene::paint::Prim::Frame { material, .. }
1461             | crate::scene::paint::Prim::Plate { material, .. }
1462             | crate::scene::paint::Prim::Droplet { material, .. }
1463                 if material.fill(PlateRole::Nested)[3] < 0.0
1464         ) || matches!(
1465             &item.prim,
1466             crate::scene::paint::Prim::Border { fill, .. } if fill[3] < 0.0
1467         ) || matches!(
1468             &item.prim,
1469             crate::scene::paint::Prim::Fill { material, .. } if material.fill(PlateRole::Nested)[3] < 0.0
1470         );
1471         // Logical [cx, cy, r] → the physical-pixel triple the vertex attribute carries.
1472         let no = item
1473             .clip_circle
1474             .map(|c| [c[0] * scale, c[1] * scale, c[2] * scale])
1475             .unwrap_or([0.0f32, 0.0, 0.0]);
1476         // Fixed 16-segment fans read as polygons once a circle/arc is pane-sized; scale
1477         // the fan with the PHYSICAL radius (capped — beyond 128 the chord error is
1478         // subpixel even on HiDPI).
1479         let segs = |radius: f32| -> usize { ((radius * scale) as usize).clamp(16, 128) };
1480         match &item.prim {
1481             Prim::Text { .. } => continue, // text goes through the glyph/text-span path
1482             Prim::Image { image, rect, alpha } => {
1483                 images.push(DlImage {
1484                     image: *image,
1485                     rect: *rect,
1486                     alpha: *alpha,
1487                     at: verts.len() as u32,
1488                     clip: item.clip,
1489                 });
1490                 continue;
1491             }
1492             // A frosted FLAT fill — a `Flat` control face, a menu panel, a
1493             // popover, an inset plate's face — is a zero-depth plate batch
1494             // (RFC material § 6.2): the same shader path as every plate, so
1495             // it carries its own frost recipe instead of a window-wide one,
1496             // with the configured `corner_shape` and no roll, which is what the
1497             // tessellated fill drew. The display list is untouched, so the
1498             // legacy bridges that extract RoundedRects still see one.
1499             Prim::Quad { rect, color } if shader_plates && color[3] < 0.0 => {
1500                 verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, *color));
1501                 plate = Some(flat_frost_push(rect, (0.0, 0.0, 0.0, 0.0), *color, scale, plate_light, plate_mat));
1502                 promoted = true;
1503             }
1504             Prim::RoundedRect { rect, radius, corners, color } if shader_plates && color[3] < 0.0 => {
1505                 let radii = (
1506                     if corners.0 { *radius } else { 0.0 },
1507                     if corners.1 { *radius } else { 0.0 },
1508                     if corners.2 { *radius } else { 0.0 },
1509                     if corners.3 { *radius } else { 0.0 },
1510                 );
1511                 verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, *color));
1512                 plate = Some(flat_frost_push(rect, radii, *color, scale, plate_light, plate_mat));
1513                 promoted = true;
1514             }
1515             Prim::Fill { rect, radii, material } if shader_plates && material.frost.is_frosted() => {
1516                 // A material's flat fill: the frosted promotion above with
1517                 // the MATERIAL's recipe (compression, refraction, radius)
1518                 // instead of the DE default's. Zero depth, the configured
1519                 // corner shape, no host — exactly a promoted RoundedRect.
1520                 let color = material.fill(PlateRole::Nested);
1521                 verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, color));
1522                 let mut p = plate_push_raised(rect, *radii, 0.0, scale, plate_light, plate_mat, false, None);
1523                 let [fz, fw] = material.frost.pack(scale);
1524                 p.host[2] = fz;
1525                 p.host[3] = fw;
1526                 plate = Some(p);
1527                 promoted = true;
1528             }
1529             Prim::Fill { rect, radii, material } => {
1530                 // Opaque (or the legacy path): a plain rounded fill.
1531                 let cr = crate::widget::CornerRadii::new(radii.0, radii.1, radii.2, radii.3);
1532                 push_rounded_rect_vertices_corners(rect.x, rect.y, rect.width, rect.height, cr, sw, sh, material.fill(PlateRole::Nested), no, None, &mut verts);
1533             }
1534             Prim::Border { rect, radii, fill, border, thickness } if shader_plates && fill[3] < 0.0 => {
1535                 // The fill as its own plate batch, closed here; the stroke
1536                 // follows as ordinary geometry in the batch the tail makes.
1537                 verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, *fill));
1538                 let p = flat_frost_push(rect, *radii, *fill, scale, plate_light, plate_mat);
1539                 let end = verts.len() as u32;
1540                 plate_stack.clear();
1541                 batches.push(DlBatch { scissor: item.clip, clip_rrect: item.clip_rrect, start, end, plate: Some(p), blur_behind: true });
1542                 start = end;
1543                 blur_behind = false;
1544                 let cr = crate::widget::CornerRadii::new(radii.0, radii.1, radii.2, radii.3);
1545                 push_plate_solid_border_vertices(rect.x, rect.y, rect.width, rect.height, cr, *thickness, sw, sh, *border, no, &mut verts);
1546             }
1547             Prim::Quad { rect, color } => {
1548                 // Quads honor an active circle clip like circles/arcs do (the
1549                 // Ramp's foam-cell fills draw as clipped strips).
1550                 verts.extend(quad_vertices_with_clip(rect.x, rect.y, rect.width, rect.height, sw, sh, *color, no));
1551             }
1552             Prim::RoundedRect { rect, radius, corners, color } => {
1553                 let radii = crate::widget::CornerRadii::new(
1554                     if corners.0 { *radius } else { 0.0 },
1555                     if corners.1 { *radius } else { 0.0 },
1556                     if corners.2 { *radius } else { 0.0 },
1557                     if corners.3 { *radius } else { 0.0 },
1558                 );
1559                 push_rounded_rect_vertices_corners(rect.x, rect.y, rect.width, rect.height, radii, sw, sh, *color, no, None, &mut verts);
1560             }
1561             Prim::Border { rect, radii, fill, border, thickness } => {
1562                 let cr = crate::widget::CornerRadii::new(radii.0, radii.1, radii.2, radii.3);
1563                 push_rounded_rect_vertices_corners(rect.x, rect.y, rect.width, rect.height, cr, sw, sh, *fill, no, None, &mut verts);
1564                 push_plate_solid_border_vertices(rect.x, rect.y, rect.width, rect.height, cr, *thickness, sw, sh, *border, no, &mut verts);
1565             }
1566             Prim::Glow { rect, radius, reach, color } => {
1567                 push_glow_vertices(rect.x, rect.y, rect.width, rect.height, *radius, *reach, sw, sh, *color, no, &mut verts);
1568             }
1569             Prim::Bevel { rect, radii, material, depth, tint } if shader_plates => {
1570                 let color = material.fill(PlateRole::Nested);
1571                 let mat = material.finish.to_array();
1572                 // SDF-lit raised plate: one cover quad; the shader owns fill,
1573                 // roll shading, corners, and silhouette AA. Nominal corner
1574                 // radii (scale_corners false): a Bevel is a WIDGET-scale plate
1575                 // whose silhouette must match the nominal-radius squircles of
1576                 // the controls around it — only window-scale `Plate`s get the
1577                 // curvature-matched span.
1578                 verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, color));
1579                 let mut p = plate_push_raised(rect, *radii, *depth, scale, plate_light, mat, false, None);
1580                 // The plate's own frost recipe rides host.zw (see PlatePush).
1581                 let [fz, fw] = material.frost.pack(scale);
1582                 p.host[2] = fz;
1583                 p.host[3] = fw;
1584                 // w = 1 marks an accent-tinted plate (the focused-pane
1585                 // treatment): the shader keeps the roll's light and shadow
1586                 // and recolours them — light toward the tint, shadow toward
1587                 // a dark tint — matching the free-carve path's tinted-well
1588                 // convention. Neutral white keeps w = 0 (a no-op multiply).
1589                 let full = if *tint == [1.0, 1.0, 1.0] { 0.0 } else { 1.0 };
1590                 p.specular_tint = [tint[0], tint[1], tint[2], full];
1591                 plate = Some(p);
1592                 made_plate = Some(*rect);
1593             }
1594             Prim::Frame { rect, hole, hole_radii, material, depth } if shader_plates => {
1595                 // The Bevel branch turned inside out (shader MODE_FRAME): the
1596                 // cover quad is the face's bound, the SDF box is the HOLE,
1597                 // and the shader reads the distance outside it as the
1598                 // plate's depth. Nominal corner radii, as a Bevel's. A carve
1599                 // host over `rect`, like any filled plate.
1600                 let color = material.fill(PlateRole::Nested);
1601                 let mat = material.finish.to_array();
1602                 verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, color));
1603                 let mut p = plate_push_raised(hole, *hole_radii, *depth, scale, plate_light, mat, false, None);
1604                 let [fz, fw] = material.frost.pack(scale);
1605                 p.host[2] = fz;
1606                 p.host[3] = fw;
1607                 p.mode = 17.0; // MODE_FRAME
1608                 plate = Some(p);
1609                 made_plate = Some(*rect);
1610             }
1611             Prim::Plate { rect, radii, material, depth, shape } if shader_plates => {
1612                 let color = material.fill(PlateRole::Nested);
1613                 let mat = material.finish.to_array();
1614                 if *depth < 0.0 {
1615                     // Negative depth = fill-less roll overlay (MODE_ROLL): the
1616                     // window-edge roll shading alone, screened over whatever is
1617                     // beneath — for a root plate whose face is not a fill (the
1618                     // designer's 3D canvas). The cover quad carries no color,
1619                     // and the batch is NOT opened as a carve host: an overlay
1620                     // owns no surface for a CSG feature to cut into.
1621                     verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, [0.0; 4]));
1622                     let mut p = plate_push_raised(rect, *radii, -*depth, scale, plate_light, mat, true, *shape);
1623                     p.mode = 11.0; // MODE_ROLL
1624                     plate = Some(p);
1625                 } else {
1626                     // Same lit-plate branch; the cover quad is the exact rect so the
1627                     // silhouette and the compositor's rounded window corners agree.
1628                     verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, color));
1629                     let mut p = plate_push_raised(rect, *radii, *depth, scale, plate_light, mat, true, *shape);
1630                     let [fz, fw] = material.frost.pack(scale);
1631                     p.host[2] = fz;
1632                     p.host[3] = fw;
1633                     plate = Some(p);
1634                     made_plate = Some(*rect);
1635                 }
1636             }
1637             // A sunken well ending in a flush run (see `Prim::Field`): one
1638             // outline, one overlay — never grouped, its profile is not a
1639             // monotonic step. The cover quad inflates by half the wall, as a
1640             // free carve's does; the host-box slot carries the run's two
1641             // ends (physical px), since nothing fades against a host here.
1642             Prim::Field { rect, radii, depth, split, end, tint } if shader_plates => {
1643                 let infl = *depth * 0.5 + 2.0;
1644                 verts.extend(quad_vertices(
1645                     rect.x - infl, rect.y - infl,
1646                     rect.width + 2.0 * infl, rect.height + 2.0 * infl,
1647                     sw, sh, [0.0; 4],
1648                 ));
1649                 let mut p = plate_push_raised(rect, *radii, *depth, scale, plate_light, plate_mat, false, None);
1650                 p.mode = 16.0; // MODE_FIELD
1651                 if let Some(t) = tint {
1652                     p.specular_tint = [t[0], t[1], t[2], 1.0];
1653                 }
1654                 p.host = [*split * scale, *end * scale, 0.0, 0.0];
1655                 plate = Some(p);
1656             }
1657             Prim::Recess { rect, radii, depth, edges, .. }
1658             | Prim::Boss { rect, radii, depth, edges, .. }
1659             | Prim::Ridge { rect, radii, depth, edges }
1660             | Prim::Trough { rect, radii, depth, edges, .. }
1661                 if shader_plates =>
1662             {
1663                 let tint = match &item.prim {
1664                     Prim::Recess { tint, .. } => *tint,
1665                     Prim::Boss { tint, .. } => *tint,
1666                     Prim::Trough { tint, .. } => *tint,
1667                     _ => None,
1668                 };
1669                 // Recess carves down into the surface; Boss raises a plateau out
1670                 // of it (same machinery, depth sign flipped); Ridge is a raised
1671                 // rim straddling the boundary and Trough the sunken valley twin
1672                 // (their own overlay profiles — never grouped, the CSG features
1673                 // only model monotonic steps).
1674                 let mode = match &item.prim {
1675                     Prim::Boss { .. } => 3.0f32,
1676                     Prim::Ridge { .. } => 4.0,
1677                     Prim::Trough { .. } => 9.0,
1678                     _ => 2.0,
1679                 };
1680                 let raised = mode > 2.5;
1681                 // Grouped into the enclosing plate whenever one is live: the
1682                 // carve becomes a CSG feature of that plate's single draw —
1683                 // exact composite shading, real junctions at the plate's rolled
1684                 // perimeter — instead of a shading overlay (the fallback below).
1685                 //
1686                 // Edge-suppressed carves NEVER group: a suppressed wall's rect
1687                 // extends past the carve (below), relying on the overlay cover
1688                 // quad to keep that shading out of the drawn pixels — a clip
1689                 // the plate's whole-surface draw does not have, so grouped it
1690                 // smears the extended walls across the plate. Union pieces
1691                 // (section wells, a spinbox's field and button run) are
1692                 // exactly these.
1693                 // A tinted carve also never groups: a CSG feature is geometry only,
1694                 // so the tint could only land on the whole plate's specular.
1695                 let full_ring = *edges == (true, true, true, true);
1696                 let host_plate = if mode < 3.5 && full_ring && tint.is_none() && features.len() < crate::draw::MAX_PLATE_FEATURES {
1697                     // The carve's shaded region, for the occlusion test below
1698                     // (the overlay path's cover-quad inflation).
1699                     let infl = *depth * 0.5 + 2.0;
1700                     let (sx0, sy0) = (rect.x - infl, rect.y - infl);
1701                     let (sx1, sy1) = (rect.x + rect.width + infl, rect.y + rect.height + infl);
1702                     plate_stack
1703                         .iter()
1704                         .enumerate()
1705                         .rev()
1706                         .find(|(si, (bi, prect))| {
1707                             let inside = rect.x >= prect.x - 0.5
1708                                 && rect.y >= prect.y - 0.5
1709                                 && rect.x + rect.width <= prect.x + prect.width + 0.5
1710                                 && rect.y + rect.height <= prect.y + prect.height + 0.5;
1711                             if !inside {
1712                                 return false;
1713                             }
1714                             // Pixels drawn since this plate (a LATER plate in the
1715                             // stack) must not overlap the carve — its shading would
1716                             // land beneath them in this plate's earlier draw.
1717                             if plate_stack[si + 1..].iter().any(|(_, orect)| {
1718                                 sx0 < orect.x + orect.width
1719                                     && sx1 > orect.x
1720                                     && sy0 < orect.y + orect.height
1721                                     && sy1 > orect.y
1722                             }) {
1723                                 return false;
1724                             }
1725                             // Contiguity: only the last feature-receiving plate (or
1726                             // one with no features yet) may take another.
1727                             batches[*bi].plate.as_ref().is_some_and(|p| p.host[1] == 0.0)
1728                                 || last_feature_plate == Some(*bi)
1729                         })
1730                         .map(|(_, &(bi, _))| bi)
1731                 } else {
1732                     None
1733                 };
1734                 // Debug-build loudness for the silent grouped→overlay flip —
1735                 // see `near_roll_fallback_reason` on what qualifies and why
1736                 // this warns instead of panicking.
1737                 #[cfg(debug_assertions)]
1738                 if host_plate.is_none() && mode < 3.5 && full_ring && tint.is_none() {
1739                     let enclosing = plate_stack.iter().enumerate().rev().find(|(_, (_, p))| {
1740                         rect.x >= p.x - 0.5
1741                             && rect.y >= p.y - 0.5
1742                             && rect.x + rect.width <= p.x + p.width + 0.5
1743                             && rect.y + rect.height <= p.y + p.height + 0.5
1744                     });
1745                     if let Some((si, &(bi, prect))) = enclosing {
1746                         // Host roll width rides the push's light.w (physical px).
1747                         let roll = batches[bi].plate.as_ref().map_or(0.0, |p| p.light[3]) / scale;
1748                         let later: Vec<crate::scene::layout::Rect> =
1749                             plate_stack[si + 1..].iter().map(|&(_, r)| r).collect();
1750                         let budget_full = features.len() >= crate::draw::MAX_PLATE_FEATURES;
1751                         if let Some(why) =
1752                             near_roll_fallback_reason(rect, *depth, &prect, roll, &later, budget_full)
1753                         {
1754                             let kind = if mode > 2.5 { "boss" } else { "recess" };
1755                             plate_carve_warn_once(format!(
1756                                 "plate-carve: near-roll {kind} ({:.0},{:.0} {:.0}x{:.0}) lost grouping — {why}; \
1757                                  its junction with the host plate's roll shades through the overlay fallback, \
1758                                  visually different from grouped frames (CCE_PLATE_DEBUG=1 traces verdicts) \
1759                                  [debug-build warning, printed once]",
1760                                 rect.x, rect.y, rect.width, rect.height
1761                             ));
1762                         }
1763                     }
1764                 }
1765                 if dbg_plates {
1766                     match host_plate {
1767                         Some(_) => dbg_grouped += 1,
1768                         None => {
1769                             // Re-derive WHY, in the same order the guard tests
1770                             // them. Debug-only: the hot path above is untouched.
1771                             let kind = match &item.prim {
1772                                 Prim::Boss { .. } => "boss",
1773                                 Prim::Ridge { .. } => "ridge",
1774                                 Prim::Trough { .. } => "trough",
1775                                 _ => "recess",
1776                             };
1777                             let infl = *depth * 0.5 + 2.0;
1778                             let (sx0, sy0) = (rect.x - infl, rect.y - infl);
1779                             let (sx1, sy1) = (rect.x + rect.width + infl, rect.y + rect.height + infl);
1780                             let enclosing: Vec<usize> = plate_stack
1781                                 .iter()
1782                                 .enumerate()
1783                                 .filter(|(_, (_, p))| {
1784                                     rect.x >= p.x - 0.5
1785                                         && rect.y >= p.y - 0.5
1786                                         && rect.x + rect.width <= p.x + p.width + 0.5
1787                                         && rect.y + rect.height <= p.y + p.height + 0.5
1788                                 })
1789                                 .map(|(si, _)| si)
1790                                 .collect();
1791                             let occluded = |si: usize| {
1792                                 plate_stack[si + 1..].iter().any(|(_, o)| {
1793                                     sx0 < o.x + o.width && sx1 > o.x && sy0 < o.y + o.height && sy1 > o.y
1794                                 })
1795                             };
1796                             let why = if mode >= 3.5 {
1797                                 "ridge — never groups (its bump profile is not a monotonic step)".into()
1798                             } else if !full_ring {
1799                                 format!("edge-suppressed {edges:?} — the extended wall would smear across the host")
1800                             } else if tint.is_some() {
1801                                 "tinted — a CSG feature is geometry only, it carries no color".into()
1802                             } else if features.len() >= crate::draw::MAX_PLATE_FEATURES {
1803                                 format!("feature budget full ({} used)", features.len())
1804                             } else if enclosing.is_empty() {
1805                                 format!("no enclosing plate ({} open)", plate_stack.len())
1806                             } else if enclosing.iter().all(|&si| occluded(si)) {
1807                                 "a later plate overlaps this carve's shaded region".into()
1808                             } else {
1809                                 "host plate's feature run is closed (another carve appended since)".into()
1810                             };
1811                             dbg_fell_back.push(format!(
1812                                 "  overlay: {kind} ({:.0},{:.0} {:.0}x{:.0}) — {why}",
1813                                 rect.x, rect.y, rect.width, rect.height
1814                             ));
1815                         }
1816                     }
1817                 }
1818                 if let Some(bi) = host_plate {
1819                     {
1820                         // A wall the carve shares with the plate's edge extends
1821                         // past the plate, so the carve has no wall there.
1822                         let ext = *depth + 4.0;
1823                         let (mut x0, mut y0) = (rect.x, rect.y);
1824                         let (mut x1, mut y1) = (rect.x + rect.width, rect.y + rect.height);
1825                         if !edges.0 { y0 -= ext; }
1826                         if !edges.1 { x1 += ext; }
1827                         if !edges.2 { y1 += ext; }
1828                         if !edges.3 { x0 -= ext; }
1829                         let t_px = *depth * scale;
1830                         // The carve's drop: the material's pinned height, else
1831                         // the analytic ratio of the wall saturating at the DE's
1832                         // roll width (`layout::carve_depth_px` states the rule
1833                         // once for this path and the shader's free carves).
1834                         let k_mag = crate::layout::carve_depth_px(*depth) * scale;
1835                         // Negative depth = raised (Boss); the shader's summed
1836                         // slope vectors and curvature sign follow it.
1837                         let k_px = if raised { -k_mag } else { k_mag };
1838                         if let Some(p) = batches[bi].plate.as_mut() {
1839                             if p.host[1] == 0.0 {
1840                                 p.host[0] = features.len() as f32;
1841                             }
1842                             p.host[1] += 1.0;
1843                         }
1844                         last_feature_plate = Some(bi);
1845                         features.push([
1846                             (x0 + x1) * 0.5 * scale,
1847                             (y0 + y1) * 0.5 * scale,
1848                             (x1 - x0) * 0.5 * scale,
1849                             (y1 - y0) * 0.5 * scale,
1850                             radii.0 * scale,
1851                             radii.1 * scale,
1852                             radii.2 * scale,
1853                             radii.3 * scale,
1854                             t_px,
1855                             k_px,
1856                             0.0,
1857                             0.0,
1858                         ]);
1859                         continue;
1860                     }
1861                 }
1862                 // Overlay-only carve: the cover quad inflates by half the roll
1863                 // width (the step straddles the boundary) and carries no color —
1864                 // the shader emits translucent white/black over what's beneath.
1865                 let infl = *depth * 0.5 + 2.0;
1866                 verts.extend(quad_vertices(
1867                     rect.x - infl, rect.y - infl,
1868                     rect.width + 2.0 * infl, rect.height + 2.0 * infl,
1869                     sw, sh, [0.0; 4],
1870                 ));
1871                 // A suppressed wall is pushed past the cover quad, so its
1872                 // shading falls outside the drawn pixels (see Prim::Recess on
1873                 // why a flush region is a step, not a trough).
1874                 let ext = *depth + 4.0;
1875                 let (mut x0, mut y0) = (rect.x, rect.y);
1876                 let (mut x1, mut y1) = (rect.x + rect.width, rect.y + rect.height);
1877                 if !edges.0 { y0 -= ext; }
1878                 if !edges.1 { x1 += ext; }
1879                 if !edges.2 { y1 += ext; }
1880                 if !edges.3 { x0 -= ext; }
1881                 let sdf_rect = crate::scene::layout::Rect { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
1882                 let mut p = plate_push_raised(&sdf_rect, *radii, *depth, scale, plate_light, plate_mat, false, None);
1883                 p.mode = mode;
1884                 // w = 1.0 flags the free-carve shader path to composite its
1885                 // light in the tint and its shadow in a dark tint instead of
1886                 // white and black (plates leave w at 0.0).
1887                 if let Some(t) = tint {
1888                     p.specular_tint = [t[0], t[1], t[2], 1.0];
1889                 }
1890                 // Host-plate box for the roll fade: a suppressed wall means the
1891                 // recess runs flush to the host's edge there, so that side of
1892                 // the box sits at the original rect edge; enabled walls face
1893                 // host interior, pushed to ±1e5 so no fade applies.
1894                 const FAR: f32 = 1e5;
1895                 let (hx0, hy0) = (
1896                     if edges.3 { rect.x - FAR } else { rect.x },
1897                     if edges.0 { rect.y - FAR } else { rect.y },
1898                 );
1899                 let (hx1, hy1) = (
1900                     if edges.1 { rect.x + rect.width + FAR } else { rect.x + rect.width },
1901                     if edges.2 { rect.y + rect.height + FAR } else { rect.y + rect.height },
1902                 );
1903                 p.host = [
1904                     (hx0 + hx1) * 0.5 * scale,
1905                     (hy0 + hy1) * 0.5 * scale,
1906                     (hx1 - hx0) * 0.5 * scale,
1907                     (hy1 - hy0) * 0.5 * scale,
1908                 ];
1909                 plate = Some(p);
1910             }
1911             Prim::Bevel { rect, radii, material, depth, tint: _ } => {
1912                 let color = material.fill(PlateRole::Nested);
1913                 // Full-size fill: the lip is now a shading overlay, not a paint of the
1914                 // outer ring, so the fill must cover the whole rect (the old inset fill
1915                 // would leave the ring showing whatever lay beneath).
1916                 let corners = crate::widget::CornerRadii {
1917                     top_left: radii.0, top_right: radii.1,
1918                     bottom_right: radii.2, bottom_left: radii.3,
1919                 };
1920                 push_rounded_rect_vertices_corners(rect.x, rect.y, rect.width, rect.height, corners, sw, sh, color, no, None, &mut verts);
1921                 push_plate_bevel_vertices(rect.x, rect.y, rect.width, rect.height, radii.0, *depth, sw, sh, color, no, &mut verts);
1922             }
1923             // The legacy banded path has no inside-out SDF: the face square,
1924             // and only below the hole, so the coves' rows are left to what
1925             // is beneath (A/B comparison path only).
1926             Prim::Frame { rect, hole, material, .. } => {
1927                 let color = material.fill(PlateRole::Nested);
1928                 let y0 = rect.y.max(hole.y + hole.height);
1929                 let y1 = rect.y + rect.height;
1930                 if y1 > y0 {
1931                     verts.extend(quad_vertices(rect.x, y0, rect.width, y1 - y0, sw, sh, color));
1932                 }
1933             }
1934             Prim::Plate { rect, radii, material, depth, .. } => {
1935                 let color = material.fill(PlateRole::Nested);
1936                 if *depth < 0.0 {
1937                     // Fill-less roll overlay (negative-depth sentinel): the banded
1938                     // legacy tessellation has no overlay compositing, so the roll
1939                     // is simply absent here — the A/B path draws nothing rather
1940                     // than a wrong fill.
1941                     continue;
1942                 }
1943                 // Fill at full size (no inset — see Prim::Plate), then light the face,
1944                 // then roll the perimeter. The lip rides on top of the fill's outer band
1945                 // rather than replacing it, so the plate's silhouette and the
1946                 // compositor's rounded window corners still agree exactly.
1947                 let corners = crate::widget::CornerRadii {
1948                     top_left: radii.0, top_right: radii.1,
1949                     bottom_right: radii.2, bottom_left: radii.3,
1950                 };
1951                 push_rounded_rect_vertices_corners(
1952                     rect.x, rect.y, rect.width, rect.height, corners, sw, sh, color, no, None, &mut verts,
1953                 );
1954                 push_plate_face_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, no, &mut verts);
1955                 push_bevel_edge_vertices_radii(
1956                     rect.x, rect.y, rect.width, rect.height, *radii, *depth,
1957                     sw, sh, color, no, 1.0, &mut verts,
1958                 );
1959             }
1960             Prim::Recess { rect, radii, depth, edges, .. } => {
1961                 // Edges only — no fill: the shading is an overlay, so whatever is painted
1962                 // below (fill, rim gradient, blur) shows through the carve modulated
1963                 // rather than repainted. `light_sign = -1.0` shadows the lit-facing edges,
1964                 // which is the raised->recessed inversion.
1965                 push_bevel_edge_vertices_banded(
1966                     rect.x, rect.y, rect.width, rect.height, *radii, *depth,
1967                     sw, sh, [0.0; 4], no, -1.0, default_bevel_bands(*depth), *edges,
1968                     EdgeKind::Step, &mut verts,
1969                 );
1970             }
1971             Prim::Boss { rect, radii, depth, edges, .. } => {
1972                 // Legacy raised step: the recess overlay with the light sign upright.
1973                 push_bevel_edge_vertices_banded(
1974                     rect.x, rect.y, rect.width, rect.height, *radii, *depth,
1975                     sw, sh, [0.0; 4], no, 1.0, default_bevel_bands(*depth), *edges,
1976                     EdgeKind::Step, &mut verts,
1977                 );
1978             }
1979             Prim::Ridge { rect, radii, depth, edges } => {
1980                 // Legacy approximation: a raised step up at the boundary plus a
1981                 // recessed step down half a width in (the banded machinery has no
1982                 // bump profile; the double-pass hot crest is accepted here — the
1983                 // legacy path exists only for A/B comparison).
1984                 let half = *depth * 0.5;
1985                 push_bevel_edge_vertices_banded(
1986                     rect.x, rect.y, rect.width, rect.height, *radii, half,
1987                     sw, sh, [0.0; 4], no, 1.0, default_bevel_bands(half), *edges,
1988                     EdgeKind::Step, &mut verts,
1989                 );
1990                 let ir = (radii.0 - half).max(0.0);
1991                 push_bevel_edge_vertices_banded(
1992                     rect.x + half, rect.y + half,
1993                     rect.width - *depth, rect.height - *depth,
1994                     (ir, ir, ir, ir), half,
1995                     sw, sh, [0.0; 4], no, -1.0, default_bevel_bands(half), *edges,
1996                     EdgeKind::Step, &mut verts,
1997                 );
1998             }
1999             Prim::Trough { rect, radii, depth, edges, .. } => {
2000                 // Legacy approximation, the Ridge arm's two steps with the light
2001                 // signs swapped: down at the boundary, back up half a width in.
2002                 // The banded machinery has no valley profile, so this is the old
2003                 // stacked look — accepted here, as the legacy path exists only
2004                 // for A/B comparison against the SDF one.
2005                 let half = *depth * 0.5;
2006                 push_bevel_edge_vertices_banded(
2007                     rect.x, rect.y, rect.width, rect.height, *radii, half,
2008                     sw, sh, [0.0; 4], no, -1.0, default_bevel_bands(half), *edges,
2009                     EdgeKind::Step, &mut verts,
2010                 );
2011                 let ir = (radii.0 - half).max(0.0);
2012                 push_bevel_edge_vertices_banded(
2013                     rect.x + half, rect.y + half,
2014                     rect.width - *depth, rect.height - *depth,
2015                     (ir, ir, ir, ir), half,
2016                     sw, sh, [0.0; 4], no, 1.0, default_bevel_bands(half), *edges,
2017                     EdgeKind::Step, &mut verts,
2018                 );
2019             }
2020             Prim::Field { rect, radii, depth, split, end, .. } => {
2021                 // Legacy approximation: the two-box form `Prim::Field`
2022                 // replaced — a well either side of the run a step down, the
2023                 // run the Trough arm's down-then-up stack. The banded
2024                 // machinery has no blended outline, and the legacy path
2025                 // exists only for A/B comparison.
2026                 let all = (true, true, true, true);
2027                 let (fl, fr) = (rect.x, rect.x + rect.width);
2028                 let (well_l, well_r) = (*split > fl, *end < fr);
2029                 let rx = split.max(fl);
2030                 let rw = (end.min(fr) - rx).max(0.0);
2031                 if well_l {
2032                     push_bevel_edge_vertices_banded(
2033                         fl, rect.y, rx - fl, rect.height, (radii.0, 0.0, 0.0, radii.3), *depth,
2034                         sw, sh, [0.0; 4], no, -1.0, default_bevel_bands(*depth), all,
2035                         EdgeKind::Step, &mut verts,
2036                     );
2037                 }
2038                 if well_r {
2039                     push_bevel_edge_vertices_banded(
2040                         rx + rw, rect.y, fr - rx - rw, rect.height, (0.0, radii.1, radii.2, 0.0), *depth,
2041                         sw, sh, [0.0; 4], no, -1.0, default_bevel_bands(*depth), all,
2042                         EdgeKind::Step, &mut verts,
2043                     );
2044                 }
2045                 // A run's own corners where it reaches the outline; square at a seam.
2046                 let (l0, l3) = if well_l { (0.0, 0.0) } else { (radii.0, radii.3) };
2047                 let (r1, r2) = if well_r { (0.0, 0.0) } else { (radii.1, radii.2) };
2048                 let half = *depth * 0.5;
2049                 push_bevel_edge_vertices_banded(
2050                     rx, rect.y, rw, rect.height, (l0, r1, r2, l3), half,
2051                     sw, sh, [0.0; 4], no, -1.0, default_bevel_bands(half), all,
2052                     EdgeKind::Step, &mut verts,
2053                 );
2054                 let ir = if well_r { 0.0 } else { (radii.1 - half).max(0.0) };
2055                 let il = if well_l { 0.0 } else { (radii.0 - half).max(0.0) };
2056                 push_bevel_edge_vertices_banded(
2057                     rx + half, rect.y + half, rw - *depth, rect.height - *depth, (il, ir, ir, il), half,
2058                     sw, sh, [0.0; 4], no, 1.0, default_bevel_bands(half), all,
2059                     EdgeKind::Step, &mut verts,
2060                 );
2061             }
2062             Prim::Arc { cx, cy, radius, thickness, start: sa, end: ea, color } => {
2063                 push_arc_background_vertices(*cx, *cy, *radius, *thickness, *sa, *ea, sw, sh, *color, segs(*radius), no, &mut verts);
2064             }
2065             Prim::ArcShaded { cx, cy, radius, thickness, start: sa, end: ea, inner, crest, outer } => {
2066                 push_arc_shaded_vertices(*cx, *cy, *radius, *thickness, *sa, *ea, sw, sh, *inner, *crest, *outer, segs(*radius), no, &mut verts);
2067             }
2068             Prim::Vector { x1, y1, x2, y2, thickness, color, cap } => {
2069                 let lc = match cap {
2070                     Cap::Flat => LineCap::Flat,
2071                     Cap::Round => LineCap::Round,
2072                     Cap::Arrow => LineCap::Arrow,
2073                 };
2074                 verts.extend(vector_vertices(*x1, *y1, *x2, *y2, *thickness, sw, sh, *color, lc));
2075             }
2076             Prim::Circle { cx, cy, radius, color } => {
2077                 if item.clip_circle.is_none() && *radius > 1.5 {
2078                     // Cover quad with the disc itself as the (feathered) circle
2079                     // clip: a per-pixel smooth silhouette instead of a hard-edged
2080                     // fan. The quad overhangs by 1px for the feather. Only when
2081                     // no ancestor clip holds the slot — then it's the fan path.
2082                     let own = [cx * scale, cy * scale, radius * scale];
2083                     let d = *radius + 1.0;
2084                     verts.extend(quad_vertices_with_clip(
2085                         cx - d, cy - d, 2.0 * d, 2.0 * d, sw, sh, *color, own,
2086                     ));
2087                 } else {
2088                     verts.extend(circle_vertices(*cx, *cy, *radius, sw, sh, *color, segs(*radius), no));
2089                 }
2090             }
2091             Prim::Sphere { cx, cy, radius, material } if shader_plates => {
2092                 let color = material.fill(PlateRole::Nested);
2093                 let mat = material.finish.to_array();
2094                 // A hemisphere lit per pixel by the plate branch (mode 5): one
2095                 // cover quad, its own never-merged batch. The quad overhangs
2096                 // the disc by 1px for the shader's silhouette anti-aliasing.
2097                 let d = *radius + 1.0;
2098                 verts.extend(quad_vertices(cx - d, cy - d, 2.0 * d, 2.0 * d, sw, sh, color));
2099                 plate = Some(crate::draw::PlatePush {
2100                     // Center + radius in physical px; the SDF box machinery is
2101                     // unused in this mode, so .w is free.
2102                     rect: [cx * scale, cy * scale, radius * scale, 0.0],
2103                     radii: [0.0; 4],
2104                     light: [plate_light[0], plate_light[1], plate_light[2], 0.0],
2105                     material: mat,
2106                     host: [0.0; 4],
2107                     specular_tint: [1.0, 1.0, 1.0, 0.0],
2108                     mode: 5.0,
2109                     shape: 2.0,
2110                 });
2111             }
2112             Prim::Sphere { cx, cy, radius, material } => {
2113                 let color = material.fill(PlateRole::Nested);
2114                 // Legacy path: the flat disc, exactly a Circle.
2115                 verts.extend(circle_vertices(*cx, *cy, *radius, sw, sh, color, segs(*radius), no));
2116             }
2117             Prim::DropletScrim { rect, material, spec, feather } if shader_plates => {
2118                 let color = material.fill(PlateRole::Nested);
2119                 let mat = material.finish.to_array();
2120                 // Shader mode 12: the droplet's own SDF, filled flat and
2121                 // feathered inward. No contact shadow, so unlike the lit drop
2122                 // the cover quad is exactly the box — a scrim never draws
2123                 // outside the silhouette.
2124                 let g = droplet_geom(rect, spec);
2125                 verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, color));
2126                 plate = Some(crate::draw::PlatePush {
2127                     rect: [
2128                         (rect.x + rect.width * 0.5) * scale,
2129                         (rect.y + rect.height * 0.5) * scale,
2130                         g.hx * scale,
2131                         g.hy * scale,
2132                     ],
2133                     radii: [g.sag * scale, g.br * scale, g.bw * scale, g.k * scale],
2134                     // p_light.w carries the FEATHER here; mode 12 returns
2135                     // before the shading band it otherwise holds is read.
2136                     light: [plate_light[0], plate_light[1], plate_light[2], feather.max(0.001) * scale],
2137                     material: [mat[0], 0.0, 0.0, 0.0],
2138                     host: [g.sr * scale, 0.0, 0.0, g.ar * scale],
2139                     specular_tint: [0.0, 0.0, 0.0, g.bow * scale],
2140                     mode: 12.0,
2141                     shape: spec.curve.clamp(2.0, 6.0),
2142                 });
2143             }
2144             Prim::Droplet { rect, material, spec } if shader_plates => {
2145                 let color = material.fill(PlateRole::Nested);
2146                 let mat = material.finish.to_array();
2147                 // A water droplet lit by shader mode 10: one cover quad; the
2148                 // shader owns silhouette (sheet ∪smin belly), dome shading,
2149                 // fresnel rim and thin-edge clarity. The spec's height
2150                 // fractions resolve against the concrete rect here, clamped so
2151                 // small or narrow boxes stay well-formed (a belly wider than
2152                 // the box would turn the SDF interior inside out).
2153                 // The cover quad grows sideways and BELOW the box by the
2154                 // contact shadow's reach — shadow fragments live outside the
2155                 // silhouette, so they need covered pixels to shade.
2156                 let g = droplet_geom(rect, spec);
2157                 let (hx, hy, sag, br, bw, k, sr, ar, band, bow, sh_reach) =
2158                     (g.hx, g.hy, g.sag, g.br, g.bw, g.k, g.sr, g.ar, g.band, g.bow, g.sh_reach);
2159                 verts.extend(quad_vertices(
2160                     rect.x - sh_reach,
2161                     rect.y,
2162                     rect.width + 2.0 * sh_reach,
2163                     rect.height + sh_reach,
2164                     sw, sh, color,
2165                 ));
2166                 plate = Some(crate::draw::PlatePush {
2167                     rect: [
2168                         (rect.x + rect.width * 0.5) * scale,
2169                         (rect.y + rect.height * 0.5) * scale,
2170                         hx * scale,
2171                         hy * scale,
2172                     ],
2173                     radii: [sag * scale, br * scale, bw * scale, k * scale],
2174                     light: [plate_light[0], plate_light[1], plate_light[2], band * scale],
2175                     // Slots y/z/w feed roll_spec and the rim term directly:
2176                     // a droplet's material carries its own gleam/shine/rim
2177                     // there (`DropletSpec::finish`; a drop is wetter than the
2178                     // DE's plates), so this is the material's finish like any
2179                     // plate's.
2180                     material: mat,
2181                     host: [sr * scale, spec.clarity.clamp(0.0, 1.0), spec.dome, ar * scale],
2182                     // Droplet glints are always white, so the tint RGB slots
2183                     // carry droplet params instead: x = core density,
2184                     // y = contact-shadow reach px, z = shadow strength.
2185                     specular_tint: [
2186                         spec.core.clamp(0.0, 2.0),
2187                         sh_reach * scale,
2188                         spec.shadow.clamp(0.0, 1.0),
2189                         bow * scale,
2190                     ],
2191                     mode: 10.0,
2192                     shape: spec.curve.clamp(2.0, 6.0),
2193                 });
2194             }
2195             Prim::DropletScrim { rect, material, spec, .. } => {
2196                 let color = material.fill(PlateRole::Nested);
2197                 // Legacy banded path: no SDF to feather against, so the scrim
2198                 // degrades to the same flat outline the drop itself does —
2199                 // hard-edged, but present. A prim with no arm here VANISHES.
2200                 let cap = (rect.height * 0.5).min(rect.width * 0.5);
2201                 let sr = (spec.sheet_r.clamp(0.0, 1.0) * rect.height).min(cap);
2202                 let ar = (spec.attach.clamp(0.0, 1.0) * rect.height).min(cap);
2203                 let radii = crate::widget::CornerRadii::new(ar, ar, sr, sr);
2204                 push_rounded_rect_vertices_corners(rect.x, rect.y, rect.width, rect.height, radii, sw, sh, color, no, None, &mut verts);
2205             }
2206             Prim::Droplet { rect, material, spec } => {
2207                 let color = material.fill(PlateRole::Nested);
2208                 // Legacy banded path: the flat drop outline — attach-tapered
2209                 // top, round bottom. Degrades the material but keeps the
2210                 // silhouette (a prim with no arm here VANISHES, it doesn't
2211                 // degrade — see Ridge/Groove above).
2212                 let cap = (rect.height * 0.5).min(rect.width * 0.5);
2213                 let sr = (spec.sheet_r.clamp(0.0, 1.0) * rect.height).min(cap);
2214                 let ar = (spec.attach.clamp(0.0, 1.0) * rect.height).min(cap);
2215                 let radii = crate::widget::CornerRadii::new(ar, ar, sr, sr);
2216                 push_rounded_rect_vertices_corners(rect.x, rect.y, rect.width, rect.height, radii, sw, sh, color, no, None, &mut verts);
2217             }
2218             Prim::ConcaveFillet { cx, cy, radius, depth, start: a0, raised } if shader_plates => {
2219                 // A quarter-arc carve wall (shader mode 6/7): one cover quad
2220                 // over the wedge's reach; the wall straddles the arc by ±t/2
2221                 // like every carve boundary. p_rect carries centre + radius,
2222                 // p_radii.x the wedge start angle. Host box pushed far out —
2223                 // an inside-corner fillet never fades.
2224                 let m = *depth * 0.5 + 2.0;
2225                 let r = *radius + m;
2226                 verts.extend(quad_vertices(cx - r, cy - r, 2.0 * r, 2.0 * r, sw, sh, [0.0; 4]));
2227                 plate = Some(crate::draw::PlatePush {
2228                     rect: [cx * scale, cy * scale, *radius * scale, 0.0],
2229                     radii: [*a0, 0.0, 0.0, 0.0],
2230                     light: [plate_light[0], plate_light[1], plate_light[2], *depth * scale],
2231                     material: plate_mat,
2232                     host: [0.0, 0.0, 1e6, 1e6],
2233                     specular_tint: [1.0, 1.0, 1.0, 0.0],
2234                     mode: if *raised { 7.0 } else { 6.0 },
2235                     shape: crate::layout::corner_shape(),
2236                 });
2237             }
2238             // Legacy banded path has no radial wall — the composed corner
2239             // stays square there (A/B comparison path only).
2240             Prim::ConcaveFillet { .. } => {}
2241             Prim::Groove { a, b, width, depth, host, strength } if shader_plates => {
2242                 // A slab carve about the line a–b (shader mode 8): the cover
2243                 // quad is the segment's bounding box grown by the groove's own
2244                 // half-width plus the wall's reach. Off-band corners of that
2245                 // box sit at u = 1 (plateau), so the box overhang shades
2246                 // nothing — the slab is what bounds the mark, not the quad.
2247                 let m = *width * 0.5 + *depth * 0.5 + 2.0;
2248                 let (x0, x1) = (a.0.min(b.0) - m, a.0.max(b.0) + m);
2249                 let (y0, y1) = (a.1.min(b.1) - m, a.1.max(b.1) + m);
2250                 verts.extend(quad_vertices(x0, y0, x1 - x0, y1 - y0, sw, sh, [0.0; 4]));
2251                 // Unit normal of the line — the direction the slab's distance is
2252                 // measured along. A degenerate segment falls back to vertical so
2253                 // a zero-length groove is a no-op wall rather than a NaN.
2254                 let (dx, dy) = (b.0 - a.0, b.1 - a.1);
2255                 let len = (dx * dx + dy * dy).sqrt();
2256                 let n = if len > 1e-4 { (-dy / len, dx / len) } else { (1.0, 0.0) };
2257                 plate = Some(crate::draw::PlatePush {
2258                     // Centre + slab half-width in physical px; .w unused.
2259                     rect: [
2260                         (a.0 + b.0) * 0.5 * scale,
2261                         (a.1 + b.1) * 0.5 * scale,
2262                         *width * 0.5 * scale,
2263                         0.0,
2264                     ],
2265                     radii: [n.0, n.1, 0.0, 0.0],
2266                     light: [plate_light[0], plate_light[1], plate_light[2], *depth * scale],
2267                     // The finish's shading, specular and AO, at the groove's
2268                     // strength; shininess is a shape, not an amount.
2269                     material: {
2270                         let s = strength.clamp(0.0, 1.0);
2271                         [plate_mat[0] * s, plate_mat[1] * s, plate_mat[2], plate_mat[3] * s]
2272                     },
2273                     host: [
2274                         (host.x + host.width * 0.5) * scale,
2275                         (host.y + host.height * 0.5) * scale,
2276                         host.width * 0.5 * scale,
2277                         host.height * 0.5 * scale,
2278                     ],
2279                     specular_tint: [1.0, 1.0, 1.0, 0.0],
2280                     mode: 8.0,
2281                     shape: crate::layout::corner_shape(),
2282                 });
2283             }
2284             Prim::Groove { a, b, width, depth, host: _, strength } => {
2285                 // Legacy approximation. The banded tessellators walk BOX edges —
2286                 // exactly the axis-aligned assumption a groove exists to escape —
2287                 // so the walls are drawn directly as two feathered lines meeting
2288                 // at the centerline: the engraved-line fake, one half in shadow
2289                 // and one lit. Coarser than the SDF (no profile curve, no host
2290                 // fade), but this path exists for A/B comparison, and drawing
2291                 // NOTHING would silently delete the mark rather than degrade it
2292                 // — see `Prim::Ridge` above, which accepts a hot crest for the
2293                 // same reason.
2294                 let (dx, dy) = (b.0 - a.0, b.1 - a.1);
2295                 let len = (dx * dx + dy * dy).sqrt();
2296                 if len < 0.001 {
2297                     continue;
2298                 }
2299                 let n = (-dy / len, dx / len);
2300                 // Same convention as `push_bevel_edge_vertices_banded`: the
2301                 // light folded through `light_sign` (-1.0 — a groove is a
2302                 // carve), dotted with each wall's OUTWARD normal, amplitude on
2303                 // `bevel_depth`. So a groove re-lights with the DE's light
2304                 // instead of hardcoding which side is dark.
2305                 let rad = crate::layout::light_source_position();
2306                 let (lx, ly) = (-rad.cos(), rad.sin());
2307                 let v = crate::layout::bevel_depth() * (n.0 * lx + n.1 * ly);
2308                 // Each wall covers its own half, centreline to outer edge —
2309                 // abutting rather than overlapping. The SDF gets away with
2310                 // walls that overlap across a sub-pixel floor because it is one
2311                 // evaluation of |distance|; two opposite-signed overlays would
2312                 // just blend to mud.
2313                 let half = (*width * 0.5 + *depth * 0.5).max(0.5);
2314                 for side in [1.0f32, -1.0] {
2315                     let sv = v * side;
2316                     let mut c = if sv >= 0.0 { overlay_light(sv) } else { overlay_dark(sv) };
2317                     c[3] *= strength.clamp(0.0, 1.0);
2318                     if c[3] <= 0.0 {
2319                         continue;
2320                     }
2321                     let off = side * half * 0.5;
2322                     push_feathered_line_vertices(
2323                         a.0 + n.0 * off, a.1 + n.1 * off,
2324                         b.0 + n.0 * off, b.1 + n.1 * off,
2325                         half, sw, sh, c, &mut verts,
2326                     );
2327                 }
2328             }
2329             Prim::Lattice { rect, period, origin, cell, radius, depth } if shader_plates => {
2330                 // A periodic well field (shader mode 13): one cover quad over
2331                 // `rect`; the shader folds each pixel into the period and
2332                 // measures the nearest cell, so the whole lattice is a single
2333                 // evaluation. p_rect = one cell's centre + half-extents,
2334                 // p_radii = the corner radius, p_host.xy = the period; the
2335                 // host-box fade sides are pushed far out (a lattice never
2336                 // fades against a host — its own rect bounds it).
2337                 let (pw, ph) = (period.0.max(1e-3), period.1.max(1e-3));
2338                 verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, [0.0; 4]));
2339                 plate = Some(crate::draw::PlatePush {
2340                     rect: [origin.0 * scale, origin.1 * scale, cell.0 * 0.5 * scale, cell.1 * 0.5 * scale],
2341                     radii: [*radius * scale; 4],
2342                     light: [plate_light[0], plate_light[1], plate_light[2], *depth * scale],
2343                     material: plate_mat,
2344                     host: [pw * scale, ph * scale, 1e6, 1e6],
2345                     specular_tint: [1.0, 1.0, 1.0, 0.0],
2346                     mode: 13.0,
2347                     shape: crate::layout::corner_shape(),
2348                 });
2349             }
2350             Prim::Grout { rect, period, origin, cell, radius, color } if shader_plates => {
2351                 // The lattice's fold, painted flat (shader mode 15): one cover
2352                 // quad in the grout colour; the shader keeps it outside the
2353                 // cells. Same push layout as the lattice; light/material are
2354                 // carried but unread.
2355                 let (pw, ph) = (period.0.max(1e-3), period.1.max(1e-3));
2356                 verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, *color));
2357                 plate = Some(crate::draw::PlatePush {
2358                     rect: [origin.0 * scale, origin.1 * scale, cell.0 * 0.5 * scale, cell.1 * 0.5 * scale],
2359                     radii: [*radius * scale; 4],
2360                     light: [plate_light[0], plate_light[1], plate_light[2], 0.0],
2361                     material: plate_mat,
2362                     host: [pw * scale, ph * scale, 1e6, 1e6],
2363                     specular_tint: [1.0, 1.0, 1.0, 0.0],
2364                     mode: 15.0,
2365                     shape: crate::layout::corner_shape(),
2366                 });
2367             }
2368             // Legacy banded path: no periodic wall — the lattice and the grout
2369             // draw nothing there, like the fillet (A/B comparison path only).
2370             Prim::Lattice { .. } | Prim::Grout { .. } => {}
2371             Prim::CarveUnion { boxes, depth, raised } if shader_plates => {
2372                 // The union of several boxes as ONE wall (shader mode 14): the
2373                 // boxes go into the frame's feature buffer as a contiguous run
2374                 // and the shader takes the nearest one per pixel. The cover
2375                 // quad is the union's bounding box grown by the wall's reach;
2376                 // off-shape corners of it sit at the plateau and shade nothing.
2377                 let budget = crate::draw::MAX_PLATE_FEATURES.saturating_sub(features.len());
2378                 let take = boxes.len().min(budget);
2379                 if take < boxes.len() && plate_debug() {
2380                     eprintln!(
2381                         "plate-carve: union of {} boxes gets {} — feature budget full ({} used)",
2382                         boxes.len(), take, features.len()
2383                     );
2384                 }
2385                 if take == 0 {
2386                     continue;
2387                 }
2388                 let kept = &boxes[..take];
2389                 let (mut x0, mut y0, mut x1, mut y1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
2390                 for (r, _) in kept {
2391                     x0 = x0.min(r.x);
2392                     y0 = y0.min(r.y);
2393                     x1 = x1.max(r.x + r.width);
2394                     y1 = y1.max(r.y + r.height);
2395                 }
2396                 let infl = *depth * 0.5 + 2.0;
2397                 verts.extend(quad_vertices(
2398                     x0 - infl, y0 - infl,
2399                     (x1 - x0) + 2.0 * infl, (y1 - y0) + 2.0 * infl,
2400                     sw, sh, [0.0; 4],
2401                 ));
2402                 let off = features.len() as f32;
2403                 for (r, radii) in kept {
2404                     features.push([
2405                         (r.x + r.width * 0.5) * scale,
2406                         (r.y + r.height * 0.5) * scale,
2407                         r.width * 0.5 * scale,
2408                         r.height * 0.5 * scale,
2409                         radii.0 * scale,
2410                         radii.1 * scale,
2411                         radii.2 * scale,
2412                         radii.3 * scale,
2413                         *depth * scale,
2414                         0.0,
2415                         0.0,
2416                         0.0,
2417                     ]);
2418                 }
2419                 // The run is complete: a plate with an open feature run must
2420                 // not append past it (its features would no longer be
2421                 // contiguous), so it is closed here like any other appender.
2422                 last_feature_plate = None;
2423                 plate = Some(crate::draw::PlatePush {
2424                     rect: [
2425                         (x0 + x1) * 0.5 * scale,
2426                         (y0 + y1) * 0.5 * scale,
2427                         (x1 - x0) * 0.5 * scale,
2428                         (y1 - y0) * 0.5 * scale,
2429                     ],
2430                     // x: the raised flag; the shader reads nothing else here.
2431                     radii: [if *raised { 1.0 } else { 0.0 }, 0.0, 0.0, 0.0],
2432                     light: [plate_light[0], plate_light[1], plate_light[2], *depth * scale],
2433                     material: plate_mat,
2434                     // Feature run [offset, count] (the renderer rebases the
2435                     // offset onto the frame slot, as for mode 1); zw far out
2436                     // so the host-box fade never applies.
2437                     host: [off, take as f32, 1e6, 1e6],
2438                     specular_tint: [1.0, 1.0, 1.0, 0.0],
2439                     mode: 14.0,
2440                     shape: crate::layout::corner_shape(),
2441                 });
2442             }
2443             // Legacy banded path: no union — nothing is drawn there, like the
2444             // fillet and the lattice (A/B comparison path only).
2445             Prim::CarveUnion { .. } => {}
2446         }
2447         let end = verts.len() as u32;
2448         if end == start {
2449             continue;
2450         }
2451         // Some tessellators (quad_vertices, vector_vertices) don't thread the circle clip —
2452         // stamp the whole emitted range so every prim kind honors it uniformly.
2453         if item.clip_circle.is_some() {
2454             for v in verts[start as usize..].iter_mut() {
2455                 v.clip_circle = no;
2456             }
2457         }
2458         // Merge into the previous batch if it shares this clip pair and is contiguous.
2459         // Plate batches carry per-draw push constants, and blur-behind batches
2460         // trigger the renderer's snapshot copy, so neither ever merges.
2461         if plate.is_none() && !blur_behind {
2462             // Ordinary geometry painted after a plate ends its carve-grouping
2463             // window: a recess emitted later must overlay this geometry (the
2464             // fallback path), not shade beneath it inside the plate's draw.
2465             if dbg_plates && !plate_stack.is_empty() {
2466                 *dbg_closed_by.entry(prim_kind(&item.prim)).or_insert(0) += plate_stack.len();
2467             }
2468             plate_stack.clear();
2469             if let Some(last) = batches.last_mut() {
2470                 if last.plate.is_none()
2471                     && last.scissor == item.clip
2472                     && last.clip_rrect == item.clip_rrect
2473                     && last.end == start
2474                 {
2475                     last.end = end;
2476                     continue;
2477                 }
2478             }
2479         }
2480         if promoted {
2481             plate_stack.clear();
2482         }
2483         batches.push(DlBatch { scissor: item.clip, clip_rrect: item.clip_rrect, start, end, plate, blur_behind });
2484         if let Some(prect) = made_plate {
2485             plate_stack.push((batches.len() - 1, prect));
2486             if dbg_plates {
2487                 dbg_opened += 1;
2488             }
2489         }
2490     }
2491 
2492     if dbg_plates && (dbg_grouped > 0 || !dbg_fell_back.is_empty()) {
2493         eprintln!(
2494             "plate-dbg: {} carves — {dbg_grouped} grouped (exact CSG), {} overlay fallback",
2495             dbg_grouped + dbg_fell_back.len(),
2496             dbg_fell_back.len(),
2497         );
2498         eprintln!(
2499             "plate-dbg:   {dbg_opened} grouping window(s) opened by a filled plate; closed early by {}",
2500             if dbg_closed_by.is_empty() {
2501                 "nothing".to_string()
2502             } else {
2503                 dbg_closed_by
2504                     .iter()
2505                     .map(|(k, n)| format!("{k}x{n}"))
2506                     .collect::<Vec<_>>()
2507                     .join(", ")
2508             }
2509         );
2510         for line in &dbg_fell_back {
2511             eprintln!("plate-dbg: {line}");
2512         }
2513     }
2514 
2515     (verts, batches, images, features)
2516 }
2517 
2518 /// The push-constant block for a raised SDF-lit plate over `rect` (logical px in,
2519 /// physical px out). Corner radii clamp to the half-extent cap the SDF needs.
2520 ///
2521 /// `shape` is a per-plate corner exponent (`Prim::Plate`'s override); `None`
2522 /// follows the DE-wide `layout::corner_shape`. The span factor follows the
2523 /// exponent actually used, so a circular override (2.0) spans nothing and a
2524 /// half-extent radius lands on a true circle.
2525 #[allow(clippy::too_many_arguments)]
2526 /// The push block of a frosted flat fill promoted to a zero-depth plate: a
2527 /// mode-1 plate with no roll (`t` = 0.001, so the face is exactly the fill),
2528 /// corners at the nominal radii in the configured `corner_shape`, and the
2529 /// fill's own frost recipe in `host.zw` (`Material::from_fill` decodes the
2530 /// sentinel).
2531 ///
2532 /// The shape must be `corner_shape`, not a fixed circle: a frosted `Border`
2533 /// draws its stroke as `push_plate_solid_border_vertices` geometry in that
2534 /// shape, and the unfrosted fill fan uses it too. A circular face under a
2535 /// squircle stroke left the stroke cutting inside the face's corners.
2536 fn flat_frost_push(
2537     rect: &crate::scene::layout::Rect,
2538     radii: (f32, f32, f32, f32),
2539     fill: [f32; 4],
2540     scale: f32,
2541     light: [f32; 3],
2542     material: [f32; 4],
2543 ) -> crate::draw::PlatePush {
2544     let mut p = plate_push_raised(rect, radii, 0.0, scale, light, material, false, None);
2545     let [fz, fw] = crate::scene::material::Material::from_fill(fill).frost.pack(scale);
2546     p.host[2] = fz;
2547     p.host[3] = fw;
2548     p
2549 }
2550 
2551 fn plate_push_raised(
2552     rect: &crate::scene::layout::Rect,
2553     radii: (f32, f32, f32, f32),
2554     width: f32,
2555     scale: f32,
2556     light: [f32; 3],
2557     material: [f32; 4],
2558     scale_corners: bool,
2559     shape: Option<f32>,
2560 ) -> crate::draw::PlatePush {
2561     // Floored: a rect already shrunk past its padding (a window dragged
2562     // below what its layout can hold) has a NEGATIVE extent here, and
2563     // `clamp(0.0, cap)` with a negative cap is a panic, not a zero radius.
2564     let cap = (rect.width.min(rect.height) * 0.5).max(0.0);
2565     let shape = shape.map_or_else(crate::layout::corner_shape, |n| n.clamp(2.0, 16.0));
2566     // For PLATES (`scale_corners`), widen the corner span by the
2567     // curvature-match factor (see `layout::corner_span_factor`): the diagonal
2568     // curvature radius equals the configured radius, the corner reads as the
2569     // same size as a circular one, and every roll inset ≤ r stays crease-free
2570     // (past the diagonal curvature radius the offset curve the specular band
2571     // follows creases into a visible square corner). Widget-scale overlay
2572     // reliefs (recess/boss/ridge fallbacks) pass false: their radii must MATCH
2573     // the nominal-radius squircles of the widget silhouettes around them, and
2574     // at their few-px roll widths the offset crease is subpixel.
2575     let rscale = if scale_corners { crate::layout::corner_span_factor_for(shape) } else { 1.0 };
2576     crate::draw::PlatePush {
2577         rect: [
2578             (rect.x + rect.width * 0.5) * scale,
2579             (rect.y + rect.height * 0.5) * scale,
2580             rect.width * 0.5 * scale,
2581             rect.height * 0.5 * scale,
2582         ],
2583         radii: [
2584             (radii.0 * rscale).clamp(0.0, cap) * scale,
2585             (radii.1 * rscale).clamp(0.0, cap) * scale,
2586             (radii.2 * rscale).clamp(0.0, cap) * scale,
2587             (radii.3 * rscale).clamp(0.0, cap) * scale,
2588         ],
2589         light: [light[0], light[1], light[2], width * scale],
2590         material,
2591         // Mode-1 semantics: [feature offset, feature count] — no carves yet;
2592         // the tessellator fills these in as recesses group into this plate.
2593         host: [0.0, 0.0, 0.0, 0.0],
2594         specular_tint: [1.0, 1.0, 1.0, 0.0],
2595         mode: 1.0,
2596         shape,
2597     }
2598 }
2599 
2600 pub fn extra_quad_vertices(
2601     w: &dyn crate::widget::WidgetHost,
2602     qx: f32, qy: f32, qw: f32, qh: f32,
2603     sw: f32, sh: f32,
2604     qc: [f32; 4],
2605     clip_circle: [f32; 3],
2606 ) -> Vec<Vertex> {
2607     let mut verts = Vec::new();
2608     push_extra_quad_vertices(w, qx, qy, qw, qh, sw, sh, qc, clip_circle, &mut verts);
2609     verts
2610 }
2611 
2612 fn get_child_widget_for_quad(
2613     w: &dyn crate::widget::WidgetHost,
2614     qx: f32, qy: f32, qw: f32, qh: f32,
2615 ) -> &dyn crate::widget::WidgetHost {
2616     if let Some(pbg) = w.as_any().downcast_ref::<crate::widget::ParametersBg>() {
2617         for s in pbg.sliders.iter().flatten() {
2618             let (sx, sy, sww, shh) = s.rect();
2619             if qx >= sx - 0.1 && qx + qw <= sx + sww + 0.1 && qy >= sy - 0.1 && qy + qh <= sy + shh + 0.1 {
2620                 return s;
2621             }
2622         }
2623         for f in pbg.float3s.iter().flatten() {
2624             let (fx, fy, fww, fhh) = f.rect();
2625             if qx >= fx - 0.1 && qx + qw <= fx + fww + 0.1 && qy >= fy - 0.1 && qy + qh <= fy + fhh + 0.1 {
2626                 return f;
2627             }
2628         }
2629         for sb in pbg.spinboxes.iter().flatten() {
2630             let (sx, sy, sww, shh) = sb.rect();
2631             if qx >= sx - 0.1 && qx + qw <= sx + sww + 0.1 && qy >= sy - 0.1 && qy + qh <= sy + shh + 0.1 {
2632                 return sb;
2633             }
2634         }
2635         for btn in pbg.buttons.iter().flatten() {
2636             let (bx, by, bww, bhh) = btn.rect();
2637             if qx >= bx - 0.1 && qx + qw <= bx + bww + 0.1 && qy >= by - 0.1 && qy + qh <= by + bhh + 0.1 {
2638                 return btn;
2639             }
2640         }
2641         for ch in pbg.choices.iter().flatten() {
2642             let (cx, cy, cww, chh) = ch.rect();
2643             if qx >= cx - 0.1 && qx + qw <= cx + cww + 0.1 && qy >= cy - 0.1 && qy + qh <= cy + chh + 0.1 {
2644                 return ch;
2645             }
2646         }
2647         for t in pbg.texts.iter().flatten() {
2648             let (tx, ty, tww, thh) = t.rect();
2649             if qx >= tx - 0.1 && qx + qw <= tx + tww + 0.1 && qy >= ty - 0.1 && qy + qh <= ty + thh + 0.1 {
2650                 return t;
2651             }
2652         }
2653         for cb in pbg.toggles.iter().flatten() {
2654             let (cx, cy, cww, chh) = cb.rect();
2655             if qx >= cx - 0.1 && qx + qw <= cx + cww + 0.1 && qy >= cy - 0.1 && qy + qh <= cy + chh + 0.1 {
2656                 return cb;
2657             }
2658         }
2659         for c in pbg.colors.iter().flatten() {
2660             let (cx, cy, cww, chh) = c.rect();
2661             if qx >= cx - 0.1 && qx + qw <= cx + cww + 0.1 && qy >= cy - 0.1 && qy + qh <= cy + chh + 0.1 {
2662                 return c;
2663             }
2664         }
2665     }
2666     w
2667 }
2668 
2669 pub fn push_extra_quad_vertices(
2670     w: &dyn crate::widget::WidgetHost,
2671     qx: f32, qy: f32, qw: f32, qh: f32,
2672     sw: f32, sh: f32,
2673     qc: [f32; 4],
2674     clip_circle: [f32; 3],
2675     out: &mut Vec<Vertex>,
2676 ) {
2677     if let Some(graph) = w.as_any().downcast_ref::<crate::widget::display::Graph>() {
2678         if graph.is_node_rect(qx, qy, qw, qh) {
2679             let r = crate::layout::graph_node_corner_radius();
2680             let extra_radii = crate::widget::CornerRadii::new(r, r, r, r);
2681             push_rounded_rect_vertices_corners(qx, qy, qw, qh, extra_radii, sw, sh, qc, clip_circle, None, out);
2682             return;
2683         }
2684     }
2685 
2686     let target_w = get_child_widget_for_quad(w, qx, qy, qw, qh);
2687     let radii = target_w.corner_radii();
2688     if radii.top_left <= 0.1 && radii.top_right <= 0.1 && radii.bottom_right <= 0.1 && radii.bottom_left <= 0.1 {
2689         out.extend_from_slice(&quad_vertices_with_clip(qx, qy, qw, qh, sw, sh, qc, clip_circle));
2690         if let Some((color, thickness)) = target_w.solid_border() {
2691             let (wx, wy, ww, wh) = target_w.rect();
2692             if (qx - wx).abs() < 0.1 && (qy - wy).abs() < 0.1 && (qw - ww).abs() < 0.1 && (qh - wh).abs() < 0.1 {
2693                 push_plate_solid_border_vertices(qx, qy, qw, qh, radii, thickness, sw, sh, color, clip_circle, out);
2694             }
2695         }
2696         return;
2697     }
2698 
2699     let (wx, mut wy, ww, mut wh) = target_w.rect();
2700     let top_room = target_w.label_strip();
2701     wy += top_room;
2702     wh -= top_room;
2703     let extra_radii = crate::widget::CornerRadii::new(
2704         if qx <= wx + 1.5 && qy <= wy + 1.5 { radii.top_left } else { 0.0 },
2705         if qx + qw >= wx + ww - 1.5 && qy <= wy + 1.5 { radii.top_right } else { 0.0 },
2706         if qx + qw >= wx + ww - 1.5 && qy + qh >= wy + wh - 1.5 { radii.bottom_right } else { 0.0 },
2707         if qx <= wx + 1.5 && qy + qh >= wy + wh - 1.5 { radii.bottom_left } else { 0.0 },
2708     );
2709 
2710     push_rounded_rect_vertices_corners(qx, qy, qw, qh, extra_radii, sw, sh, qc, clip_circle, None, out);
2711 
2712     if let Some((color, thickness)) = target_w.solid_border() {
2713         let (rx, mut ry, rw, mut rh) = target_w.rect();
2714         let top = target_w.label_strip();
2715         ry += top;
2716         rh -= top;
2717         if (qx - rx).abs() < 0.1 && (qy - ry).abs() < 0.1 && (qw - rw).abs() < 0.1 && (qh - rh).abs() < 0.1 {
2718             push_plate_solid_border_vertices(qx, qy, qw, qh, radii, thickness, sw, sh, color, clip_circle, out);
2719         }
2720     }
2721 }
2722 
2723 pub fn extra_quad_vertices_clipped(
2724     w: &dyn crate::widget::WidgetHost,
2725     qx: f32, qy: f32, qw: f32, qh: f32,
2726     sw: f32, sh: f32,
2727     qc: [f32; 4],
2728     clip: (f32, f32, f32, f32),
2729     clip_circle: [f32; 3],
2730 ) -> Vec<Vertex> {
2731     let mut verts = Vec::new();
2732     push_extra_quad_vertices_clipped(w, qx, qy, qw, qh, sw, sh, qc, clip, clip_circle, &mut verts);
2733     verts
2734 }
2735 
2736 pub fn push_extra_quad_vertices_clipped(
2737     w: &dyn crate::widget::WidgetHost,
2738     qx: f32, qy: f32, qw: f32, qh: f32,
2739     sw: f32, sh: f32,
2740     qc: [f32; 4],
2741     clip: (f32, f32, f32, f32),
2742     clip_circle: [f32; 3],
2743     out: &mut Vec<Vertex>,
2744 ) {
2745     if let Some(graph) = w.as_any().downcast_ref::<crate::widget::display::Graph>() {
2746         if graph.is_node_rect(qx, qy, qw, qh) {
2747             let r = crate::layout::graph_node_corner_radius();
2748             let extra_radii = crate::widget::CornerRadii::new(r, r, r, r);
2749             push_rounded_rect_vertices_corners(qx, qy, qw, qh, extra_radii, sw, sh, qc, clip_circle, Some(clip), out);
2750             return;
2751         }
2752     }
2753 
2754     let target_w = get_child_widget_for_quad(w, qx, qy, qw, qh);
2755     let radii = target_w.corner_radii();
2756     if radii.top_left <= 0.1 && radii.top_right <= 0.1 && radii.bottom_right <= 0.1 && radii.bottom_left <= 0.1 {
2757         let (cx0, cy0, cx1, cy1) = clip;
2758         let ix0 = qx.max(cx0);
2759         let iy0 = qy.max(cy0);
2760         let ix1 = (qx + qw).min(cx1);
2761         let iy1 = (qy + qh).min(cy1);
2762         if ix1 <= ix0 || iy1 <= iy0 {
2763             return;
2764         }
2765         out.extend_from_slice(&quad_vertices_with_clip(ix0, iy0, ix1 - ix0, iy1 - iy0, sw, sh, qc, clip_circle));
2766         if let Some((color, thickness)) = target_w.solid_border() {
2767             let (wx, wy, ww, wh) = target_w.rect();
2768             if (qx - wx).abs() < 0.1 && (qy - wy).abs() < 0.1 && (qw - ww).abs() < 0.1 && (qh - wh).abs() < 0.1 {
2769                 push_plate_solid_border_vertices(qx, qy, qw, qh, radii, thickness, sw, sh, color, clip_circle, out);
2770             }
2771         }
2772         return;
2773     }
2774 
2775     let (wx, mut wy, ww, mut wh) = target_w.rect();
2776     let top_room = target_w.label_strip();
2777     wy += top_room;
2778     wh -= top_room;
2779     let extra_radii = crate::widget::CornerRadii::new(
2780         if qx <= wx + 1.5 && qy <= wy + 1.5 { radii.top_left } else { 0.0 },
2781         if qx + qw >= wx + ww - 1.5 && qy <= wy + 1.5 { radii.top_right } else { 0.0 },
2782         if qx + qw >= wx + ww - 1.5 && qy + qh >= wy + wh - 1.5 { radii.bottom_right } else { 0.0 },
2783         if qx <= wx + 1.5 && qy + qh >= wy + wh - 1.5 { radii.bottom_left } else { 0.0 },
2784     );
2785 
2786     push_rounded_rect_vertices_corners(qx, qy, qw, qh, extra_radii, sw, sh, qc, clip_circle, Some(clip), out);
2787 
2788     if let Some((color, thickness)) = target_w.solid_border() {
2789         let (rx, mut ry, rw, mut rh) = target_w.rect();
2790         let top = target_w.label_strip();
2791         ry += top;
2792         rh -= top;
2793         if (qx - rx).abs() < 0.1 && (qy - ry).abs() < 0.1 && (qw - rw).abs() < 0.1 && (qh - rh).abs() < 0.1 {
2794             push_plate_solid_border_vertices(qx, qy, qw, qh, radii, thickness, sw, sh, color, clip_circle, out);
2795         }
2796     }
2797 }
2798 
2799 pub fn circle_vertices(
2800     cx: f32, cy: f32, r: f32,
2801     sw: f32, sh: f32,
2802     color: [f32; 4],
2803     segments: usize,
2804     clip_circle: [f32; 3],
2805 ) -> Vec<Vertex> {
2806     let mut verts = Vec::new();
2807     for i in 0..segments {
2808         let theta1 = (i as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
2809         let theta2 = ((i + 1) as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
2810         let x0 = cx;
2811         let y0 = cy;
2812         let x1 = cx + r * theta1.cos();
2813         let y1 = cy + r * theta1.sin();
2814         let x2 = cx + r * theta2.cos();
2815         let y2 = cy + r * theta2.sin();
2816         
2817         let ndc_x0 = (x0 / sw) * 2.0 - 1.0;
2818         let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
2819         let ndc_x1 = (x1 / sw) * 2.0 - 1.0;
2820         let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
2821         let ndc_x2 = (x2 / sw) * 2.0 - 1.0;
2822         let ndc_y2 = 1.0 - (y2 / sh) * 2.0;
2823         
2824         verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
2825         verts.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
2826         verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
2827     }
2828     verts
2829 }
2830 
2831 pub fn circle_border_vertices(
2832     cx: f32, cy: f32, r: f32,
2833     thickness: f32,
2834     sw: f32, sh: f32,
2835     color: [f32; 4],
2836     segments: usize,
2837     clip_circle: [f32; 3],
2838 ) -> Vec<Vertex> {
2839     let mut verts = Vec::new();
2840     for i in 0..segments {
2841         let theta1 = (i as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
2842         let theta2 = ((i + 1) as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
2843         
2844         let x0 = cx + (r - thickness) * theta1.cos();
2845         let y0 = cy + (r - thickness) * theta1.sin();
2846         let x1 = cx + r * theta1.cos();
2847         let y1 = cy + r * theta1.sin();
2848         
2849         let x2 = cx + r * theta2.cos();
2850         let y2 = cy + r * theta2.sin();
2851         let x3 = cx + (r - thickness) * theta2.cos();
2852         let y3 = cy + (r - thickness) * theta2.sin();
2853         
2854         let ndc_x0 = (x0 / sw) * 2.0 - 1.0; let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
2855         let ndc_x1 = (x1 / sw) * 2.0 - 1.0; let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
2856         let ndc_x2 = (x2 / sw) * 2.0 - 1.0; let ndc_y2 = 1.0 - (y2 / sh) * 2.0;
2857         let ndc_x3 = (x3 / sw) * 2.0 - 1.0; let ndc_y3 = 1.0 - (y3 / sh) * 2.0;
2858         
2859         verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
2860         verts.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
2861         verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
2862         
2863         verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
2864         verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
2865         verts.push(Vertex { position: [ndc_x3, ndc_y3], color, clip_circle });
2866     }
2867     verts
2868 }
2869 
2870 pub fn arc_background_vertices(
2871     cx: f32, cy: f32, r: f32,
2872     thickness: f32,
2873     start_angle: f32, end_angle: f32,
2874     sw: f32, sh: f32,
2875     color: [f32; 4],
2876     segments: usize,
2877     clip_circle: [f32; 3],
2878 ) -> Vec<Vertex> {
2879     let mut verts = Vec::new();
2880     push_arc_background_vertices(cx, cy, r, thickness, start_angle, end_angle, sw, sh, color, segments, clip_circle, &mut verts);
2881     verts
2882 }
2883 
2884 /// A ring band with radial Gouraud shading: two sub-bands (inner rim → crest
2885 /// centerline, crest → outer rim) whose vertex colors interpolate across the
2886 /// stroke — the rounded-bevel profile — plus the half-px alpha feathers at
2887 /// both true rims (colors matched to the adjacent band, so no seams).
2888 #[allow(clippy::too_many_arguments)]
2889 pub fn push_arc_shaded_vertices(
2890     cx: f32, cy: f32, r: f32,
2891     thickness: f32,
2892     start_angle: f32, end_angle: f32,
2893     sw: f32, sh: f32,
2894     inner: [f32; 4], crest: [f32; 4], outer: [f32; 4],
2895     segments: usize,
2896     clip_circle: [f32; 3],
2897     out: &mut Vec<Vertex>,
2898 ) {
2899     let f = 0.5f32.min(thickness * 0.25);
2900     let r_out = r;
2901     let r_in = (r - thickness).max(0.0);
2902     let r_mid = (r_in + r_out) / 2.0;
2903     let fade_in = [inner[0], inner[1], inner[2], 0.0];
2904     let fade_out = [outer[0], outer[1], outer[2], 0.0];
2905     // (inner radius, outer radius, color at inner edge, color at outer edge)
2906     let bands = [
2907         ((r_in - f).max(0.0), r_in + f, fade_in, inner),
2908         (r_in + f, r_mid, inner, crest),
2909         (r_mid, r_out - f, crest, outer),
2910         (r_out - f, r_out + f, outer, fade_out),
2911     ];
2912     for i in 0..segments {
2913         let theta1 = start_angle + (i as f32) * (end_angle - start_angle) / (segments as f32);
2914         let theta2 = start_angle + ((i + 1) as f32) * (end_angle - start_angle) / (segments as f32);
2915         let (c1, s1) = (theta1.cos(), theta1.sin());
2916         let (c2, s2) = (theta2.cos(), theta2.sin());
2917         for &(ra, rb, ca, cb) in &bands {
2918             if rb <= ra {
2919                 continue;
2920             }
2921             let p = |rad: f32, c: f32, s: f32| -> [f32; 2] {
2922                 [((cx + rad * c) / sw) * 2.0 - 1.0, 1.0 - ((cy + rad * s) / sh) * 2.0]
2923             };
2924             let (i1, o1) = (p(ra, c1, s1), p(rb, c1, s1));
2925             let (i2, o2) = (p(ra, c2, s2), p(rb, c2, s2));
2926             out.push(Vertex { position: i1, color: ca, clip_circle });
2927             out.push(Vertex { position: o1, color: cb, clip_circle });
2928             out.push(Vertex { position: o2, color: cb, clip_circle });
2929             out.push(Vertex { position: i1, color: ca, clip_circle });
2930             out.push(Vertex { position: o2, color: cb, clip_circle });
2931             out.push(Vertex { position: i2, color: ca, clip_circle });
2932         }
2933     }
2934 }
2935 
2936 pub fn push_arc_background_vertices(
2937     cx: f32, cy: f32, r: f32,
2938     thickness: f32,
2939     start_angle: f32, end_angle: f32,
2940     sw: f32, sh: f32,
2941     color: [f32; 4],
2942     segments: usize,
2943     clip_circle: [f32; 3],
2944     out: &mut Vec<Vertex>,
2945 ) {
2946     // The stroke band [r - thickness, r], with a half-px alpha ramp on each rim
2947     // (Gouraud across thin edge bands) so curved edges resolve smoothly instead
2948     // of hard-stepping — the poor-man's AA the flat pipeline doesn't provide.
2949     let f = 0.5f32.min(thickness * 0.25);
2950     let r_in = (r - thickness).max(0.0);
2951     // (inner radius, outer radius, alpha at inner rim, alpha at outer rim)
2952     let bands = [
2953         ((r_in - f).max(0.0), r_in + f, 0.0, color[3]),
2954         (r_in + f, r - f, color[3], color[3]),
2955         (r - f, r + f, color[3], 0.0),
2956     ];
2957     for i in 0..segments {
2958         let theta1 = start_angle + (i as f32) * (end_angle - start_angle) / (segments as f32);
2959         let theta2 = start_angle + ((i + 1) as f32) * (end_angle - start_angle) / (segments as f32);
2960         let (c1, s1) = (theta1.cos(), theta1.sin());
2961         let (c2, s2) = (theta2.cos(), theta2.sin());
2962         for &(ra, rb, aa, ab) in &bands {
2963             if rb <= ra {
2964                 continue;
2965             }
2966             let ca = [color[0], color[1], color[2], aa];
2967             let cb = [color[0], color[1], color[2], ab];
2968             let p = |rad: f32, c: f32, s: f32| -> [f32; 2] {
2969                 [((cx + rad * c) / sw) * 2.0 - 1.0, 1.0 - ((cy + rad * s) / sh) * 2.0]
2970             };
2971             let (i1, o1) = (p(ra, c1, s1), p(rb, c1, s1));
2972             let (i2, o2) = (p(ra, c2, s2), p(rb, c2, s2));
2973             out.push(Vertex { position: i1, color: ca, clip_circle });
2974             out.push(Vertex { position: o1, color: cb, clip_circle });
2975             out.push(Vertex { position: o2, color: cb, clip_circle });
2976             out.push(Vertex { position: i1, color: ca, clip_circle });
2977             out.push(Vertex { position: o2, color: cb, clip_circle });
2978             out.push(Vertex { position: i2, color: ca, clip_circle });
2979         }
2980     }
2981 }
2982 
2983 // `all(test, debug_assertions)`: the function under test only exists in
2984 // debug builds, so a `cargo test --release` must compile the module out too.
2985 #[cfg(all(test, debug_assertions))]
2986 mod near_roll_fallback_tests {
2987     use super::near_roll_fallback_reason;
2988     use crate::scene::layout::Rect;
2989 
2990     fn r(x: f32, y: f32, w: f32, h: f32) -> Rect {
2991         Rect { x, y, width: w, height: h }
2992     }
2993 
2994     const HOST: Rect = Rect { x: 0.0, y: 0.0, width: 800.0, height: 600.0 };
2995     const ROLL: f32 = 8.0;
2996 
2997     #[test]
2998     fn interior_carve_is_quiet() {
2999         // Well inside the deflated host: the overlay fallback is exact there.
3000         let carve = r(100.0, 100.0, 200.0, 100.0);
3001         assert_eq!(near_roll_fallback_reason(&carve, 6.0, &HOST, ROLL, &[], false), None);
3002     }
3003 
3004     #[test]
3005     fn shaded_region_reaching_the_roll_is_loud() {
3006         // Carve rect stops 3px short of the roll band, but its shaded region
3007         // (depth*0.5 + 2 = 5px) crosses in — the inflation must count.
3008         let carve = r(ROLL + 3.0, 100.0, 200.0, 100.0);
3009         assert_eq!(
3010             near_roll_fallback_reason(&carve, 6.0, &HOST, ROLL, &[], false),
3011             Some("the host's feature run is closed (another plate appended features since)")
3012         );
3013     }
3014 
3015     #[test]
3016     fn occlusion_is_named_before_run_contiguity() {
3017         let carve = r(2.0, 100.0, 200.0, 100.0);
3018         let occluder = r(150.0, 150.0, 100.0, 100.0);
3019         assert_eq!(
3020             near_roll_fallback_reason(&carve, 6.0, &HOST, ROLL, &[occluder], false),
3021             Some("a later plate overlaps the carve's shaded region")
3022         );
3023     }
3024 
3025     #[test]
3026     fn non_overlapping_later_plate_is_not_occlusion() {
3027         let carve = r(2.0, 100.0, 200.0, 100.0);
3028         let elsewhere = r(500.0, 400.0, 100.0, 100.0);
3029         assert_eq!(
3030             near_roll_fallback_reason(&carve, 6.0, &HOST, ROLL, &[elsewhere], false),
3031             Some("the host's feature run is closed (another plate appended features since)")
3032         );
3033     }
3034 
3035     #[test]
3036     fn budget_wins_over_every_other_reason() {
3037         let carve = r(2.0, 100.0, 200.0, 100.0);
3038         let occluder = r(150.0, 150.0, 100.0, 100.0);
3039         assert_eq!(
3040             near_roll_fallback_reason(&carve, 6.0, &HOST, ROLL, &[occluder], true),
3041             Some("the feature budget is full")
3042         );
3043     }
3044 }
3045 
3046 #[cfg(test)]
3047 mod frame_tests {
3048     use crate::scene::layout::Rect;
3049     use crate::scene::paint::PaintCtx;
3050 
3051     /// A frame is a plate turned inside out: its batch carries the HOLE as
3052     /// the SDF box, in mode 17, its cover quad is the face's bound, and it
3053     /// hosts the carves inside that bound as a Bevel does.
3054     #[test]
3055     fn a_frame_is_an_inside_out_plate_that_hosts_its_carves() {
3056         if !crate::layout::bevel_shader() {
3057             eprintln!("skipping: the shader plates are off in this configuration");
3058             return;
3059         }
3060         let (w, h, scale) = (400.0f32, 200.0f32, 1.0f32);
3061         let face = Rect { x: -10.0, y: 100.0, width: 420.0, height: 110.0 };
3062         let hole = Rect { x: 0.0, y: -100.0, width: 400.0, height: 224.0 };
3063         let mut pc = PaintCtx::new();
3064         let material = crate::scene::Material::opaque([0.3, 0.3, 0.35, 1.0]);
3065         pc.frame(face, hole, (0.0, 0.0, 20.0, 20.0), &material, 8.0);
3066         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);
3067         let dl = pc.finish();
3068         let (_, batches, _, features) = super::tessellate_display_list(&dl, w, h, scale);
3069         let plate = batches.iter().find_map(|b| b.plate.filter(|p| p.mode == 17.0)).expect("a mode-17 batch");
3070         assert_eq!(plate.rect, [200.0, 12.0, 200.0, 112.0], "the SDF box is the hole");
3071         assert_eq!(plate.radii[2], 20.0, "the hole's bottom corners are the coves");
3072         assert_eq!(features.len(), 1, "the carve inside the face groups into the frame");
3073     }
3074 }