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

commit3f31a1d47031cd00a73f2f49791cbcdd5eb16f21
parente6de7c7745
authorLucas Galante <[email protected]>
date2026-08-25 10:46
feat: meta Wireframe — per-node wire overlay

Third meta pref: Wireframe draws the flagged node's triangle edges as a
LINE_LIST wire pass in the geometry's own colors, over the scene fill —
the sphere_edges expansion scoped to one node, riding the same overlay
rebuild as the markers. The fill takes its slope-scaled offset whenever
ANY wires are coming (the global toggle's or a node's), so the lattice
reads solid either way. Existing metas gain the param through the
ensure migration; no scene-format change.

 src/app.rs    | 33 ++++++++++++++++++++++++++++-----
 src/main.rs   | 19 +++++++++++--------
 src/render.rs | 31 +++++++++++++++++++++++++------
 3 files changed, 64 insertions(+), 19 deletions(-)

diff --git a/src/app.rs b/src/app.rs
index c5cdbd5..c37cc1b 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -327,7 +327,9 @@ pub fn ensure_meta_on(node: &mut FsNode) {
             });
         }
         let meta = node.children.iter_mut().find(|c| c.node_type == "meta").unwrap();
-        for (name, default) in [("Point Markers", "false"), ("Point Numbers", "false")] {
+        for (name, default) in
+            [("Point Markers", "false"), ("Point Numbers", "false"), ("Wireframe", "false")]
+        {
             if !meta.params.iter().any(|p| p.name == name) {
                 meta.params.push(ParamDef {
                     name: name.to_string(),
@@ -805,6 +807,8 @@ pub struct SceneMeshes {
     pub group_points: cce_ui::vk::MeshId,
     /// Per-node meta "Point Markers" overlay.
     pub meta_points: cce_ui::vk::MeshId,
+    /// Per-node meta "Wireframe" overlay (LINE_LIST edge pairs).
+    pub meta_wires: cce_ui::vk::MeshId,
 }
 
 /// A left-press on the detached circular window's chrome that becomes an
@@ -1075,6 +1079,10 @@ pub struct State {
     pub meta_points_dirty: bool,
     pub meta_point_count: u32,
     pub meta_number_labels: Vec<([f32; 3], u32)>,
+    /// Per-node meta "Wireframe": LINE_LIST edge pairs of the flagged nodes'
+    /// triangles, drawn as a wire pass over the scene fill.
+    pub meta_wire_verts: Vec<Vertex3D>,
+    pub meta_wire_count: u32,
     /// The raster scene's model-view-projection and the viewport pane rect in
     /// LOGICAL px, cached at staging so the 2D pass can project 3D overlays.
     pub last_scene_mvp: Option<Mat4>,
@@ -3252,6 +3260,8 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
             meta_points_dirty: false,
             meta_point_count: 0,
             meta_number_labels: Vec::new(),
+            meta_wire_verts: Vec::new(),
+            meta_wire_count: 0,
             last_scene_mvp: None,
             last_scene_view_rect: (0.0, 0.0, 0.0, 0.0),
             last_viewport_rt_mode: false,
@@ -5802,11 +5812,13 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
             self.viewport_dirty = true;
         }
 
-        // Per-node meta markers, staged by rebuild_scene_geometry.
+        // Per-node meta markers and wires, staged by rebuild_scene_geometry.
         if self.meta_points_dirty {
             self.meta_points_dirty = false;
             renderer.update_mesh(meshes.meta_points, bytemuck::cast_slice(&self.meta_marker_verts));
             self.meta_point_count = self.meta_marker_verts.len() as u32;
+            renderer.update_mesh(meshes.meta_wires, bytemuck::cast_slice(&self.meta_wire_verts));
+            self.meta_wire_count = self.meta_wire_verts.len() as u32;
             self.viewport_dirty = true;
         }
     }
@@ -5833,6 +5845,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
             points: renderer.create_mesh(&[]),
             group_points: renderer.create_mesh(&[]),
             meta_points: renderer.create_mesh(&[]),
+            meta_wires: renderer.create_mesh(&[]),
         });
         // Scene geometry built during `State::new` (before the renderer
         // existed) uploads on the first frame's flush.
@@ -6006,9 +6019,14 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                         draws.push(SceneDraw { mesh: meshes.meta_points, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0, line_width: 1.0, wire_base_width: 0.0 });
                     }
                     if self.vertex_count_spheres > 0 {
-                        // With wires coming, the fill is pushed back by its
-                        // slope-scaled offset so the lattice reads solid.
-                        let base = if self.wireframe { self.wire_width } else { 0.0 };
+                        // With wires coming — the global toggle's or any
+                        // node's meta Wireframe — the fill is pushed back by
+                        // its slope-scaled offset so the lattice reads solid.
+                        let base = if self.wireframe || self.meta_wire_count > 0 {
+                            self.wire_width
+                        } else {
+                            0.0
+                        };
                         draws.push(SceneDraw { mesh: meshes.spheres, mvp, wireframe: false, wire_tint: NO_TINT, opacity: geo_opacity, line_width: 1.0, wire_base_width: base });
                         if self.wireframe {
                             // The wire pass rides ON TOP of the fill (never
@@ -6030,6 +6048,11 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                             let wire_alpha = self.wire_color[3].clamp(0.0, 1.0);
                             draws.push(SceneDraw { mesh: meshes.sphere_edges, mvp, wireframe: true, wire_tint: tint, opacity: wire_alpha, line_width: self.wire_width, wire_base_width: 0.0 });
                         }
+                        // Per-node meta Wireframe: the same wire pass, scoped
+                        // to the flagged nodes' edges, in geometry colors.
+                        if self.meta_wire_count > 0 {
+                            draws.push(SceneDraw { mesh: meshes.meta_wires, mvp, wireframe: true, wire_tint: NO_TINT, opacity: 1.0, line_width: self.wire_width, wire_base_width: 0.0 });
+                        }
                     }
                     renderer.stage_scene((sx, sy, cw, ch), draws);
                     }
