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

commitdc81a78ad7b7ea61bcf67e0f7e3a44c9cf585088
parent961c788b74
authorLucas Galante <[email protected]>
date2026-07-21 15:35
refactor: Square Aspect + Show Camera Pivot move from Main to the cameras

Per-camera display params now: the camera template carries the two
toggles, ensure_menubar_subnets ensures them on every camera node
(seeded from a retired older-save Main copy, else live state) and drops
Main's copies, and apply_settings reads them from the ACTIVE camera
(Default Camera has no node — live values stand). The viewport menubar
items and Ctrl+a keep working through their Action handlers, which now
write the flip back to the active camera node so the next settings
apply doesn't revert it.

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

 nodes/camera.json | 34 +++++++++++++++++++++---
 src/app.rs        | 28 +++++++++++++++++---
 src/project.rs    | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++-----
 3 files changed, 128 insertions(+), 13 deletions(-)

diff --git a/nodes/camera.json b/nodes/camera.json
index 50c3f14..dacac67 100644
--- a/nodes/camera.json
+++ b/nodes/camera.json
@@ -4,8 +4,36 @@
   "inputs": 0,
   "outputs": 0,
   "params": [
-    { "name": "Position", "type": "float3", "default": "2.50:1.80:2.50", "min": -10.0, "max": 10.0 },
-    { "name": "Rotation", "type": "float3", "default": "0.00:0.00:0.00", "min": -180.0, "max": 180.0 },
-    { "name": "Pivot", "type": "float3", "default": "0.00:0.00:0.00", "min": -10.0, "max": 10.0 }
+    {
+      "name": "Position",
+      "type": "float3",
+      "default": "2.50:1.80:2.50",
+      "min": -10.0,
+      "max": 10.0
+    },
+    {
+      "name": "Rotation",
+      "type": "float3",
+      "default": "0.00:0.00:0.00",
+      "min": -180.0,
+      "max": 180.0
+    },
+    {
+      "name": "Pivot",
+      "type": "float3",
+      "default": "0.00:0.00:0.00",
+      "min": -10.0,
+      "max": 10.0
+    },
+    {
+      "name": "Square Aspect",
+      "type": "toggle",
+      "default": "false"
+    },
+    {
+      "name": "Show Camera Pivot",
+      "type": "toggle",
+      "default": "false"
+    }
   ]
 }
