graphic design tool
git clone https://git.lucas.co/cce-designer.git
feat(network): node names get a compressed floor
With node_compression set, each node name the network draws stands on a
floor of the node bodies' material, as each params row does under
param.backdrop_compression, so a name over a bright model stays readable
when the pane plate is faint.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 16 ++++++++++++++++
src/dialog.rs | 2 +-
src/render.rs | 43 +++++++++++++++++++++++++++++++++++++++++++
3 files changed, 60 insertions(+), 1 deletion(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 83477c2..aefe7ee 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -2712,6 +2712,22 @@ Until then the graph filled itself with `cell_color` (chosen by a
`gap_color`; all three keys are retired, reported by path at load and not
read.
The next section is about dropping the pane plate entirely.
+
+**Floors: a faint plate, compressed content** (since 2026-10-06). The way
+to make the plates recede and keep what is on them readable is to compress
+the scene under the CONTENT rather than under the whole plate:
+`style.surface.param.backdrop_compression` puts a floor of the pane
+material under each params row (cce-ui's `paint_row_floors`), and
+`style.surface.graph.node_compression` sets the node bodies' compression
+and, set at all, puts the same floor behind each node's NAME
+(`State::node_label_floors`, from the graph's own laid-out text, drawn in
+the bodies' run so they share its blur snapshot). Compression pulls the
+backdrop's luminance toward the material's tint RGB and ignores its alpha,
+so a DARK pane colour at a low alpha (`#2020280d`) is a plate that barely
+shows and a dark key under light ink: the mid-grey `#6c6c7b` it replaced
+capped full compression at about 3.3:1 against `#ccccd4`. 0.85 was where
+labels read over a bright model in a shadow session; 0.6 left them grey
+on grey.
The params widget alone also reads `style.surface.param.backdrop_compression`.
The rules live in cce-ui's CLAUDE.md ("There is one roll width", "The
relief is two shapes", "Frost is one block"); this is the designer's view of
diff --git a/src/dialog.rs b/src/dialog.rs
index bd8ee62..7f9fd24 100644
--- a/src/dialog.rs
+++ b/src/dialog.rs
@@ -902,7 +902,7 @@ impl Layout for Dialog {
/// through usvg's font database and measured a choice's value some 8 px
/// wider than it drew, which put the right arrow twice as far from it as
/// the left; it is the fallback for a font system that shapes nothing.
-fn shaped_width(text: &str, family: &str, font_size: f32) -> f32 {
+pub(crate) fn shaped_width(text: &str, family: &str, font_size: f32) -> f32 {
cce_ui::geometry_font_system()
.lock()
.ok()
diff --git a/src/render.rs b/src/render.rs
index 47a1d1a..84d19c5 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -510,6 +510,13 @@ impl State {
// at the network opacity, a flat blue-grey wash that predated
// the plate and made the network pane the one pane with a hue.
+ // A node's name hangs off its body onto whatever is behind the
+ // pane, so with the plate faint it stands on the bare scene. With
+ // `node_compression` set it gets a floor of the bodies' material,
+ // as each params row does (`param.backdrop_compression`).
+ let label_floors =
+ if self.node_compression.is_some() { self.node_label_floors(w) } else { Vec::new() };
+
pc.clip(clip, |pc| {
// Node bodies wear the parameter plate's fill exactly — same
// tint, opacity, and blur-behind marker (param_plate_fill) —
@@ -616,6 +623,10 @@ impl State {
// what still says so while the node is selected and its
// roll is the selection's colour.
let mut bars: Vec<cce_ui::scene::layout::Rect> = Vec::new();
+ // In the bodies' run, so they share its blur snapshot.
+ for (floor, r) in &label_floors {
+ pc.fill_material(*floor, (*r, *r, *r, *r), &node_mat);
+ }
for (qx, qy, qw, qh, highlighted, off) in bodies {
if highlighted {
pc.bevel_tinted(rect(qx, qy, qw, qh), radii, &node_mat, node_bevel, hl_tint);
@@ -986,6 +997,38 @@ impl State {
/// graph's clamped to the network pane (and distance-filtered against the circular
/// pane), network text fading with `network_opacity`. Popovers follow in
/// `append_popovers`.
+ /// A floor behind each node name a network editor draws, with its corner
+ /// radius: the labels as the graph lays them out (read back from its
+ /// text, so the left-hand flip is the widget's own), measured as
+ /// shaped, padded a little. A name the circular pane drops in
+ /// `append_frame_text` gets no floor either.
+ fn node_label_floors(&self, w: &dyn WidgetHost) -> Vec<(Rect, f32)> {
+ let (ncx, ncy, ncr) =
+ (self.circular_network_layout.x, self.circular_network_layout.y, self.circular_network_layout.r);
+ let mut scratch = PaintCtx::new();
+ append_widget_text(&self.ui_context, w, &mut scratch);
+ scratch
+ .finish()
+ .items
+ .into_iter()
+ .filter_map(|item| {
+ let Prim::Text { text, x, y, font_size, font, .. } = item.prim else { return None };
+ if self.circular_network_pane && (x - ncx).powi(2) + (y - ncy).powi(2) > ncr * ncr {
+ return None;
+ }
+ let family = match &font {
+ Some(f) => cce_ui::layout::split_font_string(f).0.to_string(),
+ None => cce_ui::layout::graph_node_font_parsed().0,
+ };
+ let tw = crate::dialog::shaped_width(&text, &family, font_size);
+ let (px, py) = (font_size * 0.4, font_size * 0.2);
+ let h = font_size + 2.0 * py;
+ let r = cce_ui::layout::control_corner_radius().min(h * 0.5);
+ Some((rect(x - px, y - py, tw + 2.0 * px, h), r))
+ })
+ .collect()
+ }
+
fn append_frame_text(&self, pc: &mut PaintCtx) {
let circular = self.circular_network_pane;
let ncx = self.circular_network_layout.x;