GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat(relief): Prim::CarveUnion — several boxes carved or raised as one wall
One Recess per box shades every box's full outline: where two overlap
each wall runs through the other's interior, and where walls cross the
shadings stack in colour space. Shader mode 14 puts the boxes in the
frame's feature buffer as a run and takes the NEAREST one per pixel —
the union SDF — so the outline is evaluated once, interior walls vanish,
and an inside corner is a sharp mitre. Over-budget unions keep as many
boxes as fit and say so under CCE_PLATE_DEBUG. Mirrored in the
height-field export with a test; examples/carve_union_demo.rs shows an
L and a plus beside their per-box overlays.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
CLAUDE.md | 2 +-
examples/carve_union_demo.rs | 93 ++++++++++++++++++++++++++++++++++++++++++++
src/backend/window_runner.rs | 78 ++++++++++++++++++++++++++++++++++++-
src/scene/heightfield.rs | 54 ++++++++++++++++++++++++-
src/scene/paint.rs | 42 ++++++++++++++++++--
src/vk/renderer.rs | 8 ++--
src/vk/shader2d.wgsl | 22 +++++++++++
7 files changed, 289 insertions(+), 10 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index d355287..a75aa7e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -23,7 +23,7 @@ set outright.
too — every `glyphon::` item used here was a cosmic-text re-export, and dropping it takes
wgpu out of the build. There is no HTML/DOM — the UI is GPU primitives (quads, rounded rects with
per-corner radii, vectors with caps, arcs, circles, and the **relief primitives** — the
- lit-surface family: bevels, plates, recesses, bosses, ridges, fillets, grooves; see the
+ lit-surface family: bevels, plates, recesses, bosses, ridges, fillets, grooves, lattices, box unions; see the
`Prim` enum doc in `src/scene/paint.rs`). Tessellators live in
`backend/window_runner.rs` and are re-exported through `src/engine.rs`.
- It is **both a library and a binary.** `src/lib.rs` is the toolkit; `src/main.rs` is
diff --git a/examples/carve_union_demo.rs b/examples/carve_union_demo.rs
new file mode 100644
index 0000000..93951a2
--- /dev/null
+++ b/examples/carve_union_demo.rs
@@ -0,0 +1,93 @@
+//! A visual check of [`cce_ui::scene::paint::Prim::CarveUnion`] against the
+//! per-box overlays it replaces. Left column: an L and a plus drawn as ONE
+//! union carve (recess above, boss below). Right column: the same L and plus
+//! drawn as one recess/boss per box — the crossing walls and stacked shading
+//! the union exists to remove. Run inside a Wayland session (a cce-shadow
+//! instance works): `cargo run --release -p cce-ui --example carve_union_demo`.
+
+use cce_ui::engine::{Application, EngineState, LogicalPosition, LogicalSize, WindowSettings};
+use cce_ui::scene::layout::Rect;
+use cce_ui::scene::paint::{DisplayList, PaintCtx};
+use cce_ui::widget::{ElementState, KeyEvent, MouseButton, MouseScrollDelta};
+use wayland_client::QueueHandle;
+
+struct DemoApp;
+
+fn l_shape(x: f32, y: f32) -> Vec<(Rect, (f32, f32, f32, f32))> {
+ let r = (8.0, 8.0, 8.0, 8.0);
+ vec![
+ (Rect { x, y, width: 260.0, height: 70.0 }, r),
+ (Rect { x, y, width: 70.0, height: 200.0 }, r),
+ ]
+}
+
+fn plus_shape(x: f32, y: f32) -> Vec<(Rect, (f32, f32, f32, f32))> {
+ let r = (10.0, 10.0, 10.0, 10.0);
+ vec![
+ (Rect { x, y: y + 70.0, width: 220.0, height: 60.0 }, r),
+ (Rect { x: x + 80.0, y, width: 60.0, height: 200.0 }, r),
+ ]
+}
+
+impl Application for DemoApp {
+ type Message = ();
+
+ fn new(_qh: &QueueHandle<EngineState<Self>>, _sender: calloop::channel::Sender<()>) -> Self {
+ Self
+ }
+
+ fn settings(&self) -> WindowSettings {
+ WindowSettings {
+ title: "carve union demo".into(),
+ app_id: "cce-carve-union-demo".into(),
+ width: 760,
+ height: 560,
+ fullscreen: false,
+ min_size: None,
+ }
+ }
+
+ fn update(&mut self, _msg: (), _needs_rebuild: &mut bool, _exit: &mut bool) {}
+
+ fn tick(&mut self, _dt: f32, _needs_rebuild: &mut bool) {}
+
+ fn display_list(&mut self, size: LogicalSize, _scale: f64) -> Option<DisplayList> {
+ let mut pc = PaintCtx::new();
+ let (w, h) = (size.width as f32, size.height as f32);
+ pc.plate(
+ Rect { x: 0.0, y: 0.0, width: w, height: h },
+ (12.0, 12.0, 12.0, 12.0),
+ [0.42, 0.44, 0.50, 1.0],
+ cce_ui::layout::bevel_width(),
+ );
+ let wall = 10.0;
+ // Left column: unions.
+ pc.carve_union(l_shape(40.0, 40.0), wall, false);
+ pc.carve_union(plus_shape(60.0, 300.0), wall, true);
+ // Right column: one overlay per box — the look being replaced.
+ for (r, radii) in l_shape(420.0, 40.0) {
+ pc.recess(r, radii, wall);
+ }
+ for (r, radii) in plus_shape(440.0, 300.0) {
+ pc.boss(r, radii, wall);
+ }
+ Some(pc.finish())
+ }
+
+ fn display_list_text(&self) -> bool {
+ true
+ }
+
+ fn handle_pointer_move(&mut self, _pos: LogicalPosition, _needs_rebuild: &mut bool) {}
+ fn handle_mouse_input(&mut self, _b: MouseButton, _s: ElementState, _p: LogicalPosition, _r: &mut bool) -> Option<()> {
+ None
+ }
+ fn handle_mouse_wheel(&mut self, _d: &MouseScrollDelta, _p: LogicalPosition, _r: &mut bool) {}
+ fn handle_key_input(&mut self, _e: &KeyEvent, _r: &mut bool) -> Option<()> {
+ None
+ }
+}
+
+fn main() {
+ cce_ui::engine::run::<DemoApp>();
+}
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index a1d69f7..e1a6edf 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -1712,7 +1712,8 @@ fn prim_kind(p: &crate::scene::paint::Prim) -> &'static str {
P::Sphere { .. } => "Sphere", P::Droplet { .. } => "Droplet",
P::DropletScrim { .. } => "DropletScrim",
P::ConcaveFillet { .. } => "ConcaveFillet",
- P::Groove { .. } => "Groove", P::Lattice { .. } => "Lattice", P::Glow { .. } => "Glow",
+ P::Groove { .. } => "Groove", P::Lattice { .. } => "Lattice",
+ P::CarveUnion { .. } => "CarveUnion", P::Glow { .. } => "Glow",
P::Text { .. } => "Text", P::Image { .. } => "Image",
}
}
@@ -2566,6 +2567,81 @@ pub fn tessellate_display_list(
// Legacy banded path: no periodic wall — the lattice draws nothing
// there, like the fillet (A/B comparison path only).
Prim::Lattice { .. } => {}
+ Prim::CarveUnion { boxes, depth, raised } if shader_plates => {
+ // The union of several boxes as ONE wall (shader mode 14): the
+ // boxes go into the frame's feature buffer as a contiguous run
+ // and the shader takes the nearest one per pixel. The cover
+ // quad is the union's bounding box grown by the wall's reach;
+ // off-shape corners of it sit at the plateau and shade nothing.
+ let budget = crate::vk::MAX_PLATE_FEATURES.saturating_sub(features.len());
+ let take = boxes.len().min(budget);
+ if take < boxes.len() && plate_debug() {
+ eprintln!(
+ "plate-carve: union of {} boxes gets {} — feature budget full ({} used)",
+ boxes.len(), take, features.len()
+ );
+ }
+ if take == 0 {
+ continue;
+ }
+ let kept = &boxes[..take];
+ let (mut x0, mut y0, mut x1, mut y1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
+ for (r, _) in kept {
+ x0 = x0.min(r.x);
+ y0 = y0.min(r.y);
+ x1 = x1.max(r.x + r.width);
+ y1 = y1.max(r.y + r.height);
+ }
+ let infl = *depth * 0.5 + 2.0;
+ verts.extend(quad_vertices(
+ x0 - infl, y0 - infl,
+ (x1 - x0) + 2.0 * infl, (y1 - y0) + 2.0 * infl,
+ sw, sh, [0.0; 4],
+ ));
+ let off = features.len() as f32;
+ for (r, radii) in kept {
+ features.push([
+ (r.x + r.width * 0.5) * scale,
+ (r.y + r.height * 0.5) * scale,
+ r.width * 0.5 * scale,
+ r.height * 0.5 * scale,
+ radii.0 * scale,
+ radii.1 * scale,
+ radii.2 * scale,
+ radii.3 * scale,
+ *depth * scale,
+ 0.0,
+ 0.0,
+ 0.0,
+ ]);
+ }
+ // The run is complete: a plate with an open feature run must
+ // not append past it (its features would no longer be
+ // contiguous), so it is closed here like any other appender.
+ last_feature_plate = None;
+ plate = Some(crate::vk::PlatePush {
+ rect: [
+ (x0 + x1) * 0.5 * scale,
+ (y0 + y1) * 0.5 * scale,
+ (x1 - x0) * 0.5 * scale,
+ (y1 - y0) * 0.5 * scale,
+ ],
+ // x: the raised flag; the shader reads nothing else here.
+ radii: [if *raised { 1.0 } else { 0.0 }, 0.0, 0.0, 0.0],
+ light: [plate_light[0], plate_light[1], plate_light[2], *depth * scale],
+ material: plate_mat,
+ // Feature run [offset, count] (the renderer rebases the
+ // offset onto the frame slot, as for mode 1); zw far out
+ // so the host-box fade never applies.
+ host: [off, take as f32, 1e6, 1e6],
+ specular_tint: [1.0, 1.0, 1.0, 0.0],
+ mode: 14.0,
+ shape: crate::layout::corner_shape(),
+ });
+ }
+ // Legacy banded path: no union — nothing is drawn there, like the
+ // fillet and the lattice (A/B comparison path only).
+ Prim::CarveUnion { .. } => {}
}
let end = verts.len() as u32;
if end == start {
diff --git a/src/scene/heightfield.rs b/src/scene/heightfield.rs
index 3fddd99..64352ce 100644
--- a/src/scene/heightfield.rs
+++ b/src/scene/heightfield.rs
@@ -218,6 +218,7 @@ const MODE_GROOVE: i32 = 8;
const MODE_TROUGH: i32 = 9;
const MODE_ROLL: i32 = 11;
const MODE_LATTICE: i32 = 13;
+const MODE_UNION: i32 = 14;
impl HeightField {
/// Sample one frame's plate batches, in draw order, over a `width` ×
@@ -238,7 +239,7 @@ impl HeightField {
for b in batches {
let Some(p) = b.plate else { continue };
let mode = p.mode.round() as i32;
- if !matches!(mode, 1..=9 | 11 | 13) {
+ if !matches!(mode, 1..=9 | 11 | 13 | 14) {
continue;
}
let shape = p.shape.clamp(2.0, 16.0);
@@ -258,6 +259,13 @@ impl HeightField {
// batch does not record; the one consumer (cce-grid) covers
// its whole surface, so the window is the honest bound.
MODE_GROOVE | MODE_LATTICE => (0.0, 0.0, width as f32, height as f32),
+ // A union's push rect is its boxes' bounding box.
+ MODE_UNION => (
+ p.rect[0] - p.rect[2] - t - 2.0,
+ p.rect[1] - p.rect[3] - t - 2.0,
+ p.rect[0] + p.rect[2] + t + 2.0,
+ p.rect[1] + p.rect[3] + t + 2.0,
+ ),
_ => (
p.rect[0] - p.rect[2] - t - 2.0,
p.rect[1] - p.rect[3] - t - 2.0,
@@ -347,6 +355,21 @@ impl HeightField {
let u = (1.0 - d_out / t).clamp(0.0, 1.0);
-carve_drop * prof.carve_height(u)
}
+ MODE_UNION => {
+ // Nearest box of the run — the union SDF — through
+ // one profile (see the shader's MODE_UNION).
+ let mut best = f32::MAX;
+ for feat in features.iter().skip(f_off).take(f_cnt) {
+ let fd = rr_sdf(pt, [feat[0], feat[1], feat[2], feat[3]], [feat[4], feat[5], feat[6], feat[7]], shape, t);
+ best = best.min(fd);
+ }
+ if best == f32::MAX {
+ continue;
+ }
+ let u = (-best / t + 0.5).clamp(0.0, 1.0);
+ let sign = if p.radii[0] > 0.5 { 1.0 } else { -1.0 };
+ sign * carve_drop * prof.carve_height(u)
+ }
MODE_FILLET_DOWN | MODE_FILLET_UP => {
let (cx, cy) = (pt.0 - p.rect[0], pt.1 - p.rect[1]);
let dist = (cx * cx + cy * cy).sqrt().max(1e-4);
@@ -561,6 +584,35 @@ mod tests {
assert!((w1 - w2).abs() < 1e-3, "walls symmetric {w1} {w2}");
}
+ #[test]
+ fn a_carve_union_is_one_wall_around_the_union_of_its_boxes() {
+ // An L: a 60×20 bar across the top and a 20×60 bar down the left,
+ // sharing the corner square (10..30). Wall 8, radii 4 (fixture).
+ let bar_h = [40.0, 20.0, 30.0, 10.0, 4.0, 4.0, 4.0, 4.0, 8.0, 0.0, 0.0, 0.0];
+ let bar_v = [20.0, 40.0, 10.0, 30.0, 4.0, 4.0, 4.0, 4.0, 8.0, 0.0, 0.0, 0.0];
+ let mut b = plate([40.0, 40.0, 30.0, 30.0], 8.0, [0.0, 2.0, 1e6, 1e6]);
+ b.plate.as_mut().unwrap().mode = 14.0;
+ b.plate.as_mut().unwrap().radii = [0.0; 4];
+ let hf = HeightField::from_frame(&[b], &[bar_h, bar_v], 100, 100, 1.0);
+ let at = |x: usize, y: usize| hf.px[y * 100 + x];
+ let drop = RECESS_DEPTH * 8.0;
+ // Deep inside either bar: the full drop, once.
+ assert!((at(55, 20) + drop).abs() < 1e-3, "top bar {}", at(55, 20));
+ assert!((at(20, 55) + drop).abs() < 1e-3, "left bar {}", at(20, 55));
+ // The shared corner square lies inside BOTH boxes. With one recess
+ // per box, each box's wall would run straight through the other's
+ // interior here (the vertical bar's right wall at x = 30 crosses the
+ // top bar). As a union the interior is flat floor: exactly one drop.
+ assert!((at(25, 20) + drop).abs() < 1e-3, "corner interior {}", at(25, 20));
+ assert!((at(20, 25) + drop).abs() < 1e-3, "corner interior {}", at(20, 25));
+ // Well outside: the base.
+ assert_eq!(at(80, 80), 0.0);
+ // The wall straddles the union outline by ±t/2 = 4: sampled 2 px
+ // outside the top bar's lower edge (y = 30), on the wall.
+ let wall = at(55, 32);
+ assert!(wall < 0.0 && wall > -drop, "wall {wall}");
+ }
+
#[test]
fn resample_keeps_the_face_height() {
let b = plate([50.0, 50.0, 40.0, 40.0], 8.0, [0.0; 4]);
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index aef49cf..ab10558 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -124,15 +124,15 @@ impl PlateSpec {
/// The **relief primitives** are the members of this enum that describe a lit
/// surface rather than a flat fill: [`Prim::Bevel`], [`Prim::Plate`],
/// [`Prim::Recess`], [`Prim::Boss`], [`Prim::Ridge`], [`Prim::ConcaveFillet`],
-/// [`Prim::Groove`], [`Prim::Lattice`] and [`Prim::Sphere`]. They share one lighting model — the
+/// [`Prim::Groove`], [`Prim::Lattice`], [`Prim::CarveUnion`] and [`Prim::Sphere`]. They share one lighting model — the
/// DE's light vector, roll width and profile, per-pixel through shader2d's
/// SDF branch (see `crate::layout::bevel_shader`) — and split in two:
///
/// - **plates** carry their own fill: `Bevel`, `Plate`. Shader mode 1.
/// - **carves** emit shading ONLY, no fill, over whatever is already painted
-/// beneath: `Recess`, `Boss`, `Ridge`, `ConcaveFillet`, `Groove`, `Lattice`.
-/// Modes 2-4, 6-8 and 13. (`Sphere`, mode 5, is neither — a lit ball under
-/// the same model.)
+/// beneath: `Recess`, `Boss`, `Ridge`, `ConcaveFillet`, `Groove`, `Lattice`,
+/// `CarveUnion`. Modes 2-4, 6-8, 13 and 14. (`Sphere`, mode 5, is neither —
+/// a lit ball under the same model.)
///
/// That split is load-bearing for flat-path hosts, which need one list for the
/// faces and another for the edges drawn over them (cce-files' `rects` vs
@@ -598,6 +598,28 @@ pub enum Prim {
/// surface holds (a free carve per cell also runs into the per-frame
/// feature budget long before a zoomed-out grid does). SDF path only.
Lattice { rect: Rect, period: (f32, f32), origin: (f32, f32), cell: (f32, f32), radius: f32, depth: f32 },
+ /// Several rounded boxes carved (`raised` false) or raised (`raised`
+ /// true) as ONE shape: the union of the boxes is the well, and its wall
+ /// follows the union's outline — straddling it by ±`depth`/2 like every
+ /// carve boundary — through a single profile evaluation per pixel. An L,
+ /// a T, a plus, a slot with a round end: any outline boxes can compose.
+ ///
+ /// The alternative, one [`Prim::Recess`] per box, is N overlays that
+ /// each shade their own full outline: where two boxes overlap, each
+ /// draws a wall straight through the other's interior, and where their
+ /// walls cross the shadings stack in colour space — the junction reads
+ /// as two effects laid over each other, not one shape. Here the pixel's
+ /// distance is to the NEAREST box (the union SDF), so a box's wall
+ /// vanishes wherever it runs inside another, and an inside corner is a
+ /// sharp mitre (round it with [`Prim::ConcaveFillet`] if it must be
+ /// concave-rounded — the union has no radius there by construction).
+ ///
+ /// The boxes ride the frame's plate-feature buffer (the same slots CSG
+ /// carves use, 64 per frame), so a union costs one draw plus one slot
+ /// per box. When the budget cannot hold all of a union's boxes the
+ /// tessellator keeps as many as fit — a degraded shape rather than none —
+ /// and says so under `CCE_PLATE_DEBUG`. SDF path only.
+ CarveUnion { boxes: Vec<(Rect, Radii)>, depth: f32, raised: bool },
/// Text in sRGB u8 (the `TextLabel` convention). `font` is a font string for
/// `get_text_buffer` (family, or "family:size"); `bounds` is a logical `[l, t, r, b]` clip
/// for the glyph pass (Phase 6: the backend renders these through the glyph pass when the app
@@ -983,6 +1005,7 @@ impl PaintCtx {
Prim::Lattice { rect, period, origin, cell, radius, depth } => {
self.lattice(rect, period, origin, cell, radius, depth)
}
+ Prim::CarveUnion { boxes, depth, raised } => self.carve_union(boxes, depth, raised),
Prim::Image { image, rect, alpha } => self.image(image, rect, alpha),
}
None
@@ -1018,6 +1041,17 @@ impl PaintCtx {
self.push(Prim::Lattice { rect, period, origin: (origin.0 + ox, origin.1 + oy), cell, radius, depth });
}
+ /// Carve (or raise, with `raised`) the union of `boxes` as one shape with
+ /// one wall — see [`Prim::CarveUnion`]. `depth` is the wall's run in px,
+ /// as for [`PaintCtx::recess`].
+ pub fn carve_union(&mut self, boxes: Vec<(Rect, Radii)>, depth: f32, raised: bool) {
+ let boxes: Vec<(Rect, Radii)> = boxes.into_iter().map(|(r, radii)| (self.apply_offset(r), radii)).collect();
+ if boxes.is_empty() {
+ return;
+ }
+ self.push(Prim::CarveUnion { boxes, depth, raised });
+ }
+
pub fn border(&mut self, rect: Rect, radii: Radii, fill: [f32; 4], border: [f32; 4], thickness: f32) {
let rect = self.apply_offset(rect);
self.push(Prim::Border { rect, radii, fill, border, thickness });
diff --git a/src/vk/renderer.rs b/src/vk/renderer.rs
index 2ee656b..b61cfb7 100644
--- a/src/vk/renderer.rs
+++ b/src/vk/renderer.rs
@@ -85,7 +85,8 @@ pub struct PlatePush {
pub material: [f32; 4],
/// Mode 1: `[feature offset, feature count, 0, 0]` into the frame's
/// `plate_features` — the carves CSG'd out of this plate (the renderer adds
- /// the frame slot's base offset at record time). Mode 2: the host-plate box
+ /// the frame slot's base offset at record time). Mode 14 uses the same
+ /// `[offset, count]` for the union's boxes. Mode 2: the host-plate box
/// (center + half-extents) a free recess fades out against; far-away sides
/// (±1e5) disable the fade.
pub host: [f32; 4],
@@ -1935,9 +1936,10 @@ impl VkRenderer {
pc[20..24].copy_from_slice(&p.material);
pc[24..28].copy_from_slice(&p.host);
pc[28..32].copy_from_slice(&p.specular_tint);
- if p.mode == 1.0 {
+ if p.mode == 1.0 || p.mode == 14.0 {
// Rebase the feature offset onto this
- // frame's UBO slot.
+ // frame's UBO slot (a plate's CSG carves,
+ // or a union carve's boxes).
pc[24] += (frame_index * MAX_PLATE_FEATURES) as f32;
}
}
diff --git a/src/vk/shader2d.wgsl b/src/vk/shader2d.wgsl
index 63ac75e..4ccc3f6 100644
--- a/src/vk/shader2d.wgsl
+++ b/src/vk/shader2d.wgsl
@@ -140,6 +140,7 @@ const MODE_DROPLET: i32 = 10; // hanging water droplet clinging to the box t
const MODE_ROLL: i32 = 11; // fill-less rolled perimeter, composited as an overlay
const MODE_DROPLET_SCRIM: i32 = 12; // flat feathered fill of the droplet silhouette
const MODE_LATTICE: i32 = 13; // periodic well field: nearest-cell carve, one evaluation
+const MODE_UNION: i32 = 14; // union of feature boxes carved/raised as one wall
// Fillet modes rejoin the shared free-carve path as their flat equivalents.
const FILLET_TO_STEP: i32 = 4; // 6 -> RECESS, 7 -> BOSS
@@ -694,6 +695,27 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
fd = -lg.z + 0.5 * t;
fgd = lg.xy;
eff = MODE_RECESS;
+ } else if (mode == MODE_UNION) {
+ // MODE_UNION: the boxes in the feature run p_host.xy = [offset,
+ // count] are one shape — the pixel's distance is to the NEAREST box
+ // (the union SDF, min over the run), with that box's gradient, so a
+ // box's wall vanishes inside another and the outline is evaluated
+ // once. p_radii.x = 1 raises the union (boss) instead of carving it.
+ let u_off = u32(rrect_clip.p_host.x);
+ let u_cnt = u32(rrect_clip.p_host.y);
+ var best = 1e9;
+ var bgrad = vec2f(0.0, -1.0);
+ for (var i = 0u; i < u_cnt; i = i + 1u) {
+ let feat = plate_features.items[u_off + i];
+ let fg = rr_sdf_grad(frag, feat.rect, feat.radii);
+ if (fg.z < best) {
+ best = fg.z;
+ bgrad = fg.xy;
+ }
+ }
+ fd = -best;
+ fgd = bgrad;
+ eff = select(MODE_RECESS, MODE_BOSS, rrect_clip.p_radii.x > 0.5);
} else if (mode == MODE_FILLET_DOWN || mode == MODE_FILLET_UP) {
eff = mode - FILLET_TO_STEP;
let c = frag - rrect_clip.p_rect.xy;