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

commit94cb374ccb768fe88deaba374c7d3c27e7c0a1b6
parentaac7bcce25
authorLucas Galante <[email protected]>
date2026-09-02 12:50
feat: exclusive per-directory geometry visibility (display-flag semantics)

Enabling a node's geometry visibility now clears every sibling's in the
same directory, so at most one node per path displays its geometry —
Houdini display-flag semantics rather than an independent per-node flag.
Disabling stays local to the node.

All four toggle routes funnel through the new
FsNode::set_child_geometry_visible: the network pane's 'e' key, both
graph editors' click toggles (which now sync_nodes so cleared siblings
repaint in both views), and MCP/context-menu ToggleGeometry.

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

 src/app.rs    | 30 +++++++++++++++++++++++++++---
 src/main.rs   | 46 ++++++++++++++++++++++++++++++++++++++++++++++
 src/window.rs |  2 +-
 3 files changed, 74 insertions(+), 4 deletions(-)

diff --git a/src/app.rs b/src/app.rs
index 9263051..89392c5 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -148,6 +148,25 @@ impl FsNode {
         matches!(self.node_type.as_str(), "node" | "utility" | "simnet" | "session")
             || !self.children.is_empty()
     }
+
+    /// Set one child's geometry visibility. Enabling is EXCLUSIVE within the
+    /// directory — at most one node per path shows its geometry, so turning a
+    /// node on turns every sibling off (a display flag, not a per-node render
+    /// flag). Disabling touches only the named child. Every toggle route
+    /// (keyboard `e`, the graph widgets' click toggles, MCP/context-menu
+    /// ToggleGeometry) must go through here or the invariant silently rots.
+    pub fn set_child_geometry_visible(&mut self, slot: usize, visible: bool) {
+        if slot >= self.children.len() {
+            return;
+        }
+        if visible {
+            for (i, child) in self.children.iter_mut().enumerate() {
+                child.geometry_visible = i == slot;
+            }
+        } else {
+            self.children[slot].geometry_visible = false;
+        }
+    }
 }
 
 /// Fresh ids for a node and its whole subtree — required whenever an
@@ -5881,7 +5900,11 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
 
 
                 if let Some((i, visible)) = self.graph_mut().take_node_geom_toggle() {
-                    self.current_dir_mut().children[i].geometry_visible = visible;
+                    self.current_dir_mut().set_child_geometry_visible(i, visible);
+                    // The widget only flipped its own copy of the clicked node;
+                    // the exclusivity rule may have cleared siblings (in both
+                    // editors' views), so push the model back out.
+                    self.sync_nodes();
                     self.rebuild_scene_geometry();
                     changed = true;
                 }
@@ -5946,7 +5969,8 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                         let p2 = self.current_path2.clone();
                         let dir = self.dir_at_mut(&p2);
                         if i < dir.children.len() {
-                            dir.children[i].geometry_visible = visible;
+                            dir.set_child_geometry_visible(i, visible);
+                            self.sync_nodes();
                             self.rebuild_scene_geometry();
                             changed = true;
                         }
@@ -6174,7 +6198,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                                                     let dir = self.current_dir();
                                                     if slot_idx < dir.children.len() && dir.children[slot_idx].node_type != "utility" {
                                                         let visible = !dir.children[slot_idx].geometry_visible;
-                                                        self.current_dir_mut().children[slot_idx].geometry_visible = visible;
+                                                        self.current_dir_mut().set_child_geometry_visible(slot_idx, visible);
                                                         self.sync_nodes();
                                                         self.rebuild_scene_geometry();
                                                         changed = true;
diff --git a/src/main.rs b/src/main.rs
index f1225a3..fe7ba7a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -136,6 +136,52 @@ mod tests {
         assert!(state.has_unsaved_changes(), "the toggle must dirty the title");
     }
 
+    /// Geometry visibility is exclusive per directory: enabling one node's
+    /// display turns every sibling's off (a display flag, not a per-node
+    /// render flag), while disabling touches only that node. All toggle
+    /// routes (keyboard `e`, the graph click toggles, MCP ToggleGeometry)
+    /// funnel through `set_child_geometry_visible`.
+    #[test]
+    fn test_geometry_visibility_is_exclusive_per_directory() {
+        let child = |name: &str| FsNode {
+            id: name.to_string(),
+            name: name.to_string(),
+            node_type: "opencl".to_string(),
+            children: vec![],
+            params: vec![],
+            geometry_visible: true,
+            position: (0.0, 0.0),
+            inputs: 1,
+            outputs: 1,
+        };
+        let mut dir = FsNode {
+            id: "root".to_string(),
+            name: "root".to_string(),
+            node_type: "node".to_string(),
+            children: vec![child("a"), child("b"), child("c")],
+            params: vec![],
+            geometry_visible: true,
+            position: (0.0, 0.0),
+            inputs: 0,
+            outputs: 0,
+        };
+
+        // Enabling slot 1 clears its siblings, even ones already visible.
+        dir.set_child_geometry_visible(1, true);
+        let vis: Vec<bool> = dir.children.iter().map(|c| c.geometry_visible).collect();
+        assert_eq!(vis, [false, true, false]);
+
+        // Disabling is not exclusive — only the named node changes.
+        dir.set_child_geometry_visible(1, false);
+        let vis: Vec<bool> = dir.children.iter().map(|c| c.geometry_visible).collect();
+        assert_eq!(vis, [false, false, false]);
+
+        // Out-of-bounds is a no-op, never a panic or a sibling sweep.
+        dir.children[2].geometry_visible = true;
+        dir.set_child_geometry_visible(9, true);
+        assert!(dir.children[2].geometry_visible);
+    }
+
     /// The button must exist on Main, inside the File section, before Exit.
     #[test]
     fn test_main_node_offers_set_as_default() {
diff --git a/src/window.rs b/src/window.rs
index 990d48d..1ada59a 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -777,7 +777,7 @@ impl State {
                         Err("Cannot toggle geometry visibility on utility nodes".to_string())
                     } else {
                         let visible = !state.current_dir().children[slot].geometry_visible;
-                        state.current_dir_mut().children[slot].geometry_visible = visible;
+                        state.current_dir_mut().set_child_geometry_visible(slot, visible);
                         state.sync_nodes();
                         state.rebuild_scene_geometry();
                         needs_redraw = true;