diff --git a/src/app.rs b/src/app.rs
index 85ce924..f20741a 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1716,19 +1716,38 @@ impl State {
     /// Rewrite the Main node's setting toggles from live app state, so the
     /// switches show the real value even after panes/settings were changed
     /// through the menus or keyboard while another node was selected.
+    /// Write a per-camera display toggle (Square Aspect / Show Camera Pivot)
+    /// back to the ACTIVE camera node — the setting's home — so the next
+    /// settings apply doesn't revert a menu/shortcut flip. No-op under
+    /// Default Camera, which has no node: the live value stands alone.
+    fn write_active_camera_toggle(&mut self, name: &str, val: bool) {
+        if self.active_camera == "Default Camera" {
+            return;
+        }
+        let active = self.active_camera.clone();
+        if let Some(cam) = self
+            .current_dir_mut()
+            .children
+            .iter_mut()
+            .find(|c| c.node_type == "camera" && c.name == active)
+        {
+            if let Some(p) = cam.params.iter_mut().find(|p| p.name == name) {
+                p.default = if val { "true" } else { "false" }.to_string();
+            }
+        }
+    }
+
     fn refresh_main_node_live_toggles(&mut self, slot_idx: usize) {
-        let live: [(&str, bool); 12] = [
+        let live: [(&str, bool); 10] = [
             ("Show Network Pane", self.show_network),
             ("Show Viewport Pane", self.show_viewport),
             ("Show Parameters Pane", self.show_parameters),
             ("Show Spreadsheet Pane", self.show_spreadsheet),
             ("Show Playbar Pane", self.show_playbar),
             ("Circular Pane", self.circular_network_pane),
-            ("Square Aspect", self.square_viewport),
             ("Show Grid Guide", self.viewport().show_grid),
             ("Show Reference Cube", self.viewport().show_cube),
             ("Show Origin Axes", self.viewport().show_origin),
-            ("Show Camera Pivot", self.viewport().show_camera_pivot),
             ("Ray Traced Preview", self.viewport().rt_mode),
         ];
         let dir = self.current_dir_mut();
@@ -3328,11 +3347,14 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
             Action::ToggleCameraPivot => {
                 let val = !self.viewport().show_camera_pivot;
                 self.viewport_mut().show_camera_pivot = val;
+                self.write_active_camera_toggle("Show Camera Pivot", val);
                 self.menu_mut(RIGHT_MENUBAR_IDX).set_item_checked(2, 3, val);
                 settings_changed = true;
             }
             Action::ToggleSquareViewport => {
                 self.square_viewport = !self.square_viewport;
+                let val = self.square_viewport;
+                self.write_active_camera_toggle("Square Aspect", val);
                 settings_changed = true;
             }
             Action::ToggleConfigure => {
diff --git a/src/project.rs b/src/project.rs
index aabb396..aecc187 100644
--- a/src/project.rs
+++ b/src/project.rs
@@ -270,6 +270,7 @@ impl State {
         let show_spreadsheet = self.show_spreadsheet;
         let show_playbar = self.show_playbar;
         let wireframe = self.wireframe;
+        let square_viewport = self.square_viewport;
         let bool_str = |b: bool| if b { "true" } else { "false" };
 
         let camera_nodes: Vec<String> = self.current_dir().children.iter()
@@ -392,11 +393,9 @@ impl State {
             ensure_param(main_node, "Active Camera", "choice", &self.active_camera, &camera_options_refs, None, None, None);
         }
 
-        ensure_param(main_node, "Square Aspect", "toggle", bool_str(self.square_viewport), &[], None, None, None);
         ensure_param(main_node, "Show Grid Guide", "toggle", bool_str(vp_show_grid), &[], None, None, None);
         ensure_param(main_node, "Show Reference Cube", "toggle", bool_str(vp_show_cube), &[], None, None, None);
         ensure_param(main_node, "Show Origin Axes", "toggle", bool_str(vp_show_origin), &[], None, None, None);
-        ensure_param(main_node, "Show Camera Pivot", "toggle", bool_str(vp_show_camera_pivot), &[], None, None, None);
         ensure_param(main_node, "Ray Traced Preview", "toggle", bool_str(vp_rt_mode), &[], None, None, None);
         ensure_param(main_node, "Grid Thickness", "spinbox", &((self.grid_thickness * 1000.0) as i32).to_string(), &[], Some(2.0), Some(200.0), Some(1.0));
         ensure_param(main_node, "Origin Guide Size", "spinbox", &((self.origin_size * 10.0) as i32).to_string(), &[], Some(1.0), Some(50.0), Some(1.0));
@@ -408,14 +407,25 @@ impl State {
         // dispatched. Drop it from older saves too.
         main_node.params.retain(|p| !matches!(p.name.as_str(), "Help" | "About"));
 
+        // Square Aspect / Show Camera Pivot moved to the camera nodes
+        // (per-camera display params): retire Main's copies, keeping an older
+        // save's values as the seed for the cameras below.
+        let migrated_square = main_node.params.iter()
+            .find(|p| p.name == "Square Aspect")
+            .and_then(|p| p.default.parse::<bool>().ok());
+        let migrated_pivot = main_node.params.iter()
+            .find(|p| p.name == "Show Camera Pivot")
+            .and_then(|p| p.default.parse::<bool>().ok());
+        main_node.params.retain(|p| !matches!(p.name.as_str(), "Square Aspect" | "Show Camera Pivot"));
+
         // Boolean settings and pane-visibility items render as toggles. Older
         // saves stored these as choice dropdowns / buttons; retype them and
         // reflect live pane state so a reopened project shows real switches.
         for p in main_node.params.iter_mut() {
             match p.name.as_str() {
-                "Circular Pane" | "Square Aspect" | "Show Grid Guide"
+                "Circular Pane" | "Show Grid Guide"
                 | "Show Reference Cube" | "Show Origin Axes"
-                | "Show Camera Pivot" | "Ray Traced Preview" => {
+                | "Ray Traced Preview" => {
                     p.param_type = "toggle".to_string();
                     p.options.clear();
                     if p.default != "true" { p.default = "false".to_string(); }
@@ -463,6 +473,46 @@ impl State {
                 }
             }
         }
+
+        // 3. Camera display params — Square Aspect / Show Camera Pivot live on
+        // the camera nodes (applied from the ACTIVE camera). Ensured on every
+        // camera in the tree, seeded from the retired Main copies (older
+        // saves) or the live values.
+        fn ensure_camera_display_params(node: &mut FsNode, square: bool, pivot: bool) {
+            if node.node_type == "camera" {
+                let bool_str = |b: bool| if b { "true" } else { "false" };
+                if !node.params.iter().any(|p| p.name == "Square Aspect") {
+                    node.params.push(ParamDef {
+                        name: "Square Aspect".to_string(),
+                        label: "Square Aspect".to_string(),
+                        param_type: "toggle".to_string(),
+                        default: bool_str(square).to_string(),
+                        options: Vec::new(),
+                        min: None,
+                        max: None,
+                        step: None,
+                    });
+                }
+                if !node.params.iter().any(|p| p.name == "Show Camera Pivot") {
+                    node.params.push(ParamDef {
+                        name: "Show Camera Pivot".to_string(),
+                        label: "Camera Pivot".to_string(),
+                        param_type: "toggle".to_string(),
+                        default: bool_str(pivot).to_string(),
+                        options: Vec::new(),
+                        min: None,
+                        max: None,
+                        step: None,
+                    });
+                }
+            }
+            for child in &mut node.children {
+                ensure_camera_display_params(child, square, pivot);
+            }
+        }
+        let square_seed = migrated_square.unwrap_or(square_viewport);
+        let pivot_seed = migrated_pivot.unwrap_or(vp_show_camera_pivot);
+        ensure_camera_display_params(&mut self.fs_root, square_seed, pivot_seed);
     }
 
     pub(crate) fn apply_settings_from_menubar_subnets(&mut self) {
@@ -477,18 +527,15 @@ impl State {
                     "Show Grid Guide" => if let Ok(val) = p.default.parse::<bool>() { self.viewport_mut().show_grid = val; }
                     "Show Reference Cube" => if let Ok(val) = p.default.parse::<bool>() { self.viewport_mut().show_cube = val; }
                     "Show Origin Axes" => if let Ok(val) = p.default.parse::<bool>() { self.viewport_mut().show_origin = val; }
-                    "Show Camera Pivot" => if let Ok(val) = p.default.parse::<bool>() { self.viewport_mut().show_camera_pivot = val; }
                     "Ray Traced Preview" => if let Ok(val) = p.default.parse::<bool>() { self.viewport_mut().rt_mode = val; }
                     "Grid Thickness" => if let Ok(val) = p.default.parse::<f32>() { self.grid_thickness = val / 1000.0; }
                     "Origin Guide Size" => if let Ok(val) = p.default.parse::<f32>() { self.origin_size = val / 10.0; }
                     "Camera Pivot Size" => if let Ok(val) = p.default.parse::<f32>() { self.camera_pivot_size = val / 10.0; }
                     "Background Color" => if let Some(col) = hex_to_color(&p.default) { self.viewport_mut().bg_color = col; }
                     "Grid Color" => if let Some(col) = hex_to_color(&p.default) { self.viewport_mut().grid_color = col; }
-                    "Square Aspect" => if let Ok(val) = p.default.parse::<bool>() { self.square_viewport = val; }
                     "Show Grid" => if let Ok(val) = p.default.parse::<bool>() { self.viewport_mut().show_grid = val; }
                     "Cube" => if let Ok(val) = p.default.parse::<bool>() { self.viewport_mut().show_cube = val; }
                     "Origin" => if let Ok(val) = p.default.parse::<bool>() { self.viewport_mut().show_origin = val; }
-                    "Camera Pivot" => if let Ok(val) = p.default.parse::<bool>() { self.viewport_mut().show_camera_pivot = val; }
                     "Active Camera" => {
                         let cam = p.default.clone();
                         self.active_camera = cam.clone();
@@ -499,6 +546,24 @@ impl State {
             }
         }
 
+        // The ACTIVE camera's display params (per-camera). Default Camera has
+        // no node — the live values stand.
+        if self.active_camera != "Default Camera" {
+            let active = self.active_camera.clone();
+            let cam_params = self.current_dir().children.iter()
+                .find(|c| c.node_type == "camera" && c.name == active)
+                .map(|c| c.params.clone());
+            if let Some(params) = cam_params {
+                for p in &params {
+                    match p.name.as_str() {
+                        "Square Aspect" => if let Ok(val) = p.default.parse::<bool>() { self.square_viewport = val; }
+                        "Show Camera Pivot" => if let Ok(val) = p.default.parse::<bool>() { self.viewport_mut().show_camera_pivot = val; }
+                        _ => {}
+                    }
+                }
+            }
+        }
+
         if let Some(render_idx) = self.fs_root.children.iter().position(|c| c.name == "Render") {
             let params = self.fs_root.children[render_idx].params.clone();
             for p in &params {