graphic design tool
git clone https://git.lucas.co/cce-designer.git
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 ¶ms {
+ 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 ¶ms {