git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

commitb04ff5c42c227815f312de77202fcb5b40d92c9f
parented33cd6516
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-29 20:14
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`