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

commit4b639f48395455e9eac9f76d4bcf05110d0dc2c8
parent8d5ded9d64
authorLucas Galante <[email protected]>
date2026-08-25 15:50
feat: viewport shows only the network editor's current level

The scene walk (network_sphere_vertices_with_errors) and the meta overlay
walk (collect_meta_overlays) take a start node: the walk iterates the
current directory's children while name resolution stays rooted at
fs_root, so chains inside the displayed level resolve exactly as before.
Navigation already funnels through on_path_changed -> rebuild_scene_geometry
(interactive and MCP enter/up alike), so the viewport re-scopes on every
enter/exit. Thumbnails keep drawing the whole scene from the root; the
status line's OpenCL note now reflects the displayed level.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/geometry.rs  | 37 +++++++++++++++++++++++++++++++++++--
 src/main.rs      |  6 +++---
 src/render.rs    | 19 +++++++++++++++----
 src/thumbnail.rs |  4 +++-
 4 files changed, 56 insertions(+), 10 deletions(-)

diff --git a/src/geometry.rs b/src/geometry.rs
index 8062a2e..f948ce8 100644
--- a/src/geometry.rs
+++ b/src/geometry.rs
@@ -1567,11 +1567,18 @@ pub fn network_sphere_vertices(root: &FsNode) -> Geometry {
     let mut err = None;
     let mut cache = SimCache::default();
     let mut sim = EvalSim::new(0, 0, &mut cache);
-    network_sphere_vertices_with_errors(root, &mut err, &mut sim)
+    network_sphere_vertices_with_errors(root, root, &mut err, &mut sim)
 }
 
+/// Collect the scene's geometry by walking `start`'s children. `start` is the
+/// network level the viewport displays — the network editor's current
+/// directory — while `root` stays the evaluation root: resolvers look inputs
+/// up by name from `root`, so a chain inside the displayed level still
+/// resolves references exactly as it does when drawn from the top. Pass
+/// `root` for both to draw the whole scene (thumbnails do).
 pub fn network_sphere_vertices_with_errors(
     root: &FsNode,
+    start: &FsNode,
     ocl_error: &mut Option<String>,
     sim: &mut EvalSim,
 ) -> Geometry {
@@ -1678,7 +1685,7 @@ pub fn network_sphere_vertices_with_errors(
 
     let mut out = Geometry::new();
     let mut count = 0;
-    for child in &root.children {
+    for child in &start.children {
         visit(root, child, true, &mut count, &mut out, ocl_error, sim);
     }
     out
@@ -2582,6 +2589,32 @@ mod simnet_tests {
         }
     }
 
+    /// The scene walk draws the level it is STARTED at — the network editor's
+    /// current directory — while evaluation stays rooted at the tree root.
+    /// From the root a subnet's internals draw (recursion); started at the
+    /// subnet, only its own children do.
+    #[test]
+    fn test_scene_walk_scoped_to_start_level() {
+        let outer = node("id-outer", "Sphere 1", "sphere", vec![param("Radius", "0.5")], vec![]);
+        let inner = node("id-inner", "Sphere 2", "sphere", vec![param("Radius", "0.5")], vec![]);
+        let sub = node("id-sub", "Sub 1", "node", vec![], vec![inner]);
+        let root = node("id-root", "root", "node", vec![], vec![outer, sub]);
+
+        const SPHERE: usize = 16 * 24 * 6;
+        let mut err = None;
+        let mut cache = SimCache::default();
+        let mut sim = EvalSim::new(0, 0, &mut cache);
+        let all = network_sphere_vertices_with_errors(&root, &root, &mut err, &mut sim);
+        assert_eq!(all.vertices.len(), 2 * SPHERE);
+
+        let mut err = None;
+        let mut cache = SimCache::default();
+        let mut sim = EvalSim::new(0, 0, &mut cache);
+        let scoped =
+            network_sphere_vertices_with_errors(&root, &root.children[1], &mut err, &mut sim);
+        assert_eq!(scoped.vertices.len(), SPHERE);
+    }
+
     /// A simnet whose chain is one Transform: each step shifts the geometry by
     /// the same offset, so the solved position reads back the step COUNT. Uses
     /// transform, not an OpenCL node, so the test is pure CPU.
diff --git a/src/main.rs b/src/main.rs
index 5246276..5ae26cc 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1305,7 +1305,7 @@ mod tests {
         // Overlays: nothing while the prefs are off…
         let mut cache = crate::geometry::SimCache::default();
         let (markers, labels, wires, normals) = crate::render::collect_meta_overlays(
-            &root, 0.02, [1.0, 0.5, 0.0], &mut crate::geometry::EvalSim::new(0, 0, &mut cache));
+            &root, &root, 0.02, [1.0, 0.5, 0.0], &mut crate::geometry::EvalSim::new(0, 0, &mut cache));
         assert!(markers.is_empty() && labels.is_empty() && wires.is_empty() && normals.is_empty());
 
         // …all four overlays for the flagged sphere: 240 marker verts per
@@ -1320,7 +1320,7 @@ mod tests {
         assert!(crate::app::meta_pref(&root.children[0], "Wireframe"));
         let mut cache = crate::geometry::SimCache::default();
         let (markers, labels, wires, normals) = crate::render::collect_meta_overlays(
-            &root, 0.02, [1.0, 0.5, 0.0], &mut crate::geometry::EvalSim::new(0, 0, &mut cache));
+            &root, &root, 0.02, [1.0, 0.5, 0.0], &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));
@@ -1348,7 +1348,7 @@ mod tests {
         root.children[0].geometry_visible = false;
         let mut cache = crate::geometry::SimCache::default();
         let (markers, labels, wires, normals) = crate::render::collect_meta_overlays(
-            &root, 0.02, [1.0, 0.5, 0.0], &mut crate::geometry::EvalSim::new(0, 0, &mut cache));
+            &root, &root, 0.02, [1.0, 0.5, 0.0], &mut crate::geometry::EvalSim::new(0, 0, &mut cache));
         assert!(markers.is_empty() && labels.is_empty() && wires.is_empty() && normals.is_empty());
     }
 
