graphic design tool
git clone https://git.lucas.co/cce-designer.git
fix: a point number under a plate is not drawn
All text is drawn after all geometry, so a number under a frosted plate
stood over it sharp while the scene beside it showed through blurred.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CLAUDE.md | 11 ++++++++++-
src/app.rs | 19 +++++++++++++++++++
src/main.rs | 37 +++++++++++++++++++++++++++++++++++++
src/render.rs | 50 +++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 109 insertions(+), 8 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 07b5d7a..5532802 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -2513,7 +2513,16 @@ stage pass, for the eye being staged; a label under 2% is not drawn, so
behind an OPAQUE face a number is hidden, where until then every number
showed through everything. The wires are not counted against a number: a
line a pixel wide is not in front of a label in any way one alpha could
-show. **A scene rebuild works the dimming out itself**, for the view the
+show. **A number under a plate is not drawn** (`State::under_a_plate`,
+`point_number_labels`, since 2026-09-29). A blur-behind plate samples the
+frame so far, which is geometry; the renderer draws ALL text in one pass
+after every batch, so a number under a plate stood over it, sharp, where
+the markers and wires beside it showed through frosted. Blurring it with
+the scene needs a text layer drawn ahead of the plates, which cce-ui does
+not have; under the pane tint a blurred 10 px number would not be read in
+any case. `a_point_number_under_a_plate_is_not_drawn` is the test.
+
+**A scene rebuild works the dimming out itself**, for the view the
scene was last staged from (`last_scene_mvp`, `last_scene_eye`). The 2D
frame is painted BEFORE the stage pass, so a rebuild that only cleared
the alphas drew one frame of every number at full strength; a playing
diff --git a/src/app.rs b/src/app.rs
index b99bc07..abd92da 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -2999,6 +2999,25 @@ impl State {
})
}
+ /// Whether (px, py) is under a plate drawn over the scene: a floating
+ /// pane, or the network's while it has one.
+ pub fn under_a_plate(&self, px: f32, py: f32) -> bool {
+ if self.over_floating_pane_at(px, py) {
+ return true;
+ }
+ if !self.show_network || self.is_detached_network {
+ return false;
+ }
+ if self.circular_network_pane {
+ return self.circular_network_layout.hit_test_content(px, py, 0.0, BREADCRUMB_H);
+ }
+ self.network_plate
+ && [NETWORK_PANEL_IDX, crate::slots::NETWORK_PANEL2_IDX].iter().any(|&idx| {
+ let (x, y, w, h) = self.positions[idx];
+ w > 0.0 && h > 0.0 && px >= x && px < x + w && py >= y && py < y + h
+ })
+ }
+
fn over_floating_pane(&self) -> bool {
self.over_floating_pane_at(self.cursor_x, self.cursor_y)
}
diff --git a/src/main.rs b/src/main.rs
index 57f19b1..21e581b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1686,6 +1686,43 @@ mod tests {
let _ = fs::remove_dir_all(dir.parent().unwrap());
}
+ /// A number under a plate is not drawn: the engine lays text out after
+ /// all geometry, so it would stand sharp over a plate that frosts
+ /// everything else behind it.
+ #[test]
+ fn a_point_number_under_a_plate_is_not_drawn() {
+ let mut state = State::new(false);
+ state.resize(1600.0, 900.0, 1.0);
+ state.rebuild_positions();
+ state.apply_layout();
+ state.show_point_numbers = true;
+ state.geo_opacity = 0.2;
+ state.show_occluded = true;
+ state.rebuild_scene_geometry();
+ let view = Mat4::look_at_rh(Vec3::new(0.0, 0.0, 6.0), Vec3::ZERO, Vec3::Y);
+ let proj = Mat4::perspective_rh(0.9, 1600.0 / 900.0, 0.1, 100.0);
+ state.last_scene_mvp = Some(proj * view);
+ state.last_scene_eye = Vec3::new(0.0, 0.0, 6.0);
+ state.last_scene_view_rect = (0.0, 0.0, 1600.0, 900.0);
+ state.sync_point_number_alpha(proj * view, state.last_scene_eye);
+
+ let (px, py, pw, ph) = state.positions[crate::slots::PARAM_IDX];
+ assert!(pw > 0.0 && ph > 0.0, "the params plate is laid out");
+ let all = state.point_number_labels();
+ assert!(!all.is_empty());
+ assert!(all.iter().all(|(_, x, y, ..)| !state.under_a_plate(*x, *y + 6.0)), "none stands under a plate");
+
+ // Put the params plate over the middle of the scene: the numbers
+ // there go, the rest stay.
+ state.positions[crate::slots::PARAM_IDX] = (700.0, 350.0, 200.0, 200.0);
+ let fewer = state.point_number_labels();
+ assert!(fewer.len() < all.len(), "{} of {} are left", fewer.len(), all.len());
+ assert!(!fewer.is_empty());
+ assert!(fewer.iter().all(|(_, x, y, ..)| !(*x >= 700.0 && *x < 900.0 && *y + 6.0 >= 350.0 && *y + 6.0 < 550.0)));
+ state.positions[crate::slots::PARAM_IDX] = (px, py, pw, ph);
+ assert_eq!(state.point_number_labels().len(), all.len());
+ }
+
/// A scene rebuild leaves the numbers dimmed as they were: the 2D frame
/// is painted before the stage pass, so a rebuild that cleared the
/// dimming drew one frame of every number at full strength, and a
diff --git a/src/render.rs b/src/render.rs
index 40fd455..1638e27 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -35,6 +35,10 @@ fn merge_bounds(a: Option<[f32; 4]>, b: Option<[f32; 4]>) -> Option<[f32; 4]> {
/// this colour, and nothing else in the pane is.
const BYPASS_TINT: [f32; 3] = [1.0, 0.74, 0.18];
+/// The point numbers' font size, logical px.
+const POINT_NUMBER_PX: f32 = 10.0;
+
+
impl State {
/// Per-corner plate radii for a pane rect: a corner that sits ON a window
/// corner is this pane's share of the window silhouette — the compositor
@@ -964,9 +968,32 @@ impl State {
/// camera or pane changes (`stage_frame`), so the labels track orbits;
/// a frame staged before the first scene staging simply draws none.
fn append_point_numbers(&self, pc: &mut PaintCtx) {
- if !self.show_viewport {
+ let labels = self.point_number_labels();
+ if labels.is_empty() {
return;
}
+ let (vx, vy, vw, vh) = self.last_scene_view_rect;
+ pc.clip(rect(vx, vy, vw, vh), |pc| {
+ for (text, x, y, color, alpha) in labels {
+ pc.text_faded(text, x, y, POINT_NUMBER_PX, color, alpha, None, None);
+ }
+ });
+ }
+
+ /// The numbers as they are drawn: text, where, colour and strength.
+ ///
+ /// **A number under a plate is not drawn.** A plate frosts what is
+ /// behind it, and the scene's markers and wires show through one
+ /// blurred; but the engine lays ALL text out after all geometry, so a
+ /// number under a plate would be drawn over it, sharp, where everything
+ /// beside it is frosted. It cannot be blurred with the scene from here,
+ /// and under the pane tint a blurred 10 px number would not be read
+ /// anyway.
+ pub(crate) fn point_number_labels(&self) -> Vec<(String, f32, f32, [u8; 3], f32)> {
+ let mut out = Vec::new();
+ if !self.show_viewport {
+ return out;
+ }
// Points, primitives, vertices: each its own colour, and its own
// nudge off the place it names — a point's number beside its
// marker, the other two about on the spot, which is theirs alone.
@@ -976,14 +1003,14 @@ impl State {
(&self.overlay_vertex_labels, &self.overlay_vertex_alpha, VERTEX_LABEL_COLOR, (-3.0, -5.0)),
];
if lists.iter().all(|(labels, ..)| labels.is_empty()) {
- return;
+ return out;
}
- let Some(mvp) = self.last_scene_mvp else { return };
+ let Some(mvp) = self.last_scene_mvp else { return out };
let (vx, vy, vw, vh) = self.last_scene_view_rect;
if vw <= 0.0 || vh <= 0.0 {
- return;
+ return out;
}
- pc.clip(rect(vx, vy, vw, vh), |pc| {
+ {
for (labels, alphas, color, (dx, dy)) in lists {
for (i, (pos, idx)) in labels.iter().enumerate() {
// What the fill in front of the place lets through; a
@@ -1002,10 +1029,19 @@ impl State {
}
let sx = vx + (ndc.x * 0.5 + 0.5) * vw;
let sy = vy + (0.5 - ndc.y * 0.5) * vh;
- pc.text_faded(idx.to_string(), sx + dx, sy + dy, 10.0, color, alpha, None, None);
+ let text = idx.to_string();
+ // Both ends of the label, at about its middle height.
+ let (lx, ly) = (sx + dx, sy + dy);
+ let wide = text.len() as f32 * POINT_NUMBER_PX * 0.62;
+ let mid = ly + POINT_NUMBER_PX * 0.6;
+ if self.under_a_plate(lx, mid) || self.under_a_plate(lx + wide, mid) {
+ continue;
+ }
+ out.push((text, lx, ly, color, alpha));
}
}
- });
+ }
+ out
}
/// The view's scale on the pivot plane, bottom-left of the pane: `1:2.3`