graphic design tool
git clone https://git.lucas.co/cce-designer.git
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();