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

commit46eb5b923f8f657d442713287a18f934137aa273
parent00e8ff4d51
authorLucas Galante <[email protected]>
date2026-05-22 10:19
Implement Camera node template, custom bounds, and dynamic camera selection in Viewport

 nodes/camera.json |  9 ++++++
 src/main.rs       | 89 ++++++++++++++++++++++++++++++++++++++++++++++++-------
 2 files changed, 87 insertions(+), 11 deletions(-)

diff --git a/nodes/camera.json b/nodes/camera.json
new file mode 100644
index 0000000..6456cc5
--- /dev/null
+++ b/nodes/camera.json
@@ -0,0 +1,9 @@
+{
+  "name": "Camera 1",
+  "type": "camera",
+  "params": [
+    { "name": "X", "type": "slider", "default": "2.5", "min": -10.0, "max": 10.0 },
+    { "name": "Y", "type": "slider", "default": "1.8", "min": -10.0, "max": 10.0 },
+    { "name": "Z", "type": "slider", "default": "2.5", "min": -10.0, "max": 10.0 }
+  ]
+}
diff --git a/src/main.rs b/src/main.rs
index a31e4bd..df5064a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -60,6 +60,10 @@ struct ParamDef {
     default: String,
     #[serde(default)]
     options: Vec<String>,
+    #[serde(default)]
+    min: Option<f32>,
+    #[serde(default)]
+    max: Option<f32>,
 }
 
 fn default_param_type() -> String { "string".to_string() }
@@ -96,7 +100,14 @@ fn param_display(params: &[ParamDef]) -> Vec<(String, String, String)> {
         } else {
             p.default.clone()
         };
-        (key.clone(), value, p.param_type.clone())
+        let ptype = if p.param_type == "slider" {
+            let min = p.min.unwrap_or(0.0);
+            let max = p.max.unwrap_or(2.0);
+            format!("slider:{}:{}", min, max)
+        } else {
+            p.param_type.clone()
+        };
+        (key.clone(), value, ptype)
     }).collect()
 }
 
@@ -956,6 +967,7 @@ struct State {
     pan_start_x: f32,
     pan_start_y: f32,
     space_pressed: bool,
+    active_camera: String,
 }
 
 impl State {
@@ -1142,6 +1154,20 @@ impl State {
                 self.widgets[slot].set_geom_visible(true);
             }
         }
+        let camera_nodes: Vec<String> = self.current_dir().children.iter()
+            .filter(|c| c.node_type == "camera")
+            .map(|c| c.name.clone())
+            .collect();
+        let mut items = vec!["Default Camera".to_string()];
+        items.extend(camera_nodes);
+        if !items.contains(&self.active_camera) {
+            self.active_camera = "Default Camera".to_string();
+        }
+        self.widgets[RIGHT_MENUBAR_IDX].set_menu_items(0, &items);
+        for (i, item) in items.iter().enumerate() {
+            let checked = item == &self.active_camera;
+            self.widgets[RIGHT_MENUBAR_IDX].set_item_checked(0, i, checked);
+        }
         let path_strs = self.current_path_names();
         self.widgets[BREADCRUMB_IDX].set_path(&path_strs);
     }
@@ -1519,6 +1545,7 @@ impl State {
             pan_start_x: 0.0,
             pan_start_y: 0.0,
             space_pressed: false,
+            active_camera: "Default Camera".to_string(),
         };
 
         state.sync_nodes();
@@ -2396,7 +2423,31 @@ impl State {
 
                 self.rotation += 0.015;
                 let proj = Mat4::perspective_rh(0.9, aspect, 0.1, 100.0);
-                let view_mat = Mat4::look_at_rh(Vec3::new(2.5, 1.8, 2.5), Vec3::ZERO, Vec3::Y);
+                let mut camera_pos = Vec3::new(2.5, 1.8, 2.5);
+                if self.active_camera != "Default Camera" {
+                    if let Some(node) = self.current_dir().children.iter().find(|c| c.node_type == "camera" && c.name == self.active_camera) {
+                        let mut cx = 2.5f32;
+                        let mut cy = 1.8f32;
+                        let mut cz = 2.5f32;
+                        for p in &node.params {
+                            if p.name == "X" {
+                                if let Ok(val) = p.default.parse::<f32>() {
+                                    cx = val;
+                                }
+                            } else if p.name == "Y" {
+                                if let Ok(val) = p.default.parse::<f32>() {
+                                    cy = val;
+                                }
+                            } else if p.name == "Z" {
+                                if let Ok(val) = p.default.parse::<f32>() {
+                                    cz = val;
+                                }
+                            }
+                        }
+                        camera_pos = Vec3::new(cx, cy, cz);
+                    }
+                }
+                let view_mat = Mat4::look_at_rh(camera_pos, Vec3::ZERO, Vec3::Y);
                 let model = Mat4::from_rotation_y(self.rotation) * Mat4::from_rotation_x(self.rotation * 0.2);
                 let mvp = proj * view_mat * model;
                 self.queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[mvp.to_cols_array_2d()]));
@@ -2604,16 +2655,32 @@ impl ApplicationHandler for App {
                     }
 
                     if let Some((menu_idx, item_idx)) = state.widgets[RIGHT_MENUBAR_IDX].menu_click() {
-                        let action = if menu_idx == 1 {
-                            Some(Action::ToggleSquareViewport)
-                        } else if menu_idx == 2 {
-                            match item_idx {
-                                0 => Some(Action::ToggleGrid),
-                                1 => Some(Action::ToggleCube),
-                                _ => None,
+                        if menu_idx == 0 {
+                            let camera_nodes: Vec<String> = state.current_dir().children.iter()
+                                .filter(|c| c.node_type == "camera")
+                                .map(|c| c.name.clone())
+                                .collect();
+                            let mut items = vec!["Default Camera".to_string()];
+                            items.extend(camera_nodes);
+                            if item_idx < items.len() {
+                                state.active_camera = items[item_idx].clone();
+                                for (i, item) in items.iter().enumerate() {
+                                    state.widgets[RIGHT_MENUBAR_IDX].set_item_checked(0, i, item == &state.active_camera);
+                                }
+                                changed = true;
                             }
-                        } else { None };
-                        if let Some(a) = action { state.execute_action(a); }
+                        } else {
+                            let action = if menu_idx == 1 {
+                                Some(Action::ToggleSquareViewport)
+                            } else if menu_idx == 2 {
+                                match item_idx {
+                                    0 => Some(Action::ToggleGrid),
+                                    1 => Some(Action::ToggleCube),
+                                    _ => None,
+                                }
+                            } else { None };
+                            if let Some(a) = action { state.execute_action(a); }
+                        }
                     }
 
                     while let Some((id, val)) = state.widgets[CONFIG_DIALOG_IDX].take_config_toggle() {