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

commit8bb28a1e922a5e7f4ff557ec991da047a576a641
parentd6022551d8
authorLucas Galante <[email protected]>
date2026-09-21 15:08
fix: Show Cube, Show Grid, Show Origin and Circular Pane survive the next parameter edit

`apply_settings_from_menubar_subnets` copies the meta node's utility subnets
onto the live flags on every parameter change, so a command that flipped
only the flag was undone by the next edit anywhere: Show Cube hid the cube,
and editing any node's parameter brought it back. Show Wireframe already
wrote the Render node for exactly this reason; the guide toggles now write
the Guides node and Circular Pane (all three of its sites) the Main node,
through one `write_meta_toggle` the Render writer is now a case of.

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

 src/app.rs    | 30 +++++++++++++++++++++++++++++-
 src/main.rs   | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/window.rs |  2 ++
 3 files changed, 88 insertions(+), 1 deletion(-)

diff --git a/src/app.rs b/src/app.rs
index ba1dbcf..2a21217 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -2797,12 +2797,33 @@ impl State {
     /// so a command that changed the live state leaves the node agreeing
     /// with it. Nothing when the project has no Render node yet.
     pub(crate) fn write_render_toggle(&mut self, name: &str, val: bool) {
+        self.write_meta_toggle("Render", name, val);
+    }
+
+    /// The Guides node's counterpart: what Show Grid, Show Cube and Show
+    /// Origin write, for the same reason Show Wireframe writes the Render
+    /// node — see [`State::write_meta_toggle`].
+    pub(crate) fn write_guides_toggle(&mut self, name: &str, val: bool) {
+        self.write_meta_toggle("Guides", name, val);
+    }
+
+    /// Write a toggle's value onto the utility subnet that OWNS it.
+    ///
+    /// A command that flips only the live flag has flipped it until the next
+    /// parameter edit anywhere: `apply_settings_from_menubar_subnets` copies
+    /// the meta node's subnets onto the live state on every change, so the
+    /// stored value wins and the toggle silently reverts. Show Cube did
+    /// exactly that on 2026-09-21 — hidden by its command, back the moment a
+    /// node's parameter was edited. The subnet param is the value's one
+    /// owner (the dialog's Settings half writes there too), so a command
+    /// that changes the value writes it there.
+    pub(crate) fn write_meta_toggle(&mut self, subnet: &str, name: &str, val: bool) {
         if let Some(p) = self
             .fs_root
             .children
             .iter_mut()
             .find(|c| c.node_type == "meta")
-            .and_then(|s| s.children.iter_mut().find(|c| c.name == "Render"))
+            .and_then(|s| s.children.iter_mut().find(|c| c.name == subnet))
             .and_then(|n| n.params.iter_mut().find(|p| p.name == name))
         {
             p.default = if val { "true" } else { "false" }.to_string();
@@ -5625,18 +5646,21 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
             Action::ToggleGrid => {
                 let val = !self.viewport().show_grid;
                 self.viewport_mut().show_grid = val;
+                self.write_guides_toggle("Show Grid Guide", val);
                 self.menu_mut(RIGHT_MENUBAR_IDX).set_item_checked(2, 0, val);
                 settings_changed = true;
             }
             Action::ToggleCube => {
                 let val = !self.viewport().show_cube;
                 self.viewport_mut().show_cube = val;
+                self.write_guides_toggle("Show Reference Cube", val);
                 self.menu_mut(RIGHT_MENUBAR_IDX).set_item_checked(2, 1, val);
                 settings_changed = true;
             }
             Action::ToggleOrigin => {
                 let val = !self.viewport().show_origin;
                 self.viewport_mut().show_origin = val;
+                self.write_guides_toggle("Show Origin Axes", val);
                 self.menu_mut(RIGHT_MENUBAR_IDX).set_item_checked(2, 2, val);
                 settings_changed = true;
             }
@@ -5702,6 +5726,10 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
             Action::ToggleCircularPane => {
                 self.circular_network_pane = !self.circular_network_pane;
                 let val = self.circular_network_pane;
+                // The Main node owns this one, as Guides owns the guide
+                // toggles above: without the write, the next parameter edit
+                // put the pane back the way the node said.
+                self.write_meta_toggle("Main", "Circular Pane", val);
                 self.menu_mut(LEFT_MENUBAR_IDX).set_item_checked(2, 2, val);
                 self.rebuild_positions();
                 self.apply_layout();
diff --git a/src/main.rs b/src/main.rs
index fc587c4..79e590b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5046,6 +5046,63 @@ mod tests {
         );
     }
 
+    /// The viewport guide toggles survive the next parameter edit.
+    ///
+    /// `apply_settings_from_menubar_subnets` copies the Guides node onto the
+    /// live flags on EVERY parameter change, so a command that flipped only
+    /// the flag was undone by the next edit anywhere — Show Cube hid the
+    /// cube, and editing any node's parameter brought it back. The command
+    /// has to write the Guides node, the value's owner, as Show Wireframe
+    /// writes the Render node.
+    #[test]
+    fn guide_toggles_survive_the_settings_apply_pass() {
+        let mut state = State::new(false);
+        let guides_value = |state: &State, name: &str| -> String {
+            state
+                .session_node()
+                .and_then(|s| s.children.iter().find(|c| c.name == "Guides"))
+                .and_then(|g| g.params.iter().find(|p| p.name == name))
+                .map(|p| p.default.clone())
+                .expect("the Guides param")
+        };
+        for (command, param) in [
+            ("toggle_cube", "Show Reference Cube"),
+            ("toggle_grid", "Show Grid Guide"),
+            ("toggle_origin", "Show Origin Axes"),
+        ] {
+            let flag = |state: &State| match command {
+                "toggle_cube" => state.viewport().show_cube,
+                "toggle_grid" => state.viewport().show_grid,
+                _ => state.viewport().show_origin,
+            };
+            let before = flag(&state);
+            assert_eq!(guides_value(&state, param), before.to_string(), "{param} starts in step with the flag");
+
+            assert!(state.run_command(command));
+            assert_eq!(flag(&state), !before, "{command} flipped the flag");
+            assert_eq!(guides_value(&state, param), (!before).to_string(), "{command} wrote the Guides node");
+
+            // What every parameter edit runs.
+            state.apply_settings_from_menubar_subnets();
+            assert_eq!(flag(&state), !before, "{command} was undone by the apply pass");
+
+            // And a real edit through the action path, on an unrelated node.
+            let mut redraw = false;
+            let sphere = state.current_dir().children.iter().position(|c| c.name.starts_with("Sphere")).expect("a sphere");
+            state
+                .apply_action(crate::app::McpAction::SetParam { slot: sphere, name: "Radius".into(), value: "0.7".into() }, &mut redraw)
+                .expect("set a sphere param");
+            assert_eq!(flag(&state), !before, "{command} was undone by a parameter edit");
+        }
+
+        // Circular Pane lives on the Main node and had the same hole.
+        let before = state.circular_network_pane;
+        assert!(state.run_command("toggle_circular_pane"));
+        assert_eq!(state.circular_network_pane, !before);
+        state.apply_settings_from_menubar_subnets();
+        assert_eq!(state.circular_network_pane, !before, "Circular Pane was undone by the apply pass");
+    }
+
     /// A replacement renderer invalidates the page pane's image id, and the
     /// state has to notice.
     ///
diff --git a/src/window.rs b/src/window.rs
index 7abef4a..79ded98 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -343,6 +343,7 @@ impl State {
                         2 => {
                             state.circular_network_pane = !state.circular_network_pane;
                             let val = state.circular_network_pane;
+                            state.write_meta_toggle("Main", "Circular Pane", val);
                             state.menu_mut(LEFT_MENUBAR_IDX).set_item_checked(2, 2, val);
                             state.rebuild_positions();
                             state.apply_layout();
@@ -973,6 +974,7 @@ impl State {
             McpAction::ToggleCircularPane => {
                 state.circular_network_pane = !state.circular_network_pane;
                 let val = state.circular_network_pane;
+                state.write_meta_toggle("Main", "Circular Pane", val);
                 state.menu_mut(LEFT_MENUBAR_IDX).set_item_checked(2, 2, val);
                 state.rebuild_positions();
                 state.apply_layout();