diff --git a/src/main.rs b/src/main.rs
index d330ebc..db73fbc 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1171,7 +1171,7 @@ mod tests {
         let meta = s.children.iter().find(|c| c.node_type == "meta").expect("sphere meta");
         assert_eq!(
             meta.params.iter().map(|p| p.name.as_str()).collect::<Vec<_>>(),
-            ["Point Markers", "Point Numbers"]
+            ["Point Markers", "Point Numbers", "Wireframe"]
         );
         assert!(meta.params.iter().all(|p| p.default == "false"));
         let opencl = s.children.iter().find(|c| c.name == "opencl1").unwrap();
@@ -1206,31 +1206,34 @@ mod tests {
 
         // Overlays: nothing while the prefs are off…
         let mut cache = crate::geometry::SimCache::default();
-        let (markers, labels) = crate::render::collect_meta_overlays(
+        let (markers, labels, wires) = crate::render::collect_meta_overlays(
             &root, 0.02, &mut crate::geometry::EvalSim::new(0, 0, &mut cache));
-        assert!(markers.is_empty() && labels.is_empty());
+        assert!(markers.is_empty() && labels.is_empty() && wires.is_empty());
 
-        // …both overlays for the flagged sphere (240 marker verts per
-        // deduped point, labels matching the same dedupe)…
+        // …all three overlays for the flagged sphere: 240 marker verts per
+        // deduped point, labels matching the same dedupe, and one LINE_LIST
+        // pair per triangle edge (2304 verts = 768 triangles = 4608 pairs).
         {
             let meta = root.children[0].children.iter_mut()
                 .find(|c| c.node_type == "meta").unwrap();
             for p in meta.params.iter_mut() { p.default = "true".to_string(); }
         }
         assert!(crate::app::meta_pref(&root.children[0], "Point Markers"));
+        assert!(crate::app::meta_pref(&root.children[0], "Wireframe"));
         let mut cache = crate::geometry::SimCache::default();
-        let (markers, labels) = crate::render::collect_meta_overlays(
+        let (markers, labels, wires) = crate::render::collect_meta_overlays(
             &root, 0.02, &mut crate::geometry::EvalSim::new(0, 0, &mut cache));
         assert!(!labels.is_empty() && labels.len() < 16 * 24 * 6);
         assert_eq!(markers.len(), labels.len() * 240);
         assert!(labels.iter().any(|(_, i)| *i > 0));
+        assert_eq!(wires.len(), (16 * 24 * 6 / 3) * 6);
 
         // …and none once the node's geometry is hidden.
         root.children[0].geometry_visible = false;
         let mut cache = crate::geometry::SimCache::default();
-        let (markers, labels) = crate::render::collect_meta_overlays(
+        let (markers, labels, wires) = crate::render::collect_meta_overlays(
             &root, 0.02, &mut crate::geometry::EvalSim::new(0, 0, &mut cache));
-        assert!(markers.is_empty() && labels.is_empty());
+        assert!(markers.is_empty() && labels.is_empty() && wires.is_empty());
     }
 
     /// The Plane template's construction controls: Rows/Columns set the grid
diff --git a/src/render.rs b/src/render.rs
index 05fbf95..c0d5669 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -856,13 +856,14 @@ impl State {
         // Per-node meta overlays ride the same rebuild: markers and point
         // numbers for nodes whose meta child asks for them.
         let mut sim_cache = std::mem::take(&mut self.sim_cache);
-        let (markers, labels) = {
+        let (markers, labels, wires) = {
             let mut sim = crate::geometry::EvalSim::new(frame, start, &mut sim_cache);
             collect_meta_overlays(&self.fs_root, self.point_size, &mut sim)
         };
         self.sim_cache = sim_cache;
         self.meta_marker_verts = markers;
         self.meta_number_labels = labels;
+        self.meta_wire_verts = wires;
         self.meta_points_dirty = true;
     }
 
@@ -898,9 +899,10 @@ pub(crate) fn collect_meta_overlays(
     root: &FsNode,
     point_size: f32,
     sim: &mut crate::geometry::EvalSim,
-) -> (Vec<crate::geometry::Vertex3D>, Vec<([f32; 3], u32)>) {
+) -> (Vec<crate::geometry::Vertex3D>, Vec<([f32; 3], u32)>, Vec<crate::geometry::Vertex3D>) {
     let mut markers = Vec::new();
     let mut labels = Vec::new();
+    let mut wires = Vec::new();
     fn visit(
         root: &FsNode,
         node: &FsNode,
@@ -908,17 +910,34 @@ pub(crate) fn collect_meta_overlays(
         point_size: f32,
         markers: &mut Vec<crate::geometry::Vertex3D>,
         labels: &mut Vec<([f32; 3], u32)>,
+        wires: &mut Vec<crate::geometry::Vertex3D>,
         sim: &mut crate::geometry::EvalSim,
     ) {
         let is_visible = parent_visible && node.geometry_visible;
         let want_markers = is_visible && crate::app::meta_pref(node, "Point Markers");
         let want_numbers = is_visible && crate::app::meta_pref(node, "Point Numbers");
-        if want_markers || want_numbers {
+        let want_wires = is_visible && crate::app::meta_pref(node, "Wireframe");
+        if want_markers || want_numbers || want_wires {
             let mut visited = Vec::new();
             let mut err = None;
             if let Some(geom) = crate::geometry::generate_single_node_geometry_with_errors(
                 root, node, &mut visited, &mut err, sim,
             ) {
+                if want_wires {
+                    // Each triangle's three edges as LINE_LIST pairs carrying
+                    // the geometry's own colors — the sphere_edges expansion,
+                    // scoped to this node.
+                    for tri in geom.vertices.chunks_exact(3) {
+                        for (a, b) in [(0usize, 1usize), (1, 2), (2, 0)] {
+                            for v in [&tri[a], &tri[b]] {
+                                wires.push(crate::geometry::Vertex3D {
+                                    position: v.pos,
+                                    color: v.col,
+                                });
+                            }
+                        }
+                    }
+                }
                 if want_markers {
                     let src: Vec<crate::geometry::Vertex3D> = geom
                         .vertices
@@ -947,11 +966,11 @@ pub(crate) fn collect_meta_overlays(
             }
         }
         for c in &node.children {
-            visit(root, c, is_visible, point_size, markers, labels, sim);
+            visit(root, c, is_visible, point_size, markers, labels, wires, sim);
         }
     }
     for c in &root.children {
-        visit(root, c, true, point_size, &mut markers, &mut labels, sim);
+        visit(root, c, true, point_size, &mut markers, &mut labels, &mut wires, sim);
     }
     // A dense mesh can label tens of thousands of points; the text pass is
     // per-frame, so cap it rather than melt the frame rate.
@@ -959,5 +978,5 @@ pub(crate) fn collect_meta_overlays(
     if labels.len() > MAX_LABELS {
         labels.truncate(MAX_LABELS);
     }
-    (markers, labels)
+    (markers, labels, wires)
 }