diff --git a/src/render.rs b/src/render.rs
index 202c485..db6b02a 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -809,7 +809,11 @@ impl State {
         let mut sim_cache = std::mem::take(&mut self.sim_cache);
         let geom = {
             let mut sim = crate::geometry::EvalSim::new(frame, start, &mut sim_cache);
-            network_sphere_vertices_with_errors(&self.fs_root, &mut ocl_error, &mut sim)
+            // The viewport shows the network editor's current level, not the
+            // whole tree: the walk starts at the current directory while name
+            // resolution stays rooted at fs_root. Navigation re-scopes this
+            // through on_path_changed, which lands here.
+            network_sphere_vertices_with_errors(&self.fs_root, self.current_dir(), &mut ocl_error, &mut sim)
         };
         self.sim_cache = sim_cache;
 
@@ -825,9 +829,10 @@ impl State {
             false
         }
 
+        let displayed_opencl = has_visible_opencl(self.current_dir());
         if let Some(e) = ocl_error {
             self.update_status_text(&format!("OpenCL Error: {}", e));
-        } else if has_visible_opencl(&self.fs_root) {
+        } else if displayed_opencl {
             self.update_status_text("OpenCL kernel executed successfully.");
         } else {
             self.update_status_text("Geometry updated successfully.");
@@ -849,7 +854,9 @@ impl State {
         let mut sim_cache = std::mem::take(&mut self.sim_cache);
         let (markers, labels, wires, normals) = {
             let mut sim = crate::geometry::EvalSim::new(frame, start, &mut sim_cache);
-            collect_meta_overlays(&self.fs_root, self.meta_marker_size, self.meta_marker_color, &mut sim)
+            // Same scoping as the scene walk above: overlays annotate what is
+            // on screen, so they walk the same current level.
+            collect_meta_overlays(&self.fs_root, self.current_dir(), self.meta_marker_size, self.meta_marker_color, &mut sim)
         };
         self.sim_cache = sim_cache;
         self.meta_marker_verts = markers;
@@ -887,8 +894,12 @@ impl State {
 /// node and collect marker geometry and/or (position, vertex index) labels.
 /// Positions dedupe the triangle soup's repeats; a label keeps the FIRST
 /// index at its position, matching the spreadsheet's vertex numbering.
+/// `start` scopes the walk to the displayed network level (the scene walk's
+/// contract — pass `root` for both to cover the whole tree); evaluation
+/// stays rooted at `root`.
 pub(crate) fn collect_meta_overlays(
     root: &FsNode,
+    start: &FsNode,
     point_size: f32,
     marker_color: [f32; 3],
     sim: &mut crate::geometry::EvalSim,
@@ -1012,7 +1023,7 @@ pub(crate) fn collect_meta_overlays(
             visit(root, c, is_visible, point_size, marker_color, markers, labels, wires, normals, sim);
         }
     }
-    for c in &root.children {
+    for c in &start.children {
         visit(root, c, true, point_size, marker_color, &mut markers, &mut labels, &mut wires, &mut normals, sim);
     }
     // A dense mesh can label tens of thousands of points; the text pass is
diff --git a/src/thumbnail.rs b/src/thumbnail.rs
index a3ec2ac..2a44dae 100644
--- a/src/thumbnail.rs
+++ b/src/thumbnail.rs
@@ -34,7 +34,9 @@ pub fn run(project: &Path, out: &Path, size: u32, samples: Option<u32>) -> Resul
     // A headless thumbnail has no timeline: simnets render at their seed.
     let mut sim_cache = crate::geometry::SimCache::default();
     let mut sim = crate::geometry::EvalSim::new(0, 0, &mut sim_cache);
-    let geom = network_sphere_vertices_with_errors(&proj.root, &mut ocl_error, &mut sim);
+    // Thumbnails always show the whole scene from the top, regardless of the
+    // network level the project was saved at: root as both eval and walk root.
+    let geom = network_sphere_vertices_with_errors(&proj.root, &proj.root, &mut ocl_error, &mut sim);
     if let Some(e) = ocl_error {
         // Non-fatal: OpenCL nodes just contribute nothing, like the viewport.
         eprintln!("thumbnail: OpenCL error (geometry partially skipped): {e}");