graphic design tool
git clone https://git.lucas.co/cce-designer.git
Refactor and split monolithic main.rs into modular window, app, project, render, and graphics files
default_project.json | 2 +-
src/api.rs | 128 +
src/app.rs | 5598 ++++++++++++++++++++++++++++++++++++
src/geometry.rs | 161 +-
src/graphics.rs | 27 +
src/main.rs | 7807 +-------------------------------------------------
src/project.rs | 557 ++++
src/render.rs | 817 ++++++
src/window.rs | 1572 ++++++++++
9 files changed, 8870 insertions(+), 7799 deletions(-)
diff --git a/default_project.json b/default_project.json
index 4e09934..5ba33f6 100644
--- a/default_project.json
+++ b/default_project.json
@@ -108,7 +108,7 @@
"active_camera": "Camera 1",
"pan": [
0.0,
- 122.9747
+ 0.0
],
"current_path": [],
"selected_node": null
diff --git a/src/api.rs b/src/api.rs
new file mode 100644
index 0000000..42e1aa9
--- /dev/null
+++ b/src/api.rs
@@ -0,0 +1,128 @@
+use std::net::TcpListener;
+use std::io::{BufRead, BufReader, Read, Write};
+use crate::{CustomEvent, HttpAction};
+
+pub fn start_http_server(server_sender: calloop::channel::Sender<CustomEvent>) {
+ std::thread::spawn(move || {
+ let listener = match TcpListener::bind("127.0.0.1:3000") {
+ Ok(l) => l,
+ Err(e) => {
+ eprintln!("Failed to bind HTTP server to port 3000: {:?}", e);
+ return;
+ }
+ };
+ println!("Embedded HTTP Server listening on http://127.0.0.1:3000");
+
+ for stream in listener.incoming() {
+ let stream = match stream {
+ Ok(s) => s,
+ Err(_) => continue,
+ };
+
+ let server_sender = server_sender.clone();
+ std::thread::spawn(move || {
+ let mut write_stream = match stream.try_clone() {
+ Ok(s) => s,
+ Err(_) => return,
+ };
+ let mut reader = BufReader::new(stream);
+ let mut request_line = String::new();
+ if reader.read_line(&mut request_line).is_err() {
+ return;
+ }
+
+ if request_line.starts_with("GET /state") {
+ let (tx, rx) = std::sync::mpsc::channel();
+ if server_sender.send(CustomEvent::GetState(tx)).is_ok() {
+ let response_body = rx.recv().unwrap_or_else(|_| "null".to_string());
+ let response = format!(
+ "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
+ response_body.len(),
+ response_body
+ );
+ let _ = write_stream.write_all(response.as_bytes());
+ }
+ } else if request_line.starts_with("POST /action") {
+ let mut content_length = 0;
+ loop {
+ let mut header_line = String::new();
+ if reader.read_line(&mut header_line).is_err() || header_line == "\r\n" || header_line == "\n" || header_line.is_empty() {
+ break;
+ }
+ let lower = header_line.to_lowercase();
+ if lower.starts_with("content-length:") {
+ if let Some(val) = lower.split(':').nth(1) {
+ if let Ok(len) = val.trim().parse::<usize>() {
+ content_length = len;
+ }
+ }
+ }
+ }
+
+ let mut body = vec![0; content_length];
+ if reader.read_exact(&mut body).is_ok() {
+ let body_str = String::from_utf8_lossy(&body);
+ if let Ok(action) = serde_json::from_str::<HttpAction>(&body_str) {
+ let (tx, rx) = std::sync::mpsc::channel();
+ if server_sender.send(CustomEvent::PostAction(action, tx)).is_ok() {
+ let res = rx.recv().unwrap_or_else(|_| Err("internal error".to_string()));
+ let response = match res {
+ Ok(msg) => {
+ let body = format!("{{\"status\":\"success\",\"message\":\"{}\"}}", msg);
+ format!(
+ "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
+ body.len(),
+ body
+ )
+ }
+ Err(err) => {
+ let body = format!("{{\"status\":\"error\",\"error\":\"{}\"}}", err);
+ format!(
+ "HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
+ body.len(),
+ body
+ )
+ }
+ };
+ let _ = write_stream.write_all(response.as_bytes());
+ } else {
+ let body = "{\"status\":\"error\",\"error\":\"failed to send action to event loop\"}";
+ let response = format!(
+ "HTTP/1.1 500 Internal Server Error\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
+ body.len(),
+ body
+ );
+ let _ = write_stream.write_all(response.as_bytes());
+ }
+ } else {
+ let body = "{\"status\":\"error\",\"error\":\"failed to parse action JSON\"}";
+ let response = format!(
+ "HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
+ body.len(),
+ body
+ );
+ let _ = write_stream.write_all(response.as_bytes());
+ }
+ } else {
+ let body = "{\"status\":\"error\",\"error\":\"failed to read complete body\"}";
+ let response = format!(
+ "HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
+ body.len(),
+ body
+ );
+ let _ = write_stream.write_all(response.as_bytes());
+ }
+ } else {
+ let body = "{\"error\":\"not found\"}";
+ let response = format!(
+ "HTTP/1.1 404 Not Found\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
+ body.len(),
+ body
+ );
+ let _ = write_stream.write_all(response.as_bytes());
+ }
+ let _ = write_stream.flush();
+ });
+ }
+ });
+}
diff --git a/src/app.rs b/src/app.rs
new file mode 100644
index 0000000..0a7a4b7
--- /dev/null
+++ b/src/app.rs
@@ -0,0 +1,5598 @@
+use std::time::Instant;
+use std::fs;
+use std::path::Path;
+use std::net::TcpListener;
+use std::io::{BufRead, BufReader, Read, Write};
+
+use serde::{Deserialize, Serialize};
+
+use clear_ui::widget::{ElementState, MouseButton, MouseScrollDelta, KeyEvent, Key, NamedKey};
+
+use smithay_client_toolkit::{
+ compositor::{CompositorHandler, CompositorState},
+ delegate_compositor, delegate_keyboard, delegate_pointer, delegate_registry,
+ delegate_seat, delegate_shm, delegate_xdg_shell, delegate_xdg_window, delegate_output,
+ registry::{ProvidesRegistryState, RegistryState},
+ output::{OutputHandler, OutputState},
+ seat::{
+ keyboard::KeyboardHandler,
+ pointer::{PointerHandler, ThemedPointer, ThemeSpec, CursorIcon},
+ Capability, SeatHandler, SeatState,
+ },
+ shell::{
+ xdg::{
+ window::{Window as XdgWindow, WindowConfigure, WindowHandler, WindowDecorations},
+ XdgShell,
+ },
+ WaylandSurface,
+ },
+ shm::{Shm, ShmHandler},
+};
+use wayland_client::{
+ globals::registry_queue_init,
+ protocol::{wl_keyboard, wl_output, wl_pointer, wl_seat, wl_shm, wl_surface},
+ Connection, QueueHandle, Proxy,
+};
+use calloop_wayland_source::WaylandSource;
+
+use wgpu::util::DeviceExt;
+use clear_ui::widget::{Breadcrumb, Canvas, MenuBar, Plate, ParametersBg, Splitter, Spreadsheet, StatusBar, TextLabel, ViewportBg, Element, GraphNode, Graph, Paginator, Button, Checkbox, Slider, Spinbox, ScrollingList, Label, ColorSelector, Switcher};
+use clear_ui::colors;
+use clear_ui::engine::quad_vertices_with_clip;
+use glyphon::{Attrs, Buffer, Cache, FontSystem, Metrics, Resolution, SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport};
+use glam::{Mat4, Vec3};
+
+use crate::geometry::*;
+use crate::project::*;
+use crate::render::*;
+use crate::shortcut::{Shortcut, ShortcutManager, Action};
+use crate::graphics::TexturedVertex;
+use clear_ui::engine::Vertex;
+use crate::window::{AppState, WindowEvent, LocalPosition};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum TouchPhase {
+ Started,
+ Moved,
+ Ended,
+ Cancelled,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub struct ModifiersState {
+ pub ctrl: bool,
+ pub alt: bool,
+ pub shift: bool,
+ pub logo: bool,
+}
+
+impl ModifiersState {
+ pub fn control_key(&self) -> bool { self.ctrl }
+ pub fn alt_key(&self) -> bool { self.alt }
+ pub fn shift_key(&self) -> bool { self.shift }
+ pub fn super_key(&self) -> bool { self.logo }
+ pub fn state(&self) -> Self { *self }
+}
+
+pub const HEADER_IDX: usize = 0;
+pub const CONTENT_IDX: usize = 1;
+pub const SPLITTER1_IDX: usize = 2;
+pub const VIEWPORT_IDX: usize = 3;
+pub const SPLITTER2_IDX: usize = 4;
+pub const PARAM_PLATE_IDX: usize = 5;
+pub const PARAM_IDX: usize = 6;
+pub const CANVAS_IDX: usize = 7;
+pub const LEFT_MENUBAR_IDX: usize = 8;
+pub const RIGHT_MENUBAR_IDX: usize = 9;
+pub const PARAM_MENUBAR_IDX: usize = 10;
+pub const STATUS_IDX: usize = 11;
+pub const BREADCRUMB_IDX: usize = 12;
+pub const NODE_PALETTE_IDX: usize = 13;
+pub const SPREADSHEET_IDX: usize = 14;
+pub const SPREADSHEET_MENUBAR_IDX: usize = 15;
+pub const NETWORK_PANEL_IDX: usize = 16;
+pub const PAGINATOR_IDX: usize = 17;
+
+
+pub const HEADER_H: f32 = 0.0;
+pub const STATUS_H: f32 = 0.0;
+pub const MENUBAR_H: f32 = 0.0;
+pub const SPLITTER_W: f32 = 6.0;
+
+pub const MIN_COLUMN: f32 = 120.0;
+pub const BREADCRUMB_H: f32 = 24.0;
+
+#[derive(Clone, Deserialize, Serialize)]
+pub struct ParamDef {
+ pub name: String,
+ #[serde(default)]
+ pub label: String,
+ #[serde(rename = "type")]
+ #[serde(default = "default_param_type")]
+ pub param_type: String,
+ #[serde(default)]
+ pub default: String,
+ #[serde(default)]
+ pub options: Vec<String>,
+ #[serde(default)]
+ pub min: Option<f32>,
+ #[serde(default)]
+ pub max: Option<f32>,
+ #[serde(default)]
+ pub step: Option<f32>,
+}
+
+fn default_param_type() -> String { "string".to_string() }
+
+static NODE_ID_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
+
+pub fn generate_node_id() -> String {
+ let now = std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .map(|d| d.as_nanos())
+ .unwrap_or(0);
+ let count = NODE_ID_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
+ format!("node_{:x}_{:x}", now, count)
+}
+
+#[derive(Clone, Deserialize, Serialize)]
+pub struct FsNode {
+ #[serde(default = "generate_node_id")]
+ pub id: String,
+ pub name: String,
+ #[serde(rename = "type")]
+ #[serde(default = "default_node_type")]
+ pub node_type: String,
+ #[serde(default)]
+ pub children: Vec<FsNode>,
+ #[serde(default)]
+ pub params: Vec<ParamDef>,
+ #[serde(skip)]
+ #[serde(default = "default_node_geometry_visible")]
+ pub geometry_visible: bool,
+ #[serde(default = "default_node_position")]
+ pub position: (f32, f32),
+}
+
+fn default_node_type() -> String { "node".to_string() }
+fn default_node_geometry_visible() -> bool { true }
+fn default_node_position() -> (f32, f32) { (0.0, 0.0) }
+
+#[derive(Clone, Deserialize, Serialize, Default)]
+pub struct ProjectViewState {
+ #[serde(default = "default_camera")]
+ pub active_camera: String,
+ #[serde(default)]
+ pub pan: (f32, f32),
+ #[serde(default)]
+ pub current_path: Vec<usize>,
+ #[serde(default)]
+ pub selected_node: Option<usize>,
+}
+
+fn default_camera() -> String {
+ "Default Camera".to_string()
+}
+
+#[derive(Clone, Deserialize, Serialize)]
+pub struct Project {
+ pub name: String,
+ pub root: FsNode,
+ #[serde(default)]
+ pub view_state: ProjectViewState,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(tag = "action", rename_all = "snake_case")]
+pub enum HttpAction {
+ Up,
+ Enter { slot: usize },
+ SetParam { slot: usize, name: String, value: String },
+ ResetCamera,
+ Load { path: String },
+ Save { path: String },
+ ToggleGeometry { slot: usize },
+ AddNode { template_name: String, name: Option<String>, x: f32, y: f32 },
+ DeleteNode { slot: usize },
+ RenameNode { slot: usize, new_name: String },
+ MoveNode { slot: usize, x: f32, y: f32 },
+ AddParam { slot: usize, name: String, param_type: String, default: String },
+ DeleteParam { slot: usize, name: String },
+ ToggleCircularPane,
+ MenuClick { widget_idx: usize, menu_idx: usize, item_idx: usize },
+ MenuClosed { widget_idx: usize, menu_idx: usize },
+ SelectPage { page: usize },
+}
+
+#[derive(Debug)]
+pub enum CustomEvent {
+ GetState(std::sync::mpsc::Sender<String>),
+ PostAction(HttpAction, std::sync::mpsc::Sender<Result<String, String>>),
+}
+
+#[derive(Clone)]
+pub struct NodeTemplate {
+ pub label: String,
+ pub node: FsNode,
+}
+
+pub fn param_display(params: &[ParamDef]) -> Vec<(String, String, String)> {
+ params.iter().map(|p| {
+ let key = if p.label.is_empty() { &p.name } else { &p.label };
+ let value = if p.param_type == "choice" && !p.options.is_empty() && p.default.is_empty() {
+ p.options[0].clone()
+ } else {
+ p.default.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 if p.param_type == "float3" {
+ let min = p.min.unwrap_or(-10.0);
+ let max = p.max.unwrap_or(10.0);
+ format!("float3:{}:{}", min, max)
+ } else if p.param_type == "spinbox" {
+ let min = p.min.unwrap_or(1.0) as i32;
+ let max = p.max.unwrap_or(10000.0) as i32;
+ let step = p.step.unwrap_or(1.0) as i32;
+ format!("spinbox:{}:{}:{}", min, max, step)
+ } else {
+ p.param_type.clone()
+ };
+ (key.clone(), value, ptype)
+ }).collect()
+}
+
+pub fn flatten_node_templates(root: &FsNode) -> Vec<NodeTemplate> {
+ fn visit(node: &FsNode, path: &mut Vec<String>, out: &mut Vec<NodeTemplate>) {
+ if !node.name.is_empty() {
+ path.push(node.name.clone());
+ out.push(NodeTemplate { label: path.join(" / "), node: node.clone() });
+ }
+ for child in &node.children {
+ visit(child, path, out);
+ }
+ if !node.name.is_empty() {
+ path.pop();
+ }
+ }
+
+ let mut out = Vec::new();
+ let mut path = Vec::new();
+ for child in &root.children {
+ visit(child, &mut path, &mut out);
+ }
+ out
+}
+
+pub fn load_fs_tree() -> FsNode {
+ let nodes_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("nodes");
+ let mut children = Vec::new();
+ if let Ok(entries) = fs::read_dir(&nodes_dir) {
+ let mut paths: Vec<_> = entries
+ .flatten()
+ .map(|e| e.path())
+ .filter(|p| p.extension().and_then(|e| e.to_str()) == Some("json"))
+ .collect();
+ paths.sort();
+ for path in paths {
+ if let Ok(content) = fs::read_to_string(&path) {
+ if let Ok(node) = serde_json::from_str::<FsNode>(&content) {
+ children.push(node);
+ }
+ }
+ }
+ }
+ FsNode {
+ id: generate_node_id(),
+ name: String::new(),
+ node_type: default_node_type(),
+ children,
+ params: vec![],
+ geometry_visible: true,
+ position: (0.0, 0.0),
+ }
+}
+
+fn default_grid_thickness() -> f32 { 0.03 }
+fn default_show_camera_pivot() -> bool { false }
+fn default_camera_pivot_size() -> f32 { 1.0 }
+fn default_node_color() -> [f32; 3] { [0.10, 0.45, 0.70] }
+fn default_grid_color() -> [f32; 3] { [0.35, 0.35, 0.40] }
+fn default_uniform_background() -> bool { false }
+fn default_network_opacity() -> f32 { 0.95 }
+fn default_cell_color() -> [f32; 3] { [0.13, 0.13, 0.16] }
+fn default_gap_color() -> [f32; 3] { [0.07, 0.07, 0.09] }
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+pub struct DesignSettings {
+ pub grid_snap_enabled: bool,
+ pub network_grid_enabled: bool,
+ pub grid_size_x: f32,
+ pub grid_size_y: f32,
+ pub skipped_row_h: f32,
+ pub skipped_col_w: f32,
+ pub show_grid_enabled: bool,
+ pub show_cube_enabled: bool,
+ pub show_origin_enabled: bool,
+ pub origin_size: f32,
+ pub viewport_bg_color: [f32; 3],
+ pub square_viewport: bool,
+ #[serde(default = "default_grid_thickness")]
+ pub grid_thickness: f32,
+ #[serde(default = "default_show_camera_pivot")]
+ pub show_camera_pivot_enabled: bool,
+ #[serde(default = "default_camera_pivot_size")]
+ pub camera_pivot_size: f32,
+ #[serde(default = "default_node_color")]
+ pub node_color: [f32; 3],
+ #[serde(default = "default_grid_color")]
+ pub grid_color: [f32; 3],
+ #[serde(default = "default_uniform_background")]
+ pub uniform_background: bool,
+ #[serde(default = "default_network_opacity")]
+ pub network_opacity: f32,
+ #[serde(default = "default_cell_color")]
+ pub cell_color: [f32; 3],
+ #[serde(default = "default_gap_color")]
+ pub gap_color: [f32; 3],
+}
+
+impl Default for DesignSettings {
+ fn default() -> Self {
+ Self {
+ grid_snap_enabled: true,
+ network_grid_enabled: true,
+ grid_size_x: 80.0,
+ grid_size_y: 40.0,
+ skipped_row_h: 20.0,
+ skipped_col_w: 20.0,
+ show_grid_enabled: true,
+ show_cube_enabled: false,
+ show_origin_enabled: true,
+ origin_size: 1.0,
+ viewport_bg_color: [0.05, 0.05, 0.10],
+ square_viewport: false,
+ grid_thickness: 0.03,
+ show_camera_pivot_enabled: false,
+ camera_pivot_size: 1.0,
+ node_color: default_node_color(),
+ grid_color: default_grid_color(),
+ uniform_background: false,
+ network_opacity: 0.95,
+ cell_color: default_cell_color(),
+ gap_color: default_gap_color(),
+ }
+ }
+}
+
+impl DesignSettings {
+ fn file_path() -> std::path::PathBuf {
+ let home = std::env::var("HOME").unwrap_or_else(|_| "/home/lsgalante".to_string());
+ let mut path = std::path::PathBuf::from(home);
+ path.push(".config");
+ path.push("cce");
+ path.push("design.json");
+ path
+ }
+
+ fn load() -> Self {
+ let path = Self::file_path();
+ if let Ok(content) = fs::read_to_string(&path) {
+ if let Ok(settings) = serde_json::from_str::<Self>(&content) {
+ return settings;
+ }
+ }
+ Self::default()
+ }
+
+ fn save(&self) {
+ let path = Self::file_path();
+ if let Some(parent) = path.parent() {
+ let _ = fs::create_dir_all(parent);
+ }
+ if let Ok(content) = serde_json::to_string_pretty(self) {
+ let _ = fs::write(path, content);
+ }
+ }
+}
+
+pub struct NodePalette {
+ pub x: f32, y: f32, w: f32, h: f32,
+ pub visible: bool,
+ pub query: String,
+ pub items: Vec<String>,
+ pub selected: usize,
+}
+
+impl NodePalette {
+ fn new() -> Self {
+ Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, visible: false, query: String::new(), items: Vec::new(), selected: 0 }
+ }
+
+ fn panel_rect(&self) -> (f32, f32, f32, f32) {
+ let pw = 460.0_f32.min(self.w - 40.0).max(280.0);
+ let ph = 360.0_f32.min(self.h - 80.0).max(240.0);
+ (self.x + (self.w - pw) / 2.0, self.y + 76.0, pw, ph)
+ }
+
+ pub fn set_palette_state(&mut self, visible: bool, query: &str, items: &[String], selected: usize) {
+ self.visible = visible;
+ self.query = query.to_string();
+ self.items = items.to_vec();
+ self.selected = selected;
+ }
+}
+
+impl Element for NodePalette {
+ fn as_any(&self) -> &dyn std::any::Any { self }
+ fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
+ fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+ fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
+ fn hit_test(&self, px: f32, py: f32, ctx: &clear_ui::context::UiContext) -> bool {
+ if ctx.is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
+ return false;
+ }
+ self.visible && px >= self.x && px <= self.x + self.w && py >= self.y && py <= self.y + self.h
+ }
+ fn set_visible(&mut self, visible: bool) { self.visible = visible; }
+ fn visible(&self) -> bool { self.visible }
+ fn take_click(&mut self) -> bool { self.visible }
+
+
+ fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+ if !self.visible { return Vec::new(); }
+ let mut quads = Vec::new();
+ quads.push((self.x, self.y, self.w, self.h, [0.0, 0.0, 0.0, 0.45]));
+ let (px, py, pw, ph) = self.panel_rect();
+ quads.push((px, py, pw, ph, colors::PANEL_MENU_BG));
+ quads.push((px + 16.0, py + 48.0, pw - 32.0, 32.0, [0.10, 0.10, 0.14, 1.0]));
+ let list_y = py + 92.0;
+ let row_h = 24.0;
+ let visible_rows = ((ph - 120.0) / row_h).floor().max(0.0) as usize;
+ let start = self.selected.saturating_sub(visible_rows.saturating_sub(1));
+ for (row, item_idx) in (start..self.items.len().min(start + visible_rows)).enumerate() {
+ let y = list_y + row as f32 * row_h;
+ let bg = if item_idx == self.selected { [0.24, 0.33, 0.55, 0.85] } else { [0.14, 0.14, 0.19, 0.55] };
+ quads.push((px + 16.0, y, pw - 32.0, row_h - 2.0, bg));
+ }
+ quads
+ }
+
+ fn text_labels(&self) -> Vec<TextLabel> {
+ if !self.visible { return Vec::new(); }
+ let (px, py, _pw, ph) = self.panel_rect();
+ let mut labels = Vec::new();
+ labels.push(TextLabel { text: "Add Node".into(), x: px + 16.0, y: py + 14.0, font_size: 15.0, color: [0xdd, 0xdd, 0xe6] });
+ labels.push(TextLabel { text: "Type to search, Enter to place, Esc to close".into(), x: px + 100.0, y: py + 17.0, font_size: 11.0, color: [0x88, 0x88, 0x99] });
+ let query = if self.query.is_empty() { "Search nodes...".to_string() } else { self.query.clone() };
+ let query_color = if self.query.is_empty() { [0x66, 0x66, 0x77] } else { [0xdd, 0xdd, 0xe6] };
+ labels.push(TextLabel { text: query, x: px + 28.0, y: py + 57.0, font_size: 13.0, color: query_color });
+ let row_h = 24.0;
+ let visible_rows = ((ph - 120.0) / row_h).floor().max(0.0) as usize;
+ if self.items.is_empty() {
+ labels.push(TextLabel { text: "No matching nodes".into(), x: px + 28.0, y: py + 100.0, font_size: 12.0, color: [0xaa, 0xaa, 0xbb] });
+ } else {
+ let start = self.selected.saturating_sub(visible_rows.saturating_sub(1));
+ for (row, item_idx) in (start..self.items.len().min(start + visible_rows)).enumerate() {
+ labels.push(TextLabel {
+ text: self.items[item_idx].clone(),
+ x: px + 28.0,
+ y: py + 98.0 + row as f32 * row_h,
+ font_size: 12.0,
+ color: if item_idx == self.selected { [0xff, 0xff, 0xdd] } else { [0xcc, 0xcc, 0xd8] },
+ });
+ }
+ }
+ labels
+ }
+
+ fn z_index(&self) -> i32 {
+ 200
+ }
+}
+
+
+pub fn make_text_buffer(font_system: &mut FontSystem, text: &str, size: f32) -> Buffer {
+ let metrics = Metrics::new(size, size * 1.4);
+ let mut buffer = Buffer::new(font_system, metrics);
+ buffer.set_text(font_system, text, Attrs::new(), glyphon::Shaping::Advanced);
+ buffer.shape_until_scroll(font_system, true);
+ buffer
+}
+
+pub fn make_text_buffer_with_font(font_system: &mut FontSystem, text: &str, size: f32, font: Option<&str>) -> Buffer {
+ let metrics = Metrics::new(size, size * 1.4);
+ let mut buffer = Buffer::new(font_system, metrics);
+ let mut attrs = Attrs::new();
+ let family_name = font.map(|f| clear_ui::layout::parse_font_string(f).0);
+ if let Some(ref name) = family_name {
+ let family = match name.as_str() {
+ "monospace" => glyphon::Family::Monospace,
+ "sans-serif" => glyphon::Family::SansSerif,
+ "serif" => glyphon::Family::Serif,
+ _ => glyphon::Family::Name(name),
+ };
+ attrs = attrs.family(family);
+ }
+ buffer.set_text(font_system, text, attrs, glyphon::Shaping::Advanced);
+ buffer.shape_until_scroll(font_system, true);
+ buffer
+}
+
+pub fn get_next_visible_pane(
+ current_pane: usize,
+ show_network: bool,
+ show_viewport: bool,
+ show_parameters: bool,
+ show_spreadsheet: bool,
+ shift_pressed: bool,
+) -> usize {
+ let mut visible_panes = Vec::new();
+ if show_network {
+ visible_panes.push(LEFT_MENUBAR_IDX);
+ }
+ if show_viewport {
+ visible_panes.push(RIGHT_MENUBAR_IDX);
+ }
+ if show_parameters {
+ visible_panes.push(PARAM_MENUBAR_IDX);
+ }
+ if show_spreadsheet {
+ visible_panes.push(SPREADSHEET_MENUBAR_IDX);
+ }
+ if visible_panes.is_empty() {
+ return LEFT_MENUBAR_IDX;
+ }
+
+ let current_pos = visible_panes.iter().position(|&x| x == current_pane).unwrap_or(0);
+ let next_pos = if shift_pressed {
+ (current_pos + visible_panes.len() - 1) % visible_panes.len()
+ } else {
+ (current_pos + 1) % visible_panes.len()
+ };
+ visible_panes[next_pos]
+}
+
+pub fn push_circle_vertices(
+ cx: f32, cy: f32, r: f32,
+ sw: f32, sh: f32,
+ color: [f32; 4],
+ segments: usize,
+ clip_circle: [f32; 3],
+ out: &mut Vec<Vertex>,
+) {
+ for i in 0..segments {
+ let theta1 = (i as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
+ let theta2 = ((i + 1) as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
+ let x0 = cx;
+ let y0 = cy;
+ let x1 = cx + r * theta1.cos();
+ let y1 = cy + r * theta1.sin();
+ let x2 = cx + r * theta2.cos();
+ let y2 = cy + r * theta2.sin();
+
+ let ndc_x0 = (x0 / sw) * 2.0 - 1.0;
+ let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
+ let ndc_x1 = (x1 / sw) * 2.0 - 1.0;
+ let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
+ let ndc_x2 = (x2 / sw) * 2.0 - 1.0;
+ let ndc_y2 = 1.0 - (y2 / sh) * 2.0;
+
+ out.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
+ out.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
+ out.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
+ }
+}
+
+pub fn push_circle_border_vertices(
+ cx: f32, cy: f32, r: f32,
+ thickness: f32,
+ sw: f32, sh: f32,
+ color: [f32; 4],
+ segments: usize,
+ clip_circle: [f32; 3],
+ out: &mut Vec<Vertex>,
+) {
+ for i in 0..segments {
+ let theta1 = (i as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
+ let theta2 = ((i + 1) as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
+
+ let x0 = cx + (r - thickness) * theta1.cos();
+ let y0 = cy + (r - thickness) * theta1.sin();
+ let x1 = cx + r * theta1.cos();
+ let y1 = cy + r * theta1.sin();
+
+ let x2 = cx + r * theta2.cos();
+ let y2 = cy + r * theta2.sin();
+ let x3 = cx + (r - thickness) * theta2.cos();
+ let y3 = cy + (r - thickness) * theta2.sin();
+
+ let ndc_x0 = (x0 / sw) * 2.0 - 1.0; let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
+ let ndc_x1 = (x1 / sw) * 2.0 - 1.0; let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
+ let ndc_x2 = (x2 / sw) * 2.0 - 1.0; let ndc_y2 = 1.0 - (y2 / sh) * 2.0;
+ let ndc_x3 = (x3 / sw) * 2.0 - 1.0; let ndc_y3 = 1.0 - (y3 / sh) * 2.0;
+
+ out.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
+ out.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
+ out.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
+
+ out.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
+ out.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
+ out.push(Vertex { position: [ndc_x3, ndc_y3], color, clip_circle });
+ }
+}
+
+fn push_quad_vertices_clipped(
+ x: f32, y: f32, w: f32, h: f32,
+ surface_w: f32, surface_h: f32,
+ color: [f32; 4],
+ clip: (f32, f32, f32, f32),
+ clip_circle: [f32; 3],
+ out: &mut Vec<Vertex>,
+) {
+ let (cx0, cy0, cx1, cy1) = clip;
+ let ix0 = x.max(cx0);
+ let iy0 = y.max(cy0);
+ let ix1 = (x + w).min(cx1);
+ let iy1 = (y + h).min(cy1);
+ if ix1 <= ix0 || iy1 <= iy0 {
+ return;
+ }
+ out.extend_from_slice(&quad_vertices_with_clip(ix0, iy0, ix1 - ix0, iy1 - iy0, surface_w, surface_h, color, clip_circle));
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+struct ResizeDirection {
+ pub left: bool,
+ pub right: bool,
+ pub top: bool,
+ pub bottom: bool,
+}
+
+pub struct State {
+ pub wgpu_adapter: clear_ui::backend::WgpuAdapter,
+ pub render_pipeline: wgpu::RenderPipeline,
+ pub vertex_buffer: wgpu::Buffer,
+ pub window: XdgWindow,
+ pub wl_surface: wl_surface::WlSurface,
+ pub vertex_count: u32,
+ pub vertex_data: Vec<Vertex>,
+
+ pub pipeline_3d: wgpu::RenderPipeline,
+ pub bind_group_3d: wgpu::BindGroup,
+ pub bind_group_layout_3d: wgpu::BindGroupLayout,
+ pub uniform_buffer: wgpu::Buffer,
+ pub bind_group_grid: wgpu::BindGroup,
+ pub uniform_buffer_grid: wgpu::Buffer,
+ pub bind_group_pivot: wgpu::BindGroup,
+ pub uniform_buffer_pivot: wgpu::Buffer,
+ pub vertex_buffer_3d: wgpu::Buffer,
+ pub vertex_count_3d: u32,
+ pub vertex_buffer_spheres: wgpu::Buffer,
+ pub vertex_count_spheres: u32,
+ pub vertex_buffer_grid: wgpu::Buffer,
+ pub vertex_count_grid: u32,
+ pub depth_texture: wgpu::Texture,
+ pub depth_texture_view: wgpu::TextureView,
+ pub backdrop_texture: wgpu::Texture,
+ pub backdrop_texture_view: wgpu::TextureView,
+ pub backdrop_sampler: wgpu::Sampler,
+ pub backdrop_bind_group_layout: wgpu::BindGroupLayout,
+ pub backdrop_bind_group: wgpu::BindGroup,
+ pub rotation_y: f32,
+ pub rotation_x: f32,
+ pub is_rotating_viewport: bool,
+ pub scroll_lock: u8, // 0 = None, 1 = Horizontal, 2 = Vertical
+ pub last_rotate_time: Instant,
+ pub rotate_accum_yaw: f32,
+ pub rotate_accum_pitch: f32,
+ pub rotate_velocity_yaw: f32,
+ pub rotate_velocity_pitch: f32,
+ pub show_grid: bool,
+ pub show_cube: bool,
+ pub show_origin: bool,
+ pub show_camera_pivot: bool,
+ pub viewport_bg_color: [f32; 3],
+ pub node_color: [f32; 3],
+ pub grid_color: [f32; 3],
+ pub cell_color: [f32; 3],
+ pub gap_color: [f32; 3],
+ pub vertex_buffer_origin: wgpu::Buffer,
+ pub vertex_count_origin: u32,
+ pub vertex_buffer_pivot: wgpu::Buffer,
+ pub vertex_count_pivot: u32,
+ pub origin_size: f32,
+ pub camera_pivot_size: f32,
+
+ pub fs_root: FsNode,
+ pub node_templates: Vec<NodeTemplate>,
+ pub current_path: Vec<usize>,
+ pub last_click: Option<(Instant, usize)>,
+ pub last_frame: Instant,
+
+ pub shortcut_manager: ShortcutManager,
+ pub pending_action: Option<Action>,
+ pub exit_requested: bool,
+
+ pub widgets: Vec<Box<dyn Element>>,
+ pub positions: Vec<(f32, f32, f32, f32)>,
+ pub splitter_layout: clear_ui::layout::SplitterLayout,
+ pub node_palette_visible: bool,
+ pub node_palette_query: String,
+ pub node_palette_filtered: Vec<usize>,
+ pub node_palette_selected: usize,
+
+ pub curved_text_texture: wgpu::Texture,
+ pub curved_text_texture_view: wgpu::TextureView,
+ pub curved_text_sampler: wgpu::Sampler,
+ pub curved_text_bind_group: wgpu::BindGroup,
+ pub curved_text_pipeline: wgpu::RenderPipeline,
+ pub curved_text_atlas: TextAtlas,
+ pub curved_text_renderer: TextRenderer,
+ pub curved_text_viewport: Viewport,
+ pub textured_vertex_buffer: wgpu::Buffer,
+ pub textured_vertex_count: u32,
+
+
+ pub drag_widget: Option<usize>,
+ pub focused_widget: Option<usize>,
+
+ pub cursor_x: f32,
+ pub cursor_y: f32,
+ pub grid_cursor_col: i32,
+ pub grid_cursor_row: i32,
+ pub modifiers: ModifiersState,
+
+ pub width: f32,
+ pub height: f32,
+ pub physical_width: u32,
+ pub physical_height: u32,
+ pub scale: f64,
+ pub square_viewport: bool,
+ pub grid_snap_enabled: bool,
+ pub network_grid_visible: bool,
+ pub grid_size_x: f32,
+ pub grid_size_y: f32,
+ pub skipped_row_h: f32,
+ pub skipped_col_w: f32,
+
+ pub pan_x: f32,
+ pub pan_y: f32,
+ pub pan_velocity_x: f32,
+ pub pan_velocity_y: f32,
+ pub last_frame_pan_x: f32,
+ pub last_frame_pan_y: f32,
+ pub is_panning: bool,
+ pub pan_start_cx: f32,
+ pub pan_start_cy: f32,
+ pub pan_start_x: f32,
+ pub pan_start_y: f32,
+ pub space_pressed: bool,
+ pub active_camera: String,
+ pub show_network: bool,
+ pub show_viewport: bool,
+ pub show_parameters: bool,
+ pub show_spreadsheet: bool,
+ pub is_scrolling_trackpad: bool,
+ pub last_scroll_time: Instant,
+ pub scroll_accum_x: f32,
+ pub scroll_accum_y: f32,
+ pub last_spreadsheet_node_name: Option<String>,
+ pub last_spreadsheet_node_params: Option<Vec<(String, String)>>,
+ pub viewport_zoom: f32,
+ pub is_zooming_viewport: bool,
+ pub last_zoom_time: Instant,
+ pub zoom_accum: f32,
+ pub zoom_velocity: f32,
+ pub grid_thickness: f32,
+ pub focused_pane: usize,
+ pub inertial_scroll_enabled: bool,
+ pub inertial_scroll_friction: f32,
+ pub scroll_speed: f32,
+ pub last_config_read: Instant,
+ pub circular_network_pane: bool,
+ pub circular_network_layout: clear_ui::layout::CircularPaneLayout,
+ pub is_detached_network: bool,
+ pub detached_circular_network: bool,
+ pub last_project_mod_time: Option<std::time::SystemTime>,
+ pub last_project_check: std::time::Instant,
+ pub last_inspector_check: std::time::Instant,
+ pub last_inspector_update: std::time::Instant,
+ pub needs_autosave: bool,
+ pub last_autosave_time: std::time::Instant,
+ pub window_x: i32,
+ pub window_y: i32,
+ pub active_menu_cloud_pid: Option<u32>,
+ pub active_menu_cloud_idx: Option<(usize, usize)>,
+ pub uniform_background: bool,
+ pub network_opacity: f32,
+ pub last_design_mod_time: Option<std::time::SystemTime>,
+ pub last_config_mod_time: Option<std::time::SystemTime>,
+ pub floating_network_layout: (f32, f32, f32, f32),
+ pub is_resizing_network: Option<ResizeDirection>,
+ pub drag_start_rect: (f32, f32, f32, f32),
+ pub drag_start_mouse: (f32, f32),
+ pub floating_param_width: f32,
+ pub is_resizing_param: bool,
+ pub drag_start_param_w: f32,
+ pub floating_spreadsheet_height: f32,
+ pub is_resizing_spreadsheet: bool,
+ pub drag_start_spreadsheet_h: f32,
+ pub paginator_page_widgets: Vec<Vec<Box<dyn Element>>>,
+ pub last_paginator_menubar: Option<usize>,
+ pub loaded_project_path: Option<std::path::PathBuf>,
+ pub last_saved_root_json: String,
+ pub recent_files: Vec<std::path::PathBuf>,
+ pub recent_files_list: ScrollingList,
+ pub recent_files_buttons: Vec<Button>,
+ pub text_buffer_cache: std::collections::HashMap<(String, u32, Option<String>), Buffer>,
+ pub viewport_dirty: bool,
+ pub last_viewport_camera_pos: Vec3,
+ pub last_viewport_camera_rx: f32,
+ pub last_viewport_camera_ry: f32,
+ pub last_viewport_camera_rz: f32,
+ pub last_viewport_pivot: Vec3,
+ pub last_viewport_zoom: f32,
+ pub last_viewport_rotation_x: f32,
+ pub last_viewport_rotation_y: f32,
+ pub last_viewport_bg_color: [f32; 3],
+ pub last_viewport_show_grid: bool,
+ pub last_viewport_show_cube: bool,
+ pub last_viewport_show_origin: bool,
+ pub last_viewport_show_camera_pivot: bool,
+ pub last_viewport_width: u32,
+ pub last_viewport_height: u32,
+ pub last_viewport_active_camera: String,
+ pub last_viewport_show_viewport: bool,
+ pub ui_context: clear_ui::context::UiContext,
+}
+
+impl State {
+ pub fn find_widget_index(&self, child_ptr: *mut (dyn Element + 'static)) -> Option<usize> {
+ let target_addr = child_ptr as *const ();
+ self.widgets.iter().position(|w| {
+ let w_ptr = w.as_ref() as *const dyn Element as *const ();
+ w_ptr == target_addr
+ })
+ }
+
+ pub fn has_any_open_menu(&self, idx: usize) -> bool {
+ let mut visited = vec![false; self.widgets.len()];
+ self.has_any_open_menu_impl(idx, &mut visited)
+ }
+
+ pub fn has_any_open_menu_impl(&self, idx: usize, visited: &mut [bool]) -> bool {
+ if idx >= self.widgets.len() {
+ return false;
+ }
+ if visited[idx] {
+ return false;
+ }
+ visited[idx] = true;
+ if let Some(menu) = self.widgets[idx].as_menu_controller() {
+ if menu.is_menu_open() {
+ return true;
+ }
+ }
+ let child_ptrs = self.widgets[idx].children(&self.ui_context);
+ for child_ptr in child_ptrs {
+ if let Some(child_idx) = self.find_widget_index(child_ptr) {
+ if self.has_any_open_menu_impl(child_idx, visited) {
+ return true;
+ }
+ }
+ }
+ false
+ }
+
+ pub fn palette_mut(&mut self) -> &mut NodePalette {
+ self.widgets[NODE_PALETTE_IDX].as_any_mut().downcast_mut::<NodePalette>().expect("not a NodePalette")
+ }
+
+ pub fn menu(&self, idx: usize) -> &dyn clear_ui::widget::MenuController {
+ self.widgets[idx].as_menu_controller().expect("not a MenuController")
+ }
+
+ pub fn menu_mut(&mut self, idx: usize) -> &mut dyn clear_ui::widget::MenuController {
+ self.widgets[idx].as_menu_controller_mut().expect("not a MenuController")
+ }
+
+ pub fn graph(&self) -> &dyn clear_ui::widget::GraphController {
+ self.widgets[CONTENT_IDX].as_graph_controller().expect("not a GraphController")
+ }
+
+ pub fn graph_mut(&mut self) -> &mut dyn clear_ui::widget::GraphController {
+ self.widgets[CONTENT_IDX].as_graph_controller_mut().expect("not a GraphController")
+ }
+
+ pub fn page_selector(&self, idx: usize) -> &dyn clear_ui::widget::PageSelector {
+ self.widgets[idx].as_page_selector().expect("not a PageSelector")
+ }
+
+ pub fn page_selector_mut(&mut self, idx: usize) -> &mut dyn clear_ui::widget::PageSelector {
+ self.widgets[idx].as_page_selector_mut().expect("not a PageSelector")
+ }
+
+ pub fn param(&self) -> &dyn clear_ui::widget::ParamController {
+ self.widgets[PARAM_IDX].as_param_controller().expect("not a ParamController")
+ }
+
+ pub fn param_mut(&mut self) -> &mut dyn clear_ui::widget::ParamController {
+ self.widgets[PARAM_IDX].as_param_controller_mut().expect("not a ParamController")
+ }
+
+ pub fn spreadsheet_mut(&mut self) -> &mut dyn clear_ui::widget::SpreadsheetController {
+ self.widgets[SPREADSHEET_IDX].as_spreadsheet_controller_mut().expect("not a SpreadsheetController")
+ }
+
+ pub fn path_mut(&mut self) -> &mut dyn clear_ui::widget::PathController {
+ self.widgets[BREADCRUMB_IDX].as_path_controller_mut().expect("not a PathController")
+ }
+
+ pub fn geom_mut(&mut self, idx: usize) -> &mut dyn clear_ui::widget::GeomController {
+ self.widgets[idx].as_geom_controller_mut().expect("not a GeomController")
+ }
+
+ pub fn scroll_mut(&mut self, idx: usize) -> &mut dyn clear_ui::widget::ScrollController {
+ self.widgets[idx].as_scroll_controller_mut().expect("not a ScrollController")
+ }
+
+ pub fn has_unsaved_changes(&self) -> bool {
+ if let Ok(current_json) = serde_json::to_string(&self.fs_root) {
+ current_json != self.last_saved_root_json
+ } else {
+ false
+ }
+ }
+
+
+
+
+
+ pub fn update_recent_files_layout(&mut self) {
+ let active_menubar = self.focused_pane;
+ let menu_names = self.menu(active_menubar).menu_names();
+ let file_page_idx = match menu_names.iter().position(|name| name == "File") {
+ Some(idx) => idx,
+ None => return,
+ };
+
+ if file_page_idx >= self.paginator_page_widgets.len() {
+ return;
+ }
+
+ let menu_items = self.menu(active_menubar).menu_items_list();
+ if file_page_idx >= menu_items.len() {
+ return;
+ }
+ let num_base_items = menu_items[file_page_idx].len();
+ let list_idx = num_base_items + 1;
+
+ let page = &mut self.paginator_page_widgets[file_page_idx];
+ if list_idx >= page.len() {
+ return;
+ }
+
+ let (list_x, list_y, list_w, list_h) = page[list_idx].rect();
+ if list_h <= 0.0 {
+ return;
+ }
+
+ let count = self.recent_files.len();
+ page[list_idx].as_scroll_controller_mut().expect("not a ScrollController").update_bounds(count, list_y, list_h);
+
+ let btn_h = 22.0;
+ let inner_x = list_x + 4.0;
+ let inner_w = list_w - 16.0;
+
+ for (idx, btn) in self.recent_files_buttons.iter_mut().enumerate() {
+ if let Some(draw_y) = page[list_idx].as_scroll_controller().expect("not a ScrollController").get_item_draw_y(idx, 0.0) {
+ btn.set_rect(inner_x, draw_y, inner_w, btn_h);
+ } else {
+ btn.set_rect(-9999.0, -9999.0, 0.0, 0.0);
+ }
+ }
+ }
+
+ pub fn save_settings(&mut self) {
+ let settings = DesignSettings {
+ grid_snap_enabled: self.grid_snap_enabled,
+ network_grid_enabled: self.network_grid_visible,
+ grid_size_x: self.grid_size_x,
+ grid_size_y: self.grid_size_y,
+ skipped_row_h: self.skipped_row_h,
+ skipped_col_w: self.skipped_col_w,
+ show_grid_enabled: self.show_grid,
+ show_cube_enabled: self.show_cube,
+ show_origin_enabled: self.show_origin,
+ origin_size: self.origin_size,
+ viewport_bg_color: self.viewport_bg_color,
+ node_color: self.node_color,
+ grid_color: self.grid_color,
+ cell_color: self.cell_color,
+ gap_color: self.gap_color,
+ square_viewport: self.square_viewport,
+ grid_thickness: self.grid_thickness,
+ show_camera_pivot_enabled: self.show_camera_pivot,
+ camera_pivot_size: self.camera_pivot_size,
+ uniform_background: self.uniform_background,
+ network_opacity: self.network_opacity,
+ };
+ settings.save();
+ self.last_design_mod_time = {
+ let design_path = DesignSettings::file_path();
+ std::fs::metadata(&design_path).and_then(|m| m.modified()).ok()
+ };
+ }
+
+ pub fn sync_settings_from_paginator(&mut self) {
+ let active_menubar = self.focused_pane;
+ let page_idx = if active_menubar == LEFT_MENUBAR_IDX {
+ 3
+ } else if active_menubar == RIGHT_MENUBAR_IDX {
+ 4
+ } else {
+ return;
+ };
+
+ if page_idx >= self.paginator_page_widgets.len() {
+ return;
+ }
+
+ let mut left_values = None;
+ let mut right_values = None;
+
+ {
+ let widgets = &self.paginator_page_widgets[page_idx];
+ if widgets.is_empty() {
+ return;
+ }
+
+ if active_menubar == LEFT_MENUBAR_IDX {
+ if widgets.len() >= 11 {
+ let snap = widgets[0].value();
+ let vis = widgets[1].value();
+ let gx = widgets[2].value();
+ let gy = widgets[3].value();
+ let srh = widgets[4].value();
+ let scw = widgets[5].value();
+ let uni = widgets[6].value();
+ let op = widgets[7].value();
+ let node_col = widgets[8].color_u8();
+ let cell_col = widgets[9].color_u8();
+ let gap_col = widgets[10].color_u8();
+ left_values = Some((snap, vis, gx, gy, srh, scw, uni, op, node_col, cell_col, gap_col));
+ }
+ } else if active_menubar == RIGHT_MENUBAR_IDX {
+ if widgets.len() >= 13 {
+ let sg = widgets[0].value();
+ let sc = widgets[1].value();
+ let so = widgets[2].value();
+ let cp = widgets[3].value();
+ let gt = widgets[4].value();
+ let os = widgets[5].value();
+ let cps = widgets[6].value();
+ let bgr = widgets[7].value();
+ let bgg = widgets[8].value();
+ let bgb = widgets[9].value();
+ let gcr = widgets[10].value();
+ let gcg = widgets[11].value();
+ let gcb = widgets[12].value();
+ right_values = Some((sg, sc, so, cp, gt, os, cps, bgr, bgg, bgb, gcr, gcg, gcb));
+ }
+ }
+ }
+
+ let mut changed = false;
+
+ if let Some((snap, vis, gx, gy, srh, scw, uni, op, node_col, cell_col, gap_col)) = left_values {
+ // LEFT pane Settings
+ // 0: Snap to Grid (Checkbox)
+ let snap = snap == 1;
+ if self.grid_snap_enabled != snap {
+ self.grid_snap_enabled = snap;
+ changed = true;
+ }
+ // 1: Grid Visible (Checkbox)
+ let vis = vis == 1;
+ if self.network_grid_visible != vis {
+ self.network_grid_visible = vis;
+ changed = true;
+ }
+ // 2: Grid X (Spinbox)
+ let gx = gx as f32;
+ if self.grid_size_x != gx {
+ self.grid_size_x = gx;
+ changed = true;
+ }
+ // 3: Grid Y (Spinbox)
+ let gy = gy as f32;
+ if self.grid_size_y != gy {
+ self.grid_size_y = gy;
+ changed = true;
+ }
+ // 4: Skipped Row H (Spinbox)
+ let srh = srh as f32;
+ if self.skipped_row_h != srh {
+ self.skipped_row_h = srh;
+ changed = true;
+ }
+ // 5: Skipped Col W (Spinbox)
+ let scw = scw as f32;
+ if self.skipped_col_w != scw {
+ self.skipped_col_w = scw;
+ changed = true;
+ }
+ // 6: Uniform Background (Checkbox)
+ let uni = uni == 1;
+ if self.uniform_background != uni {
+ self.uniform_background = uni;
+ changed = true;
+ }
+ // 7: Opacity (Slider, 0..100)
+ let op = op as f32 / 100.0;
+ if (self.network_opacity - op).abs() > 0.001 {
+ self.network_opacity = op;
+ changed = true;
+ }
+ // 8: Node Color (ColorSelector)
+ if let Some(col) = node_col {
+ let nr = col[0] as f32 / 255.0;
+ let ng = col[1] as f32 / 255.0;
+ let nb = col[2] as f32 / 255.0;
+ if (self.node_color[0] - nr).abs() > 0.001
+ || (self.node_color[1] - ng).abs() > 0.001
+ || (self.node_color[2] - nb).abs() > 0.001
+ {
+ self.node_color = [nr, ng, nb];
+ colors::set_node_color([nr, ng, nb, 1.0]);
+ changed = true;
+ }
+ }
+ // 9: Cell Color (ColorSelector)
+ if let Some(col) = cell_col {
+ let cr = col[0] as f32 / 255.0;
+ let cg = col[1] as f32 / 255.0;
+ let cb = col[2] as f32 / 255.0;
+ if (self.cell_color[0] - cr).abs() > 0.001
+ || (self.cell_color[1] - cg).abs() > 0.001
+ || (self.cell_color[2] - cb).abs() > 0.001
+ {
+ self.cell_color = [cr, cg, cb];
+ changed = true;
+ }
+ }
+ // 10: Gap Color (ColorSelector)
+ if let Some(col) = gap_col {
+ let gr = col[0] as f32 / 255.0;
+ let gg = col[1] as f32 / 255.0;
+ let gb = col[2] as f32 / 255.0;
+ if (self.gap_color[0] - gr).abs() > 0.001
+ || (self.gap_color[1] - gg).abs() > 0.001
+ || (self.gap_color[2] - gb).abs() > 0.001
+ {
+ self.gap_color = [gr, gg, gb];
+ changed = true;
+ }
+ }
+ } else if let Some((sg, sc, so, cp, gt, os, cps, bgr, bgg, bgb, gcr, gcg, gcb)) = right_values {
+ // RIGHT pane Settings
+ // 0: Show Grid Guide (Checkbox)
+ let sg = sg == 1;
+ if self.show_grid != sg {
+ self.show_grid = sg;
+ self.menu_mut(RIGHT_MENUBAR_IDX).set_item_checked(2, 0, sg);
+ changed = true;
+ }
+ // 1: Show Reference Cube (Checkbox)
+ let sc = sc == 1;
+ if self.show_cube != sc {
+ self.show_cube = sc;
+ self.menu_mut(RIGHT_MENUBAR_IDX).set_item_checked(2, 1, sc);
+ changed = true;
+ }
+ // 2: Show Origin Axes (Checkbox)
+ let so = so == 1;
+ if self.show_origin != so {
+ self.show_origin = so;
+ self.menu_mut(RIGHT_MENUBAR_IDX).set_item_checked(2, 2, so);
+ changed = true;
+ }
+ // 3: Show Camera Pivot (Checkbox)
+ let cp = cp == 1;
+ if self.show_camera_pivot != cp {
+ self.show_camera_pivot = cp;
+ self.menu_mut(RIGHT_MENUBAR_IDX).set_item_checked(2, 3, cp);
+ changed = true;
+ }
+ // 4: Grid Thickness (Spinbox, decimals 3)
+ let gt = gt as f32 / 1000.0;
+ if (self.grid_thickness - gt).abs() > 0.0001 {
+ self.grid_thickness = gt;
+ self.update_grid_geometry();
+ changed = true;
+ }
+ // 5: Origin Guide Size (Spinbox, decimals 1)
+ let os = os as f32 / 10.0;
+ if (self.origin_size - os).abs() > 0.001 {
+ self.origin_size = os;
+ self.update_origin_geometry();
+ changed = true;
+ }
+ // 6: Camera Pivot Size (Spinbox, decimals 1)
+ let cps = cps as f32 / 10.0;
+ if (self.camera_pivot_size - cps).abs() > 0.001 {
+ self.camera_pivot_size = cps;
+ self.update_pivot_geometry();
+ changed = true;
+ }
+ // 7, 8, 9: BG Color (R, G, B Spinboxes, 0..255)
+ let bgr = bgr as f32 / 255.0;
+ let bgg = bgg as f32 / 255.0;
+ let bgb = bgb as f32 / 255.0;
+ if (self.viewport_bg_color[0] - bgr).abs() > 0.001
+ || (self.viewport_bg_color[1] - bgg).abs() > 0.001
+ || (self.viewport_bg_color[2] - bgb).abs() > 0.001
+ {
+ self.viewport_bg_color = [bgr, bgg, bgb];
+ changed = true;
+ }
+ // 10, 11, 12: Grid Color (R, G, B Spinboxes, 0..255)
+ let gcr = gcr as f32 / 255.0;
+ let gcg = gcg as f32 / 255.0;
+ let gcb = gcb as f32 / 255.0;
+ if (self.grid_color[0] - gcr).abs() > 0.001
+ || (self.grid_color[1] - gcg).abs() > 0.001
+ || (self.grid_color[2] - gcb).abs() > 0.001
+ {
+ self.grid_color = [gcr, gcg, gcb];
+ self.update_grid_geometry();
+ changed = true;
+ }
+ }
+
+ if changed {
+ self.sync_grid_settings();
+ self.save_settings();
+ self.upload_vertices();
+ }
+ }
+
+ pub fn update_grid_geometry(&mut self) {
+ let grid_verts = grid_vertices(self.grid_thickness, self.grid_color);
+ self.wgpu_adapter.queue.write_buffer(&self.vertex_buffer_grid, 0, bytemuck::cast_slice(&grid_verts));
+ self.viewport_dirty = true;
+ }
+
+ pub fn update_origin_geometry(&mut self) {
+ let origin_verts = origin_vectors_vertices(self.origin_size);
+ self.wgpu_adapter.queue.write_buffer(&self.vertex_buffer_origin, 0, bytemuck::cast_slice(&origin_verts));
+ self.viewport_dirty = true;
+ }
+
+ pub fn update_pivot_geometry(&mut self) {
+ let pivot_verts = camera_pivot_vertices(self.camera_pivot_size);
+ self.wgpu_adapter.queue.write_buffer(&self.vertex_buffer_pivot, 0, bytemuck::cast_slice(&pivot_verts));
+ self.viewport_dirty = true;
+ }
+
+ pub fn body_h(&self) -> f32 { self.height - HEADER_H - STATUS_H }
+
+ pub fn get_col_geometries(&self) -> (f32, f32, f32, f32, f32, f32) {
+ let left_visible = self.show_network && !self.circular_network_pane && !self.is_detached_network && !self.detached_circular_network;
+ let center_visible = self.show_viewport || self.show_spreadsheet;
+ let right_visible = self.show_parameters;
+
+ let (col_l_x, col_l_w, col_c_x, col_c_w, col_r_x, col_r_w) =
+ match (left_visible, center_visible, right_visible) {
+ (true, true, true) => {
+ let l_w = self.splitter_layout.splitter1_x;
+ let c_x = l_w + SPLITTER_W;
+ let c_w = self.splitter_layout.splitter2_x - c_x;
+ let r_x = self.splitter_layout.splitter2_x + SPLITTER_W;
+ let r_w = (self.width - r_x).max(0.0);
+ (0.0, l_w, c_x, c_w, r_x, r_w)
+ }
+ (true, true, false) => {
+ let l_w = self.splitter_layout.splitter1_x;
+ let c_x = l_w + SPLITTER_W;
+ let c_w = (self.width - c_x).max(0.0);
+ (0.0, l_w, c_x, c_w, 0.0, 0.0)
+ }
+ (true, false, true) => {
+ let l_w = self.splitter_layout.splitter2_x;
+ let r_x = l_w + SPLITTER_W;
+ let r_w = (self.width - r_x).max(0.0);
+ (0.0, l_w, 0.0, 0.0, r_x, r_w)
+ }
+ (false, true, true) => {
+ let c_w = self.splitter_layout.splitter2_x;
+ let r_x = c_w + SPLITTER_W;
+ let r_w = (self.width - r_x).max(0.0);
+ (0.0, 0.0, 0.0, c_w, r_x, r_w)
+ }
+ (true, false, false) => {
+ (0.0, self.width, 0.0, 0.0, 0.0, 0.0)
+ }
+ (false, true, false) => {
+ (0.0, 0.0, 0.0, self.width, 0.0, 0.0)
+ }
+ (false, false, true) => {
+ (0.0, 0.0, 0.0, 0.0, 0.0, self.width)
+ }
+ (false, false, false) => {
+ (0.0, 0.0, 0.0, self.width, 0.0, 0.0)
+ }
+ };
+ (col_l_x, col_l_w, col_c_x, col_c_w, col_r_x, col_r_w)
+ }
+
+ pub fn content_left_w(&self) -> f32 { self.get_col_geometries().1 }
+
+ pub fn content_right_x(&self) -> f32 { self.get_col_geometries().2 }
+
+ pub fn viewport_w(&self) -> f32 { self.get_col_geometries().3 }
+
+ pub fn param_x(&self) -> f32 { self.get_col_geometries().4 }
+
+ pub fn param_w(&self) -> f32 { self.get_col_geometries().5 }
+
+ pub fn in_network_pane(&self) -> bool {
+ if self.circular_network_pane {
+ self.circular_network_layout.hit_test_content(self.cursor_x, self.cursor_y, 0.0, BREADCRUMB_H)
+ } else {
+ let (cx, cy, cw, ch) = self.positions[CONTENT_IDX];
+ self.cursor_x >= cx
+ && self.cursor_x < cx + cw
+ && self.cursor_y >= cy
+ && self.cursor_y < cy + ch
+ }
+ }
+
+ pub fn clamp_splitters(&mut self) {
+ if self.is_detached_network {
+ return;
+ }
+ self.splitter_layout.clamp(self.width, self.detached_circular_network);
+ }
+
+ pub fn current_dir(&self) -> &FsNode {
+ let mut node = &self.fs_root;
+ for &i in &self.current_path {
+ node = &node.children[i];
+ }
+ node
+ }
+
+ pub fn current_dir_mut(&mut self) -> &mut FsNode {
+ let mut node = &mut self.fs_root;
+ for &i in &self.current_path {
+ node = &mut node.children[i];
+ }
+ node
+ }
+
+ pub fn sync_parameters_to_project(&mut self) {
+ if !self.is_detached_network {
+ if let Some(slot_idx) = self.graph().selected_node() {
+ let updated_params = self.param().node_params();
+ let dir = self.current_dir_mut();
+ if let Some(child) = dir.children.get_mut(slot_idx) {
+ let mut param_changed = false;
+ for (u_name, u_val, _) in &updated_params {
+ if let Some(p) = child.params.iter_mut().find(|p| p.name == *u_name) {
+ if p.default != *u_val {
+ p.default = u_val.clone();
+ param_changed = true;
+ }
+ }
+ }
+ if param_changed {
+ self.rebuild_scene_geometry();
+ self.sync_nodes();
+ }
+ }
+ }
+ }
+ }
+
+ pub fn current_path_names(&self) -> Vec<String> {
+ let mut node = &self.fs_root;
+ let mut names = Vec::new();
+ for &i in &self.current_path {
+ if let Some(child) = node.children.get(i) {
+ names.push(child.name.clone());
+ node = child;
+ }
+ }
+ names
+ }
+
+ pub fn refresh_node_palette(&mut self) {
+ let q = self.node_palette_query.to_lowercase();
+ self.node_palette_filtered = self.node_templates.iter().enumerate()
+ .filter_map(|(i, t)| {
+ if q.is_empty() || t.label.to_lowercase().contains(&q) { Some(i) } else { None }
+ })
+ .collect();
+ if self.node_palette_selected >= self.node_palette_filtered.len() {
+ self.node_palette_selected = self.node_palette_filtered.len().saturating_sub(1);
+ }
+ let items: Vec<String> = self.node_palette_filtered.iter()
+ .map(|&i| self.node_templates[i].label.clone())
+ .collect();
+ let visible = self.node_palette_visible;
+ let query = self.node_palette_query.clone();
+ let selected = self.node_palette_selected;
+ self.palette_mut().set_palette_state(
+ visible,
+ &query,
+ &items,
+ selected,
+ );
+ }
+
+ pub fn open_file_chooser(&self) {
+ std::thread::spawn(move || {
+ use std::process::{Command, Stdio};
+ use std::io::Write;
+
+ // Find executable path
+ let exe_path = if let Ok(cur_exe) = std::env::current_exe() {
+ let sibling = cur_exe.with_file_name("clear-filesystem-interface");
+ if sibling.exists() {
+ sibling
+ } else {
+ std::path::PathBuf::from("clear-filesystem-interface")
+ }
+ } else {
+ std::path::PathBuf::from("clear-filesystem-interface")
+ };
+
+ let child = match Command::new(exe_path)
+ .arg("--select")
+ .stdout(Stdio::piped())
+ .spawn()
+ {
+ Ok(c) => c,
+ Err(e) => {
+ eprintln!("Failed to spawn clear-filesystem-interface: {:?}", e);
+ return;
+ }
+ };
+
+ let output = match child.wait_with_output() {
+ Ok(o) => o,
+ Err(e) => {
+ eprintln!("Failed to wait for clear-filesystem-interface: {:?}", e);
+ return;
+ }
+ };
+
+ if output.status.success() {
+ let selected = String::from_utf8_lossy(&output.stdout).trim().to_string();
+ if !selected.is_empty() {
+ let body = format!(
+ "{{\"action\":\"load\",\"path\":\"{}\"}}",
+ selected.replace('\\', "\\\\").replace('"', "\\\"")
+ );
+ let req = format!(
+ "POST /action HTTP/1.1\r\n\
+ Host: 127.0.0.1:3000\r\n\
+ Content-Type: application/json\r\n\
+ Content-Length: {}\r\n\
+ Connection: close\r\n\r\n\
+ {}",
+ body.len(),
+ body
+ );
+ if let Ok(mut stream) = std::net::TcpStream::connect("127.0.0.1:3000") {
+ let _ = stream.write_all(req.as_bytes());
+ let _ = stream.flush();
+ }
+ }
+ }
+ });
+ }
+
+ pub fn save_file_chooser(&self) {
+ std::thread::spawn(move || {
+ use std::process::{Command, Stdio};
+ use std::io::Write;
+
+ // Find executable path
+ let exe_path = if let Ok(cur_exe) = std::env::current_exe() {
+ let sibling = cur_exe.with_file_name("clear-filesystem-interface");
+ if sibling.exists() {
+ sibling
+ } else {
+ std::path::PathBuf::from("clear-filesystem-interface")
+ }
+ } else {
+ std::path::PathBuf::from("clear-filesystem-interface")
+ };
+
+ let child = match Command::new(exe_path)
+ .arg("--save")
+ .stdout(Stdio::piped())
+ .spawn()
+ {
+ Ok(c) => c,
+ Err(e) => {
+ eprintln!("Failed to spawn clear-filesystem-interface: {:?}", e);
+ return;
+ }
+ };
+
+ let output = match child.wait_with_output() {
+ Ok(o) => o,
+ Err(e) => {
+ eprintln!("Failed to wait for clear-filesystem-interface: {:?}", e);
+ return;
+ }
+ };
+
+ if output.status.success() {
+ let selected = String::from_utf8_lossy(&output.stdout).trim().to_string();
+ if !selected.is_empty() {
+ let body = format!(
+ "{{\"action\":\"save\",\"path\":\"{}\"}}",
+ selected.replace('\\', "\\\\").replace('"', "\\\"")
+ );
+ let req = format!(
+ "POST /action HTTP/1.1\r\n\
+ Host: 127.0.0.1:3000\r\n\
+ Content-Type: application/json\r\n\
+ Content-Length: {}\r\n\
+ Connection: close\r\n\r\n\
+ {}",
+ body.len(),
+ body
+ );
+ if let Ok(mut stream) = std::net::TcpStream::connect("127.0.0.1:3000") {
+ let _ = stream.write_all(req.as_bytes());
+ let _ = stream.flush();
+ }
+ }
+ }
+ });
+ }
+
+ pub fn open_node_palette(&mut self) {
+ self.node_palette_visible = true;
+ self.node_palette_query = String::new();
+ self.node_palette_selected = 0;
+ self.refresh_node_palette();
+ self.upload_vertices();
+ }
+
+ pub fn close_node_palette(&mut self) {
+ self.node_palette_visible = false;
+ self.refresh_node_palette();
+ self.upload_vertices();
+ }
+
+
+ pub fn place_selected_node(&mut self) -> bool {
+ let Some(&template_idx) = self.node_palette_filtered.get(self.node_palette_selected) else { return false; };
+ let mut node = self.node_templates[template_idx].node.clone();
+ let (nx, ny) = self.find_empty_cell(self.grid_cursor_col as f32, self.grid_cursor_row as f32, None);
+ node.position = (nx, ny);
+ self.current_dir_mut().children.push(node);
+ self.close_node_palette();
+ self.sync_nodes();
+ self.rebuild_positions();
+ self.apply_layout();
+ self.update_panel_bounds();
+ self.upload_vertices();
+ true
+ }
+
+
+ pub fn handle_node_palette_key(&mut self, event: &KeyEvent) -> bool {
+ if event.state != ElementState::Pressed { return false; }
+ match &event.logical_key {
+ Key::Named(NamedKey::Escape) => {
+ self.close_node_palette();
+ true
+ }
+ Key::Named(NamedKey::Enter) => self.place_selected_node(),
+ Key::Named(NamedKey::ArrowDown) => {
+ if !self.node_palette_filtered.is_empty() {
+ self.node_palette_selected = (self.node_palette_selected + 1).min(self.node_palette_filtered.len() - 1);
+ self.refresh_node_palette();
+ self.upload_vertices();
+ }
+ true
+ }
+ Key::Named(NamedKey::ArrowUp) => {
+ if self.node_palette_selected > 0 {
+ self.node_palette_selected -= 1;
+ self.refresh_node_palette();
+ self.upload_vertices();
+ }
+ true
+ }
+ Key::Named(NamedKey::Backspace) => {
+ self.node_palette_query.pop();
+ self.refresh_node_palette();
+ self.upload_vertices();
+ true
+ }
+ Key::Named(NamedKey::Tab) => {
+ self.close_node_palette();
+ true
+ }
+ _ => {
+ if let Some(text) = &event.text {
+ for ch in text.chars().filter(|c| !c.is_control()) {
+ self.node_palette_query.push(ch);
+ }
+ self.refresh_node_palette();
+ self.upload_vertices();
+ true
+ } else {
+ false
+ }
+ }
+ }
+ }
+
+fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String>>) {
+ let mut headers = vec![
+ "Vertex".to_string(),
+ "Pos.x".to_string(),
+ "Pos.y".to_string(),
+ "Pos.z".to_string(),
+ "Col.r".to_string(),
+ "Col.g".to_string(),
+ "Col.b".to_string(),
+ ];
+
+ let mut custom_keys = std::collections::BTreeSet::new();
+ for v in &geom.vertices {
+ for k in v.attributes.keys() {
+ custom_keys.insert(k.clone());
+ }
+ }
+ let custom_keys: Vec<String> = custom_keys.into_iter().collect();
+
+ for key in &custom_keys {
+ if let Some(val) = geom.vertices.iter().find_map(|v| v.attributes.get(key)) {
+ match val {
+ GAttribute::Float(_) => {
+ headers.push(key.clone());
+ }
+ GAttribute::Float2(_) => {
+ headers.push(format!("{}.x", key));
+ headers.push(format!("{}.y", key));
+ }
+ GAttribute::Float3(_) => {
+ headers.push(format!("{}.x", key));
+ headers.push(format!("{}.y", key));
+ headers.push(format!("{}.z", key));
+ }
+ GAttribute::Float4(_) => {
+ headers.push(format!("{}.x", key));
+ headers.push(format!("{}.y", key));
+ headers.push(format!("{}.z", key));
+ headers.push(format!("{}.w", key));
+ }
+ }
+ }
+ }
+
+ let mut rows = Vec::new();
+ for (i, v) in geom.vertices.iter().enumerate() {
+ let mut row = vec![
+ i.to_string(),
+ format!("{:.4}", v.pos[0]),
+ format!("{:.4}", v.pos[1]),
+ format!("{:.4}", v.pos[2]),
+ format!("{:.4}", v.col[0]),
+ format!("{:.4}", v.col[1]),
+ format!("{:.4}", v.col[2]),
+ ];
+
+ for key in &custom_keys {
+ if let Some(val) = v.attributes.get(key) {
+ match val {
+ GAttribute::Float(f) => {
+ row.push(format!("{:.4}", f));
+ }
+ GAttribute::Float2(arr) => {
+ row.push(format!("{:.4}", arr[0]));
+ row.push(format!("{:.4}", arr[1]));
+ }
+ GAttribute::Float3(arr) => {
+ row.push(format!("{:.4}", arr[0]));
+ row.push(format!("{:.4}", arr[1]));
+ row.push(format!("{:.4}", arr[2]));
+ }
+ GAttribute::Float4(arr) => {
+ row.push(format!("{:.4}", arr[0]));
+ row.push(format!("{:.4}", arr[1]));
+ row.push(format!("{:.4}", arr[2]));
+ row.push(format!("{:.4}", arr[3]));
+ }
+ }
+ } else {
+ if let Some(val) = geom.vertices.iter().find_map(|v| v.attributes.get(key)) {
+ let count = match val {
+ GAttribute::Float(_) => 1,
+ GAttribute::Float2(_) => 2,
+ GAttribute::Float3(_) => 3,
+ GAttribute::Float4(_) => 4,
+ };
+ for _ in 0..count {
+ row.push("-".to_string());
+ }
+ }
+ }
+ }
+ rows.push(row);
+ }
+
+ (headers, rows)
+}
+
+ pub fn update_active_camera_rotation(&mut self, d_yaw: f32, d_pitch: f32) -> bool {
+ if self.active_camera == "Default Camera" {
+ return false;
+ }
+ let camera_name = self.active_camera.clone();
+ let dir = self.current_dir_mut();
+ if let Some(node) = dir.children.iter_mut().find(|c| c.node_type == "camera" && c.name == camera_name) {
+ if let Some(p) = node.params.iter_mut().find(|p| p.name == "Rotation") {
+ let parts: Vec<&str> = p.default
+ .split(|c| c == ':' || c == ',' || c == ' ')
+ .filter(|s| !s.is_empty())
+ .collect();
+ let mut rx = 0.0f32;
+ let mut ry = 0.0f32;
+ let mut rz = 0.0f32;
+ if parts.len() >= 3 {
+ if let (Ok(vx), Ok(vy), Ok(vz)) = (parts[0].parse::<f32>(), parts[1].parse::<f32>(), parts[2].parse::<f32>()) {
+ rx = vx;
+ ry = vy;
+ rz = vz;
+ }
+ }
+ ry += d_yaw.to_degrees();
+ rx -= d_pitch.to_degrees();
+ while rx > 180.0 { rx -= 360.0; }
+ while rx < -180.0 { rx += 360.0; }
+ while ry > 180.0 { ry -= 360.0; }
+ while ry < -180.0 { ry += 360.0; }
+ p.default = format!("{:.2}:{:.2}:{:.2}", rx, ry, rz);
+
+ self.sync_nodes();
+ let params = if !self.is_detached_network {
+ self.graph().selected_node().and_then(|sel_idx| {
+ let dir = self.current_dir();
+ if sel_idx < dir.children.len() {
+ Some(param_display(&dir.children[sel_idx].params))
+ } else { None }
+ }).unwrap_or_default()
+ } else {
+ vec![]
+ };
+ self.param_mut().set_display_params(¶ms);
+
+ return true;
+ }
+ }
+ false
+ }
+
+ pub fn update_active_camera_rotation_reset(&mut self) -> bool {
+ if self.active_camera == "Default Camera" {
+ return false;
+ }
+ let camera_name = self.active_camera.clone();
+ let dir = self.current_dir_mut();
+ if let Some(node) = dir.children.iter_mut().find(|c| c.node_type == "camera" && c.name == camera_name) {
+ if let Some(p) = node.params.iter_mut().find(|p| p.name == "Rotation") {
+ p.default = "0.00:0.00:0.00".to_string();
+ self.sync_nodes();
+ let params = if !self.is_detached_network {
+ self.graph().selected_node().and_then(|sel_idx| {
+ let dir = self.current_dir();
+ if sel_idx < dir.children.len() {
+ Some(param_display(&dir.children[sel_idx].params))
+ } else { None }
+ }).unwrap_or_default()
+ } else {
+ vec![]
+ };
+ self.param_mut().set_display_params(¶ms);
+ return true;
+ }
+ }
+ false
+ }
+
+ pub fn on_path_changed(&mut self) {
+ self.graph_mut().set_selected_node(None);
+ self.drag_widget = None;
+ self.focused_widget = None;
+ self.pan_x = 0.0;
+ self.pan_y = 0.0;
+ self.pan_velocity_x = 0.0;
+ self.pan_velocity_y = 0.0;
+ self.last_frame_pan_x = 0.0;
+ self.last_frame_pan_y = 0.0;
+ self.is_scrolling_trackpad = false;
+ self.scroll_accum_x = 0.0;
+ self.scroll_accum_y = 0.0;
+ self.grid_cursor_col = 0;
+ self.grid_cursor_row = 0;
+ self.sync_grid_settings();
+ self.sync_nodes();
+ self.rebuild_scene_geometry();
+ self.rebuild_positions();
+ self.apply_layout();
+ self.update_panel_bounds();
+ self.upload_vertices();
+ }
+
+ pub fn find_empty_cell(&self, start_x: f32, start_y: f32, skip_idx: Option<usize>) -> (f32, f32) {
+ let x = start_x;
+ let mut y = start_y;
+ let dir = self.current_dir();
+ loop {
+ let occupied = dir.children.iter().enumerate().any(|(idx, c)| {
+ if Some(idx) == skip_idx {
+ false
+ } else {
+ (c.position.0 - x).abs() < 0.01 && (c.position.1 - y).abs() < 0.01
+ }
+ });
+ if occupied {
+ y += 1.0;
+ } else {
+ break;
+ }
+ }
+ (x, y)
+ }
+
+ pub fn sync_nodes(&mut self) {
+ let graph_nodes: Vec<GraphNode> = self.current_dir().children.iter().map(|c| {
+ GraphNode {
+ id: c.id.clone(),
+ name: c.name.clone(),
+ position: c.position,
+ parameters: param_display(&c.params),
+ geom_visible: c.geometry_visible,
+ }
+ }).collect();
+ self.graph_mut().set_nodes(&graph_nodes);
+
+ 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.menu_mut(RIGHT_MENUBAR_IDX).set_menu_items(0, &items);
+ for (i, item) in items.iter().enumerate() {
+ let checked = item == &self.active_camera;
+ self.menu_mut(RIGHT_MENUBAR_IDX).set_item_checked(0, i, checked);
+ }
+ let path_strs = self.current_path_names();
+ self.path_mut().set_path(&path_strs);
+
+ let mut selected_node = None;
+ if !self.is_detached_network {
+ if let Some(slot_idx) = self.graph().selected_node() {
+ let dir = self.current_dir();
+ if slot_idx < dir.children.len() {
+ selected_node = Some(&dir.children[slot_idx]);
+ }
+ }
+ }
+
+
+ let mut cache_hit = false;
+ let mut current_name = None;
+ let mut current_params = None;
+
+ if let Some(node) = selected_node {
+ current_name = Some(node.name.clone());
+ current_params = Some(node.params.iter().map(|p| (p.name.clone(), p.default.clone())).collect::<Vec<_>>());
+ if self.last_spreadsheet_node_name == current_name && self.last_spreadsheet_node_params == current_params {
+ cache_hit = true;
+ }
+ } else {
+ if self.last_spreadsheet_node_name.is_none() {
+ cache_hit = true;
+ }
+ }
+
+ if !cache_hit {
+ let mut headers = Vec::new();
+ let mut rows = Vec::new();
+
+ if let Some(node) = selected_node {
+ if node.node_type.eq_ignore_ascii_case("sphere") {
+ if let Some(idx) = find_sphere_index(&self.fs_root, node) {
+ let center = Vec3::new((idx % 4) as f32 * 1.25 - 1.875, 0.55, -((idx / 4) as f32) * 1.25);
+ let radius = node_param_f32(node, "Radius", 0.5).max(0.05);
+ let geom = sphere_vertices(center, radius);
+ let (h, r) = Self::geometry_to_spreadsheet_data(&geom);
+ headers = h;
+ rows = r;
+ }
+ } else if node.node_type.eq_ignore_ascii_case("line") {
+ if let Some(idx) = find_sphere_index(&self.fs_root, node) {
+ let start = Vec3::new((idx % 4) as f32 * 1.25 - 1.875, 0.55, -((idx / 4) as f32) * 1.25);
+ let length = node_param_f32(node, "Length", 1.0);
+ let thickness = node_param_f32(node, "Thickness", 0.02);
+ let end = start + Vec3::new(0.0, length, 0.0);
+ let geom = line_vertices(start, end, thickness);
+ let (h, r) = Self::geometry_to_spreadsheet_data(&geom);
+ headers = h;
+ rows = r;
+ }
+ } else if node.node_type.eq_ignore_ascii_case("add") {
+ if let Some(idx) = find_sphere_index(&self.fs_root, node) {
+ let center = Vec3::new((idx % 4) as f32 * 1.25 - 1.875, 0.55, -((idx / 4) as f32) * 1.25);
+ let num_points = node_param_f32(node, "Points", 100.0) as i32;
+ let mut geom = Geometry::new();
+ for i in 0..num_points {
+ let t = i as f32 / num_points.max(1) as f32;
+ let angle = t * std::f32::consts::TAU * 3.0;
+ let r = 0.4 * t;
+ let px = center.x + r * angle.cos();
+ let py = center.y + t * 0.5 - 0.25;
+ let pz = center.z + r * angle.sin();
+ let pt_center = Vec3::new(px, py, pz);
+ geom.merge(sphere_vertices(pt_center, 0.02));
+ }
+ let (h, r) = Self::geometry_to_spreadsheet_data(&geom);
+ headers = h;
+ rows = r;
+ }
+ } else if node.node_type.eq_ignore_ascii_case("transform") {
+ let mut visited = Vec::new();
+ if let Some(geom) = resolve_transform_geometry(&self.fs_root, node, &mut visited) {
+ let (h, r) = Self::geometry_to_spreadsheet_data(&geom);
+ headers = h;
+ rows = r;
+ }
+ }
+ }
+
+ self.spreadsheet_mut().set_spreadsheet_data(headers, rows);
+ self.last_spreadsheet_node_name = current_name;
+ self.last_spreadsheet_node_params = current_params;
+ }
+ }
+
+
+
+
+
+
+
+
+
+
+
+ pub async fn new(
+ conn: &Connection,
+ qh: &QueueHandle<AppState>,
+ compositor_state: &CompositorState,
+ xdg_shell_state: &XdgShell,
+ pw: u32,
+ ph: u32,
+ scale: f64,
+ is_detached_network: bool,
+ ) -> Self {
+ clear_ui::scale::set_scale_factor(scale as f32);
+ let settings = DesignSettings::load();
+ let lw = pw as f32 / scale as f32;
+ let lh = ph as f32 / scale as f32;
+ let sw = lw;
+
+ let wl_surface = compositor_state.create_surface(qh);
+ wl_surface.set_buffer_scale(scale as i32);
+ let window = xdg_shell_state.create_window(wl_surface.clone(), WindowDecorations::None, qh);
+ if is_detached_network {
+ window.set_title("Network Pane");
+ window.set_app_id("circular-network-pane");
+ window.set_min_size(Some((200, 200)));
+ } else {
+ window.set_title("Clear Design Interface");
+ window.set_app_id("cce-design-interface");
+ window.set_min_size(Some((480, 320)));
+ }
+ window.commit();
+
+ let display_ptr = conn.backend().display_id().as_ptr() as *mut std::ffi::c_void;
+ let surface_ptr = wl_surface.id().as_ptr() as *mut std::ffi::c_void;
+ let wgpu_adapter = clear_ui::backend::WgpuAdapter::new(display_ptr, surface_ptr, pw, ph).await;
+
+ let device = &wgpu_adapter.device;
+ let queue = &wgpu_adapter.queue;
+ let surface = &wgpu_adapter.surface;
+ let config = &wgpu_adapter.config;
+
+ let backdrop_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("Backdrop Bind Group Layout"),
+ entries: &[
+ wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Texture {
+ multisampled: false,
+ view_dimension: wgpu::TextureViewDimension::D2,
+ sample_type: wgpu::TextureSampleType::Float { filterable: true },
+ },
+ count: None,
+ },
+ wgpu::BindGroupLayoutEntry {
+ binding: 1,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
+ count: None,
+ },
+ ],
+ });
+
+ let backdrop_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
+ label: Some("Backdrop Sampler"),
+ address_mode_u: wgpu::AddressMode::ClampToEdge,
+ address_mode_v: wgpu::AddressMode::ClampToEdge,
+ address_mode_w: wgpu::AddressMode::ClampToEdge,
+ mag_filter: wgpu::FilterMode::Linear,
+ min_filter: wgpu::FilterMode::Linear,
+ mipmap_filter: wgpu::FilterMode::Nearest,
+ ..Default::default()
+ });
+
+ let backdrop_texture = device.create_texture(&wgpu::TextureDescriptor {
+ label: Some("Backdrop Texture"),
+ size: wgpu::Extent3d { width: pw.max(1), height: ph.max(1), depth_or_array_layers: 1 },
+ mip_level_count: 1,
+ sample_count: 1,
+ dimension: wgpu::TextureDimension::D2,
+ format: config.format,
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC,
+ view_formats: &[],
+ });
+ let backdrop_texture_view = backdrop_texture.create_view(&wgpu::TextureViewDescriptor::default());
+
+ let backdrop_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("Backdrop Bind Group"),
+ layout: &backdrop_bind_group_layout,
+ entries: &[
+ wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::TextureView(&backdrop_texture_view),
+ },
+ wgpu::BindGroupEntry {
+ binding: 1,
+ resource: wgpu::BindingResource::Sampler(&backdrop_sampler),
+ },
+ ],
+ });
+
+ let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("Shader"),
+ source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
+ });
+
+ let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("Pipeline Layout"),
+ bind_group_layouts: &[&backdrop_bind_group_layout],
+ push_constant_ranges: &[],
+ });
+
+ let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: Some("Render Pipeline"),
+ layout: Some(&pipeline_layout),
+ vertex: wgpu::VertexState {
+ module: &shader,
+ entry_point: Some("vs_main"),
+ buffers: &[Vertex::desc()],
+ compilation_options: Default::default(),
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &shader,
+ entry_point: Some("fs_main"),
+ targets: &[Some(wgpu::ColorTargetState {
+ format: config.format,
+ blend: Some(wgpu::BlendState::ALPHA_BLENDING),
+ write_mask: wgpu::ColorWrites::ALL,
+ })],
+ compilation_options: Default::default(),
+ }),
+ primitive: wgpu::PrimitiveState {
+ topology: wgpu::PrimitiveTopology::TriangleList,
+ strip_index_format: None,
+ front_face: wgpu::FrontFace::Ccw,
+ cull_mode: None,
+ polygon_mode: wgpu::PolygonMode::Fill,
+ unclipped_depth: false,
+ conservative: false,
+ },
+ depth_stencil: None,
+ multisample: wgpu::MultisampleState { count: 1, mask: !0, alpha_to_coverage_enabled: false },
+ multiview: None,
+ cache: None,
+ });
+
+ // 3D pipeline
+ let shader_3d = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("Shader 3D"),
+ source: wgpu::ShaderSource::Wgsl(include_str!("shader_3d.wgsl").into()),
+ });
+
+ let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("Uniform Buffer"),
+ size: 64,
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+
+ let bind_group_layout_3d = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("3D Bind Group Layout"),
+ entries: &[wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::VERTEX,
+ ty: wgpu::BindingType::Buffer {
+ ty: wgpu::BufferBindingType::Uniform,
+ has_dynamic_offset: false,
+ min_binding_size: wgpu::BufferSize::new(64),
+ },
+ count: None,
+ }],
+ });
+
+ let pipeline_layout_3d = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("3D Pipeline Layout"),
+ bind_group_layouts: &[&bind_group_layout_3d],
+ push_constant_ranges: &[],
+ });
+
+ let bind_group_3d = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("3D Bind Group"),
+ layout: &bind_group_layout_3d,
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
+ buffer: &uniform_buffer,
+ offset: 0,
+ size: wgpu::BufferSize::new(64),
+ }),
+ }],
+ });
+
+ let uniform_buffer_grid = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("Grid Uniform Buffer"),
+ size: 64,
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+
+ let bind_group_grid = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("Grid 3D Bind Group"),
+ layout: &bind_group_layout_3d,
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
+ buffer: &uniform_buffer_grid,
+ offset: 0,
+ size: wgpu::BufferSize::new(64),
+ }),
+ }],
+ });
+
+ let uniform_buffer_pivot = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("Pivot Uniform Buffer"),
+ size: 64,
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+
+ let bind_group_pivot = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("Pivot Bind Group"),
+ layout: &bind_group_layout_3d,
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
+ buffer: &uniform_buffer_pivot,
+ offset: 0,
+ size: wgpu::BufferSize::new(64),
+ }),
+ }],
+ });
+
+ let vertex_buffer_3d = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+ label: Some("Cube Vertex Buffer"),
+ contents: bytemuck::cast_slice(&cube_vertices()),
+ usage: wgpu::BufferUsages::VERTEX,
+ });
+
+ let vertex_buffer_spheres = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("Sphere Node Vertex Buffer"),
+ size: 1,
+ usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+
+ let grid_verts = grid_vertices(settings.grid_thickness, settings.grid_color);
+ let vertex_count_grid = grid_verts.len() as u32;
+ let vertex_buffer_grid = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+ label: Some("Grid Vertex Buffer"),
+ contents: bytemuck::cast_slice(&grid_verts),
+ usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+ });
+
+ let origin_verts = origin_vectors_vertices(settings.origin_size);
+ let vertex_count_origin = origin_verts.len() as u32;
+ let vertex_buffer_origin = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+ label: Some("Origin Vectors Vertex Buffer"),
+ contents: bytemuck::cast_slice(&origin_verts),
+ usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+ });
+
+ let pivot_verts = camera_pivot_vertices(settings.camera_pivot_size);
+ let vertex_count_pivot = pivot_verts.len() as u32;
+ let vertex_buffer_pivot = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+ label: Some("Camera Pivot Vertex Buffer"),
+ contents: bytemuck::cast_slice(&pivot_verts),
+ usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+ });
+
+ let pipeline_3d = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: Some("3D Pipeline"),
+ layout: Some(&pipeline_layout_3d),
+ vertex: wgpu::VertexState {
+ module: &shader_3d,
+ entry_point: Some("vs_main"),
+ buffers: &[Vertex3D::desc()],
+ compilation_options: Default::default(),
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &shader_3d,
+ entry_point: Some("fs_main"),
+ targets: &[Some(wgpu::ColorTargetState {
+ format: config.format,
+ blend: Some(wgpu::BlendState::ALPHA_BLENDING),
+ write_mask: wgpu::ColorWrites::ALL,
+ })],
+ compilation_options: Default::default(),
+ }),
+ primitive: wgpu::PrimitiveState {
+ topology: wgpu::PrimitiveTopology::TriangleList,
+ strip_index_format: None,
+ front_face: wgpu::FrontFace::Ccw,
+ cull_mode: Some(wgpu::Face::Back),
+ polygon_mode: wgpu::PolygonMode::Fill,
+ unclipped_depth: false,
+ conservative: false,
+ },
+ depth_stencil: Some(wgpu::DepthStencilState {
+ format: wgpu::TextureFormat::Depth32Float,
+ depth_write_enabled: true,
+ depth_compare: wgpu::CompareFunction::Less,
+ stencil: wgpu::StencilState::default(),
+ bias: wgpu::DepthBiasState::default(),
+ }),
+ multisample: wgpu::MultisampleState { count: 1, mask: !0, alpha_to_coverage_enabled: false },
+ multiview: None,
+ cache: None,
+ });
+
+ let cache = Cache::new(device);
+
+ let shader_textured = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("Textured Shader"),
+ source: wgpu::ShaderSource::Wgsl(include_str!("shader_textured.wgsl").into()),
+ });
+
+ let textured_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("Textured Bind Group Layout"),
+ entries: &[
+ wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Texture {
+ multisampled: false,
+ view_dimension: wgpu::TextureViewDimension::D2,
+ sample_type: wgpu::TextureSampleType::Float { filterable: true },
+ },
+ count: None,
+ },
+ wgpu::BindGroupLayoutEntry {
+ binding: 1,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
+ count: None,
+ },
+ ],
+ });
+
+ let textured_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("Textured Pipeline Layout"),
+ bind_group_layouts: &[&textured_bind_group_layout],
+ push_constant_ranges: &[],
+ });
+
+ let curved_text_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: Some("Textured Render Pipeline"),
+ layout: Some(&textured_pipeline_layout),
+ vertex: wgpu::VertexState {
+ module: &shader_textured,
+ entry_point: Some("vs_main"),
+ buffers: &[TexturedVertex::desc()],
+ compilation_options: Default::default(),
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &shader_textured,
+ entry_point: Some("fs_main"),
+ targets: &[Some(wgpu::ColorTargetState {
+ format: config.format,
+ blend: Some(wgpu::BlendState::ALPHA_BLENDING),
+ write_mask: wgpu::ColorWrites::ALL,
+ })],
+ compilation_options: Default::default(),
+ }),
+ primitive: wgpu::PrimitiveState {
+ topology: wgpu::PrimitiveTopology::TriangleList,
+ strip_index_format: None,
+ front_face: wgpu::FrontFace::Ccw,
+ cull_mode: None,
+ unclipped_depth: false,
+ polygon_mode: wgpu::PolygonMode::Fill,
+ conservative: false,
+ },
+ depth_stencil: None,
+ multisample: wgpu::MultisampleState::default(),
+ multiview: None,
+ cache: None,
+ });
+
+ let curved_text_texture = device.create_texture(&wgpu::TextureDescriptor {
+ label: Some("Curved Text Texture"),
+ size: wgpu::Extent3d {
+ width: 1024,
+ height: 1024,
+ depth_or_array_layers: 1,
+ },
+ mip_level_count: 1,
+ sample_count: 1,
+ dimension: wgpu::TextureDimension::D2,
+ format: config.format,
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
+ view_formats: &[],
+ });
+ let curved_text_texture_view = curved_text_texture.create_view(&wgpu::TextureViewDescriptor::default());
+
+ let curved_text_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
+ label: Some("Curved Text Sampler"),
+ address_mode_u: wgpu::AddressMode::ClampToEdge,
+ address_mode_v: wgpu::AddressMode::ClampToEdge,
+ address_mode_w: wgpu::AddressMode::ClampToEdge,
+ mag_filter: wgpu::FilterMode::Linear,
+ min_filter: wgpu::FilterMode::Linear,
+ mipmap_filter: wgpu::FilterMode::Nearest,
+ ..Default::default()
+ });
+
+ let curved_text_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("Curved Text Bind Group"),
+ layout: &textured_bind_group_layout,
+ entries: &[
+ wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::TextureView(&curved_text_texture_view),
+ },
+ wgpu::BindGroupEntry {
+ binding: 1,
+ resource: wgpu::BindingResource::Sampler(&curved_text_sampler),
+ },
+ ],
+ });
+
+ let mut curved_text_atlas = TextAtlas::new(&device, &queue, &cache, config.format);
+ let curved_text_renderer = TextRenderer::new(&mut curved_text_atlas, &device, wgpu::MultisampleState::default(), None);
+ let mut curved_text_viewport = Viewport::new(&device, &cache);
+ curved_text_viewport.update(&queue, Resolution { width: 1024, height: 1024 });
+
+ let textured_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("Textured Vertex Buffer"),
+ size: 1,
+ usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+
+
+ let splitter_layout = clear_ui::layout::SplitterLayout::new(sw, SPLITTER_W, MIN_COLUMN);
+ let (depth_texture, depth_texture_view) = {
+ let tex = device.create_texture(&wgpu::TextureDescriptor {
+ label: Some("Depth Texture"),
+ size: wgpu::Extent3d { width: pw.max(1), height: ph.max(1), depth_or_array_layers: 1 },
+ mip_level_count: 1,
+ sample_count: 1,
+ dimension: wgpu::TextureDimension::D2,
+ format: wgpu::TextureFormat::Depth32Float,
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
+ view_formats: &[],
+ });
+ let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
+ (tex, view)
+ };
+
+ let templates_root = load_fs_tree();
+ let node_templates = flatten_node_templates(&templates_root);
+
+ let default_proj_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("default_project.json");
+ let mut loaded_project = None;
+ if default_proj_path.exists() {
+ if let Ok(content) = fs::read_to_string(&default_proj_path) {
+ if let Ok(proj) = serde_json::from_str::<Project>(&content) {
+ loaded_project = Some(proj);
+ }
+ }
+ }
+
+ let fs_root = if let Some(ref proj) = loaded_project {
+ proj.root.clone()
+ } else {
+ FsNode {
+ id: "root".to_string(),
+ name: "root".to_string(),
+ node_type: "node".to_string(),
+ children: vec![],
+ params: vec![],
+ geometry_visible: true,
+ position: (0.0, 0.0),
+ }
+ };
+
+ let active_camera = if let Some(ref proj) = loaded_project {
+ proj.view_state.active_camera.clone()
+ } else {
+ "Default Camera".to_string()
+ };
+
+ let (pan_x, pan_y) = if let Some(ref proj) = loaded_project {
+ proj.view_state.pan
+ } else {
+ (0.0, 0.0)
+ };
+
+ let current_path = if let Some(ref proj) = loaded_project {
+ proj.view_state.current_path.clone()
+ } else {
+ vec![]
+ };
+ let context_opts = vec![
+ "0: Network".to_string(),
+ "1: Viewport".to_string(),
+ "2: Parameters".to_string(),
+ "3: Spreadsheet".to_string(),
+ "Main Menu".to_string(),
+ ];
+ let mut widgets: Vec<Box<dyn Element>> = vec![
+ Box::new(MenuBar::new(0.0, 0.0, 0.0, HEADER_H).with_title("Clear Design Interface").with_label("Main Menu Bar").with_item("File", &["New Project", "Open", "Save", "Save As", "Exit"]).with_item("Edit", &["Undo", "Redo"]).with_item("View", &["Zoom In", "Zoom Out", "Reset Zoom", "Detach Circular Window", "Show Network Pane", "Show Viewport Pane", "Show Parameters Pane", "Show Spreadsheet Pane"]).with_item("Help", &["About"]).with_z_index(110).with_context_options(context_opts.clone(), 4)),
+ Box::new(Graph::new()),
+ Box::new(Splitter::new(SPLITTER_W)),
+ Box::new(ViewportBg::new()),
+ Box::new(Splitter::new(SPLITTER_W)),
+ Box::new(Plate::new(0.0, 0.0, 0.0, 0.0).with_color(colors::PARAM_BG).with_blur(true)),
+ Box::new(ParametersBg::new()),
+ Box::new(Canvas::new()),
+ Box::new(MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("0: Network").with_label("Network Menu Bar").with_item("File", &["New", "Open", "Save", "Save As"]).with_item("Edit", &["Undo", "Redo"]).with_item("View", &["Zoom In", "Zoom Out", "Circular Pane", "Detach Pane", "Close Pane"]).with_item("Settings", &[]).with_context_options(context_opts.clone(), 0)),
+ Box::new(MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("1: Viewport").with_label("Viewport Menu Bar").with_item("Camera", &["Perspective", "Orthographic"]).with_item("Display", &["Square Aspect"]).with_item("Guides", &["Show Grid", "Cube", "Origin", "Camera Pivot"]).with_item("View", &["Close Pane"]).with_item("Settings", &[]).with_context_options(context_opts.clone(), 1)),
+ Box::new(MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("2: Parameters").with_label("Parameters Menu Bar").with_item("Preset", &["Default", "Custom"]).with_item("Reset", &["All"]).with_item("View", &["Close Pane"]).with_context_options(context_opts.clone(), 2)),
+ Box::new(StatusBar::new().with_text("Ready")),
+ Box::new(Breadcrumb::new()),
+ Box::new(NodePalette::new()),
+ Box::new(Spreadsheet::new()),
+ ];
+
+ let mut spreadsheet_menubar = MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("3: Spreadsheet").with_label("Spreadsheet Menu Bar").with_item("View", &["Close Pane"]).with_context_options(context_opts.clone(), 3);
+ spreadsheet_menubar.set_visible(false);
+ widgets.push(Box::new(spreadsheet_menubar));
+
+ let network_panel = Plate::new(0.0, 0.0, 0.0, 0.0).with_color([0.10, 0.10, 0.13, 0.95]);
+ widgets.push(Box::new(network_panel));
+
+ let paginator = Paginator::new(56.0, vec![])
+ .with_sidebar_mode(true)
+ .with_column_layout(true)
+ .with_tabs_rotated(false)
+ .with_context_options(context_opts.clone(), 0);
+ widgets.push(Box::new(paginator));
+
+ let mut positions = Vec::with_capacity(PAGINATOR_IDX + 1);
+ positions.resize_with(PAGINATOR_IDX + 1, || (0.0, 0.0, 0.0, 0.0));
+
+ let recent_files = Self::load_recent_files();
+ let recent_files_list = ScrollingList::new(22.0, 2.0);
+ let mut recent_files_buttons = Vec::new();
+ for file in &recent_files {
+ let label = file.file_name()
+ .map(|n| n.to_string_lossy().to_string())
+ .unwrap_or_else(|| file.to_string_lossy().to_string());
+ let btn = Button::new_list_row(0.0, 0.0, 0.0, 0.0).with_label(&label);
+ recent_files_buttons.push(btn);
+ }
+
+ let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("Vertex Buffer"),
+ size: 1,
+ usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+
+ let mut shortcut_manager = ShortcutManager::new();
+ shortcut_manager.register("Ctrl+g", Action::ToggleGrid).unwrap();
+ shortcut_manager.register("Ctrl+e", Action::ToggleCube).unwrap();
+ shortcut_manager.register("Ctrl+a", Action::ToggleSquareViewport).unwrap();
+ shortcut_manager.register("Ctrl+,", Action::ToggleConfigure).unwrap();
+ shortcut_manager.register("`", Action::ToggleSpreadsheet).unwrap();
+ shortcut_manager.register("Ctrl+d", Action::ToggleCircularPane).unwrap();
+ shortcut_manager.register("Ctrl+s", Action::Save).unwrap();
+
+ let mut state = Self {
+ window,
+ wl_surface,
+ wgpu_adapter,
+ render_pipeline,
+ vertex_buffer,
+ vertex_count: 0,
+ vertex_data: Vec::with_capacity(4096),
+ pipeline_3d,
+ bind_group_3d,
+ bind_group_layout_3d,
+ uniform_buffer,
+ bind_group_grid,
+ uniform_buffer_grid,
+ bind_group_pivot,
+ uniform_buffer_pivot,
+ vertex_buffer_3d,
+ vertex_count_3d: cube_vertices().len() as u32,
+ vertex_buffer_spheres,
+ vertex_count_spheres: 0,
+ vertex_buffer_grid,
+ vertex_count_grid,
+ depth_texture,
+ depth_texture_view,
+ backdrop_texture,
+ backdrop_texture_view,
+ backdrop_sampler,
+ backdrop_bind_group_layout,
+ backdrop_bind_group,
+ rotation_y: 0.0,
+ rotation_x: 0.0,
+ is_rotating_viewport: false,
+ scroll_lock: 0,
+ last_rotate_time: Instant::now(),
+ rotate_accum_yaw: 0.0,
+ rotate_accum_pitch: 0.0,
+ rotate_velocity_yaw: 0.0,
+ rotate_velocity_pitch: 0.0,
+ show_grid: settings.show_grid_enabled,
+ show_cube: settings.show_cube_enabled,
+ show_origin: settings.show_origin_enabled,
+ show_camera_pivot: settings.show_camera_pivot_enabled,
+ viewport_bg_color: settings.viewport_bg_color,
+ node_color: settings.node_color,
+ grid_color: settings.grid_color,
+ cell_color: settings.cell_color,
+ gap_color: settings.gap_color,
+ vertex_buffer_origin,
+ vertex_count_origin,
+ vertex_buffer_pivot,
+ vertex_count_pivot,
+ origin_size: settings.origin_size,
+ camera_pivot_size: settings.camera_pivot_size,
+ fs_root: fs_root.clone(),
+ node_templates,
+ current_path,
+ last_click: None,
+ last_frame: Instant::now(),
+ shortcut_manager,
+ pending_action: None,
+ exit_requested: false,
+ widgets,
+ positions,
+ splitter_layout,
+ node_palette_visible: false,
+ node_palette_query: String::new(),
+ node_palette_filtered: Vec::new(),
+ node_palette_selected: 0,
+ curved_text_texture,
+ curved_text_texture_view,
+ curved_text_sampler,
+ curved_text_bind_group,
+ curved_text_pipeline,
+ curved_text_atlas,
+ curved_text_renderer,
+ curved_text_viewport,
+ textured_vertex_buffer,
+ textured_vertex_count: 0,
+
+ drag_widget: None,
+ focused_widget: None,
+ cursor_x: 0.0,
+ cursor_y: 0.0,
+ grid_cursor_col: 0,
+ grid_cursor_row: 0,
+ modifiers: ModifiersState::default(),
+ width: lw,
+ height: lh,
+ physical_width: pw,
+ physical_height: ph,
+ scale,
+ square_viewport: settings.square_viewport,
+ grid_snap_enabled: settings.grid_snap_enabled,
+ network_grid_visible: settings.network_grid_enabled,
+ grid_size_x: settings.grid_size_x,
+ grid_size_y: settings.grid_size_y,
+ skipped_row_h: settings.skipped_row_h,
+ skipped_col_w: settings.skipped_col_w,
+
+ pan_x,
+ pan_y,
+ pan_velocity_x: 0.0,
+ pan_velocity_y: 0.0,
+ last_frame_pan_x: pan_x,
+ last_frame_pan_y: pan_y,
+ is_panning: false,
+ pan_start_cx: 0.0,
+ pan_start_cy: 0.0,
+ pan_start_x: 0.0,
+ pan_start_y: 0.0,
+ space_pressed: false,
+ active_camera,
+ show_network: true,
+ show_viewport: true,
+ show_parameters: true,
+ show_spreadsheet: false,
+ is_scrolling_trackpad: false,
+ last_scroll_time: Instant::now(),
+ scroll_accum_x: 0.0,
+ scroll_accum_y: 0.0,
+ last_spreadsheet_node_name: None,
+ last_spreadsheet_node_params: None,
+ viewport_zoom: 1.0,
+ is_zooming_viewport: false,
+ last_zoom_time: Instant::now(),
+ zoom_accum: 0.0,
+ zoom_velocity: 0.0,
+ grid_thickness: settings.grid_thickness,
+ focused_pane: LEFT_MENUBAR_IDX,
+ inertial_scroll_enabled: true,
+ inertial_scroll_friction: 0.90,
+ scroll_speed: 1.0,
+ last_config_read: Instant::now(),
+ circular_network_pane: is_detached_network,
+ circular_network_layout: clear_ui::layout::CircularPaneLayout::new(250.0, 300.0, 180.0),
+ is_detached_network,
+ detached_circular_network: false,
+ last_project_mod_time: {
+ let default_proj_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("default_project.json");
+ std::fs::metadata(&default_proj_path).and_then(|m| m.modified()).ok()
+ },
+ last_project_check: std::time::Instant::now(),
+ last_inspector_check: std::time::Instant::now(),
+ last_inspector_update: std::time::Instant::now(),
+ needs_autosave: false,
+ last_autosave_time: std::time::Instant::now(),
+ window_x: 0,
+ window_y: 0,
+ active_menu_cloud_pid: None,
+ active_menu_cloud_idx: None,
+ uniform_background: settings.uniform_background,
+ network_opacity: settings.network_opacity,
+ last_design_mod_time: {
+ let design_path = DesignSettings::file_path();
+ std::fs::metadata(&design_path).and_then(|m| m.modified()).ok()
+ },
+ last_config_mod_time: {
+ let paths = [
+ "/home/lsgalante/.config/cce/config.toml",
+ "/home/lsgalante/.config/ccec/config.toml",
+ ];
+ let mut mod_time = None;
+ for path in &paths {
+ if let Ok(m) = std::fs::metadata(path) {
+ if let Ok(t) = m.modified() {
+ mod_time = Some(t);
+ break;
+ }
+ }
+ }
+ mod_time
+ },
+ floating_network_layout: (18.0, 44.0, 400.0, 710.0),
+ is_resizing_network: None,
+ drag_start_rect: (0.0, 0.0, 0.0, 0.0),
+ drag_start_mouse: (0.0, 0.0),
+ floating_param_width: 300.0,
+ is_resizing_param: false,
+ drag_start_param_w: 0.0,
+ floating_spreadsheet_height: 250.0,
+ is_resizing_spreadsheet: false,
+ drag_start_spreadsheet_h: 0.0,
+ paginator_page_widgets: Vec::new(),
+ last_paginator_menubar: None,
+ loaded_project_path: None,
+ last_saved_root_json: serde_json::to_string(&fs_root).unwrap_or_default(),
+ recent_files,
+ recent_files_list,
+ recent_files_buttons,
+ text_buffer_cache: std::collections::HashMap::new(),
+ viewport_dirty: true,
+ last_viewport_camera_pos: Vec3::ZERO,
+ last_viewport_camera_rx: 0.0,
+ last_viewport_camera_ry: 0.0,
+ last_viewport_camera_rz: 0.0,
+ last_viewport_pivot: Vec3::ZERO,
+ last_viewport_zoom: 0.0,
+ last_viewport_rotation_x: 0.0,
+ last_viewport_rotation_y: 0.0,
+ last_viewport_bg_color: [0.0; 3],
+ last_viewport_show_grid: false,
+ last_viewport_show_cube: false,
+ last_viewport_show_origin: false,
+ last_viewport_show_camera_pivot: false,
+ last_viewport_width: 0,
+ last_viewport_height: 0,
+ last_viewport_active_camera: String::new(),
+ last_viewport_show_viewport: false,
+ ui_context: clear_ui::context::UiContext::new(),
+ };
+
+ state.update_inertial_settings();
+ state.update_window_title();
+ colors::set_node_color([
+ settings.node_color[0],
+ settings.node_color[1],
+ settings.node_color[2],
+ 1.0,
+ ]);
+ state.ensure_menubar_subnets();
+ state.apply_settings_from_menubar_subnets();
+ state.sync_nodes();
+ state.rebuild_scene_geometry();
+ state.sync_grid_settings();
+ let sg = state.show_grid;
+ let sc = state.show_cube;
+ let so = state.show_origin;
+ let cp = state.show_camera_pivot;
+ let cnp = state.circular_network_pane;
+ let dcn = state.detached_circular_network;
+ let sn = state.show_network;
+ let sv = state.show_viewport;
+ let sp = state.show_parameters;
+ let ss = state.show_spreadsheet;
+
+ state.menu_mut(RIGHT_MENUBAR_IDX).set_item_checked(2, 0, sg);
+ state.menu_mut(RIGHT_MENUBAR_IDX).set_item_checked(2, 1, sc);
+ state.menu_mut(RIGHT_MENUBAR_IDX).set_item_checked(2, 2, so);
+ state.menu_mut(RIGHT_MENUBAR_IDX).set_item_checked(2, 3, cp);
+ state.menu_mut(LEFT_MENUBAR_IDX).set_item_checked(2, 2, cnp);
+ state.menu_mut(LEFT_MENUBAR_IDX).set_item_checked(2, 3, dcn);
+ state.menu_mut(HEADER_IDX).set_item_checked(2, 3, dcn);
+ state.menu_mut(HEADER_IDX).set_item_checked(2, 4, sn);
+ state.menu_mut(HEADER_IDX).set_item_checked(2, 5, sv);
+ state.menu_mut(HEADER_IDX).set_item_checked(2, 6, sp);
+ state.menu_mut(HEADER_IDX).set_item_checked(2, 7, ss);
+
+ state.rebuild_positions();
+ state.apply_layout();
+ state.update_panel_bounds();
+ state.sync_pane_focus();
+ state.upload_vertices();
+ state
+ }
+
+ pub fn sync_grid_settings(&mut self) {
+ let active_node_area_y = self.positions[CONTENT_IDX].1;
+ let active_node_area_x = self.positions[CONTENT_IDX].0;
+
+ let network_grid_visible = self.network_grid_visible;
+ let grid_size_x = self.grid_size_x;
+ let grid_size_y = self.grid_size_y;
+ let skipped_row_h = self.skipped_row_h;
+ let skipped_col_w = self.skipped_col_w;
+ let pan_x = self.pan_x;
+ let pan_y = self.pan_y;
+ let grid_snap_enabled = self.grid_snap_enabled;
+ let graph = self.graph_mut();
+ graph.set_show_network_grid(network_grid_visible);
+ graph.set_grid_sizes(grid_size_x, grid_size_y);
+ graph.set_skipped_sizes(skipped_row_h, skipped_col_w);
+ graph.set_grid_origin(active_node_area_x + pan_x, active_node_area_y + pan_y);
+ graph.set_grid_snap_enabled(grid_snap_enabled);
+ if let Some(graph) = self.widgets[CONTENT_IDX].as_any_mut().downcast_mut::<clear_ui::widget::Graph>() {
+ graph.set_uniform_background(self.uniform_background);
+ graph.set_network_opacity(self.network_opacity);
+ graph.set_cell_color(self.cell_color);
+ graph.set_gap_color(self.gap_color);
+ }
+ if let Some(menubar) = self.widgets[LEFT_MENUBAR_IDX].as_any_mut().downcast_mut::<clear_ui::widget::MenuBar>() {
+ menubar.set_network_opacity(self.network_opacity);
+ }
+ if let Some(breadcrumb) = self.widgets[BREADCRUMB_IDX].as_any_mut().downcast_mut::<clear_ui::widget::Breadcrumb>() {
+ breadcrumb.set_network_opacity(self.network_opacity);
+ }
+ if let Some(plate) = self.widgets[NETWORK_PANEL_IDX].as_any_mut().downcast_mut::<clear_ui::widget::Plate>() {
+ plate.set_network_opacity(self.network_opacity);
+ }
+ }
+
+ pub fn update_inertial_settings(&mut self) {
+ self.last_config_read = Instant::now();
+ let config_path = "/home/lsgalante/.config/cce/config.toml";
+
+ let mut enabled = true;
+ let mut friction = 0.90;
+ let mut speed = 1.0;
+
+ if let Ok(content) = std::fs::read_to_string(config_path) {
+ #[derive(serde::Deserialize)]
+ struct InertialSection {
+ pub inertial_scroll: Option<bool>,
+ pub scroll_friction: Option<u16>,
+ pub scroll_speed: Option<f32>,
+ }
+ #[derive(serde::Deserialize)]
+ struct Config {
+ pub inertial: Option<InertialSection>,
+ }
+ if let Ok(cfg) = toml::from_str::<Config>(&content) {
+ if let Some(inertial) = cfg.inertial {
+ if let Some(val) = inertial.inertial_scroll {
+ enabled = val;
+ }
+ if let Some(friction_val) = inertial.scroll_friction {
+ friction = (friction_val as f32 / 1000.0).clamp(0.1, 0.999);
+ }
+ if let Some(speed_val) = inertial.scroll_speed {
+ speed = speed_val.clamp(0.01, 10.0);
+ }
+ }
+ }
+ }
+
+ self.inertial_scroll_enabled = enabled;
+ self.inertial_scroll_friction = friction;
+ self.scroll_speed = speed;
+ }
+
+
+ pub fn zoom(&mut self, factor: f32, center: Option<(f32, f32)>) {
+ self.pan_velocity_x = 0.0;
+ self.pan_velocity_y = 0.0;
+ self.is_scrolling_trackpad = false;
+ self.scroll_accum_x = 0.0;
+ self.scroll_accum_y = 0.0;
+ let old_gx = self.grid_size_x;
+ let old_gy = self.grid_size_y;
+
+ let new_gx = (old_gx * factor).clamp(30.0, 500.0);
+ let new_gy = (old_gy * factor).clamp(15.0, 250.0);
+
+ if (new_gx - old_gx).abs() < 0.01 {
+ return;
+ }
+
+ let old_row_h = self.skipped_row_h;
+ let old_col_w = self.skipped_col_w;
+
+ let node_area_y = self.positions[CONTENT_IDX].1;
+ let (cx, cy) = match center {
+ Some(pt) => pt,
+ None => {
+ let col = self.grid_cursor_col as f32;
+ let row = self.grid_cursor_row as f32;
+ (
+ col * (old_gx + old_col_w) + 0.5 * old_gx + self.pan_x,
+ row * (old_gy + old_row_h) + 0.5 * old_gy + self.pan_y + node_area_y,
+ )
+ }
+ };
+
+ let col_f = (cx - self.pan_x) / old_gx;
+ let row_f = (cy - self.pan_y - node_area_y) / old_gy;
+
+ self.pan_x = cx - col_f * new_gx;
+ self.pan_y = cy - row_f * new_gy - node_area_y;
+
+ self.grid_size_x = new_gx;
+ self.grid_size_y = new_gy;
+ self.skipped_row_h = old_row_h * (new_gy / old_gy);
+ self.skipped_col_w = old_col_w * (new_gx / old_gx);
+
+ self.sync_grid_settings();
+ }
+
+
+ pub fn keep_cursor_in_view(&mut self) {
+ let (px, py, pw, ph) = self.positions[CONTENT_IDX];
+
+ let cx = px + self.grid_cursor_col as f32 * (self.grid_size_x + self.skipped_col_w) + self.pan_x;
+ let cy = py + self.grid_cursor_row as f32 * (self.grid_size_y + self.skipped_row_h) + self.pan_y;
+ let cw = self.grid_size_x;
+ let ch = self.grid_size_y;
+
+ if cx < px {
+ self.pan_x -= cx - px;
+ } else if cx + cw > px + pw {
+ self.pan_x -= cx + cw - (px + pw);
+ }
+
+ if cy < py {
+ self.pan_y -= cy - py;
+ } else if cy + ch > py + ph {
+ self.pan_y -= cy + ch - (py + ph);
+ }
+ self.sync_grid_settings();
+ }
+
+ pub fn rebuild_positions(&mut self) {
+ self.clamp_splitters();
+ let center_items = self.circular_network_pane;
+ self.menu_mut(LEFT_MENUBAR_IDX).set_center_items(center_items);
+
+ let body_h = self.body_h();
+
+ if self.is_detached_network {
+ let cx = self.width / 2.0;
+ let cy = self.height / 2.0;
+ let r = (self.width.min(self.height) / 2.0 - 10.0).max(50.0);
+
+ self.circular_network_layout.x = cx;
+ self.circular_network_layout.y = cy;
+ self.circular_network_layout.r = r;
+
+ self.positions[0] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[LEFT_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
+ if let Some(menubar) = self.widgets[LEFT_MENUBAR_IDX].as_any_mut().downcast_mut::<clear_ui::widget::MenuBar>() {
+ menubar.set_curved_circle(None);
+ }
+ self.positions[BREADCRUMB_IDX] = (cx - r, cy - r + 45.0, 2.0 * r, BREADCRUMB_H);
+ self.positions[CONTENT_IDX] = (cx - r, cy - r + 45.0 + BREADCRUMB_H, 2.0 * r, 2.0 * r - (45.0 + BREADCRUMB_H));
+ self.positions[NETWORK_PANEL_IDX] = (cx - r, cy - r, 2.0 * r, 2.0 * r);
+ self.widgets[NETWORK_PANEL_IDX].set_rect(cx - r, cy - r, 2.0 * r, 2.0 * r);
+ if let Some(plate) = self.widgets[NETWORK_PANEL_IDX].as_any_mut().downcast_mut::<clear_ui::widget::Plate>() {
+ plate.set_curved_circle(Some((cx, cy, r)));
+ }
+ self.positions[SPLITTER1_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[SPLITTER2_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[PARAM_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[VIEWPORT_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[RIGHT_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[SPREADSHEET_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[SPREADSHEET_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[CANVAS_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[PARAM_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[STATUS_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[NODE_PALETTE_IDX] = (0.0, 0.0, self.width, self.height);
+
+ self.widgets[0].set_visible(false);
+ self.widgets[STATUS_IDX].set_visible(false);
+ self.widgets[CONTENT_IDX].set_visible(true);
+ self.widgets[NETWORK_PANEL_IDX].set_visible(true);
+ self.widgets[LEFT_MENUBAR_IDX].set_visible(false);
+ self.widgets[BREADCRUMB_IDX].set_visible(true);
+ self.widgets[SPLITTER1_IDX].set_visible(false);
+ self.widgets[SPLITTER2_IDX].set_visible(false);
+ self.widgets[VIEWPORT_IDX].set_visible(false);
+ self.widgets[RIGHT_MENUBAR_IDX].set_visible(false);
+ self.widgets[PARAM_IDX].set_visible(false);
+ self.widgets[PARAM_MENUBAR_IDX].set_visible(false);
+ self.widgets[SPREADSHEET_IDX].set_visible(false);
+ self.widgets[SPREADSHEET_MENUBAR_IDX].set_visible(false);
+ } else if self.detached_circular_network {
+ // Parent process: Network pane is detached (hidden from main window)
+ let left_visible = false;
+ let viewport_visible = self.show_viewport;
+ let spreadsheet_visible = self.show_spreadsheet;
+ let center_visible = viewport_visible || spreadsheet_visible;
+ let right_visible = self.show_parameters;
+
+ let (_col_l_x, _col_l_w, _s1_x, _s1_w, col_c_x, col_c_w, s2_x, s2_w, col_r_x, col_r_w) =
+ match (left_visible, center_visible, right_visible) {
+ (false, true, true) => {
+ let c_w = self.splitter_layout.splitter2_x;
+ let s2 = c_w;
+ let r_x = s2 + SPLITTER_W;
+ let r_w = (self.width - r_x).max(0.0);
+ (0.0, 0.0, 0.0, 0.0, 0.0, c_w, s2, SPLITTER_W, r_x, r_w)
+ }
+ (false, true, false) => {
+ (0.0, 0.0, 0.0, 0.0, 0.0, self.width, 0.0, 0.0, 0.0, 0.0)
+ }
+ (false, false, true) => {
+ (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, self.width)
+ }
+ _ => {
+ (0.0, 0.0, 0.0, 0.0, 0.0, self.width, 0.0, 0.0, 0.0, 0.0)
+ }
+ };
+
+ let mut vp_y = 0.0;
+ let mut vp_h = 0.0;
+ let mut sp_menub_y = 0.0;
+ let mut sp_menub_h = 0.0;
+ let mut sp_y = 0.0;
+ let mut sp_h = 0.0;
+
+ if center_visible {
+ if viewport_visible && spreadsheet_visible {
+ let viewport_h = body_h * 2.0 / 3.0;
+ let spreadsheet_h = body_h - viewport_h;
+ vp_y = HEADER_H;
+ vp_h = viewport_h;
+ sp_menub_y = 0.0;
+ sp_menub_h = 0.0;
+ sp_y = HEADER_H + viewport_h;
+ sp_h = spreadsheet_h;
+ } else if viewport_visible {
+ vp_y = HEADER_H;
+ vp_h = body_h;
+ } else if spreadsheet_visible {
+ sp_menub_y = 0.0;
+ sp_menub_h = 0.0;
+ sp_y = HEADER_H;
+ sp_h = body_h;
+ }
+ }
+
+ self.positions[0] = (0.0, 0.0, self.width, HEADER_H);
+ self.positions[LEFT_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[BREADCRUMB_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[CONTENT_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[NETWORK_PANEL_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.widgets[NETWORK_PANEL_IDX].set_rect(0.0, 0.0, 0.0, 0.0);
+ self.positions[SPLITTER1_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[SPLITTER2_IDX] = (s2_x, HEADER_H, s2_w, body_h);
+ self.positions[PARAM_IDX] = (col_r_x, HEADER_H, col_r_w, body_h);
+
+ self.positions[VIEWPORT_IDX] = (col_c_x, vp_y, col_c_w, vp_h);
+ self.positions[RIGHT_MENUBAR_IDX] = (col_c_x, vp_y, col_c_w, if viewport_visible { MENUBAR_H } else { 0.0 });
+
+ self.positions[SPREADSHEET_MENUBAR_IDX] = (col_c_x, sp_menub_y, col_c_w, sp_menub_h);
+ self.positions[SPREADSHEET_IDX] = (col_c_x, sp_y, col_c_w, sp_h);
+
+ self.positions[CANVAS_IDX] = (0.0, HEADER_H, self.width, body_h);
+ self.positions[PARAM_MENUBAR_IDX] = (col_r_x, HEADER_H, col_r_w, if right_visible { MENUBAR_H } else { 0.0 });
+ self.positions[STATUS_IDX] = (0.0, self.height - STATUS_H, self.width, STATUS_H);
+ self.positions[NODE_PALETTE_IDX] = (0.0, 0.0, self.width, self.height);
+
+ self.widgets[0].set_visible(true);
+ self.widgets[STATUS_IDX].set_visible(false);
+ self.widgets[CONTENT_IDX].set_visible(false);
+ self.widgets[NETWORK_PANEL_IDX].set_visible(false);
+ self.widgets[LEFT_MENUBAR_IDX].set_visible(false);
+ self.widgets[BREADCRUMB_IDX].set_visible(false);
+ self.widgets[SPLITTER1_IDX].set_visible(false);
+ self.widgets[SPLITTER2_IDX].set_visible(s2_w > 0.0);
+ self.widgets[VIEWPORT_IDX].set_visible(viewport_visible);
+ self.widgets[RIGHT_MENUBAR_IDX].set_visible(viewport_visible);
+ self.widgets[PARAM_IDX].set_visible(right_visible);
+ self.widgets[PARAM_MENUBAR_IDX].set_visible(right_visible);
+ self.widgets[SPREADSHEET_IDX].set_visible(spreadsheet_visible);
+ self.widgets[SPREADSHEET_MENUBAR_IDX].set_visible(spreadsheet_visible);
+ } else {
+ let paginator_w = 0.0;
+ let body_h = self.height - STATUS_H;
+ self.positions[0] = (0.0, 0.0, 0.0, 0.0);
+ if self.circular_network_pane {
+ let left_visible = false;
+ let viewport_visible = self.show_viewport;
+ let spreadsheet_visible = self.show_spreadsheet;
+ let center_visible = viewport_visible || spreadsheet_visible;
+ let right_visible = self.show_parameters;
+
+ let (_col_l_x, _col_l_w, _s1_x, _s1_w, col_c_x, col_c_w, s2_x, s2_w, col_r_x, col_r_w) =
+ match (left_visible, center_visible, right_visible) {
+ (false, true, true) => {
+ let s2 = self.splitter_layout.splitter2_x.max(paginator_w + MIN_COLUMN);
+ let viewport_w = s2 - paginator_w;
+ let r_x = s2 + SPLITTER_W;
+ let r_w = (self.width - r_x).max(0.0);
+ (0.0, 0.0, 0.0, 0.0, paginator_w, viewport_w, s2, SPLITTER_W, r_x, r_w)
+ }
+ (false, true, false) => {
+ (0.0, 0.0, 0.0, 0.0, paginator_w, self.width - paginator_w, 0.0, 0.0, 0.0, 0.0)
+ }
+ (false, false, true) => {
+ (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, paginator_w, self.width - paginator_w)
+ }
+ _ => {
+ (0.0, 0.0, 0.0, 0.0, paginator_w, self.width - paginator_w, 0.0, 0.0, 0.0, 0.0)
+ }
+ };
+
+ let mut vp_y = 0.0;
+ let mut vp_h = 0.0;
+ let mut sp_menub_y = 0.0;
+ let mut sp_menub_h = 0.0;
+ let mut sp_y = 0.0;
+ let mut sp_h = 0.0;
+
+ if center_visible {
+ if viewport_visible && spreadsheet_visible {
+ let viewport_h = body_h * 2.0 / 3.0;
+ let spreadsheet_h = body_h - viewport_h;
+ vp_y = 0.0;
+ vp_h = viewport_h;
+ sp_menub_y = 0.0;
+ sp_menub_h = 0.0;
+ sp_y = viewport_h;
+ sp_h = spreadsheet_h;
+ } else if viewport_visible {
+ vp_y = 0.0;
+ vp_h = body_h;
+ } else if spreadsheet_visible {
+ sp_menub_y = 0.0;
+ sp_menub_h = 0.0;
+ sp_y = 0.0;
+ sp_h = body_h;
+ }
+ }
+
+ if self.widgets[NETWORK_PANEL_IDX].is_dragging() {
+ let (px, py, _pw, _ph) = self.widgets[NETWORK_PANEL_IDX].rect();
+ let r = self.circular_network_layout.r;
+ self.circular_network_layout.x = px + r;
+ self.circular_network_layout.y = py + r;
+ }
+
+ let gap = 18.0;
+ let max_r = ((self.width - paginator_w - 2.0 * gap).min(self.height - STATUS_H - 2.0 * gap) / 2.0).max(50.0);
+ self.circular_network_layout.r = self.circular_network_layout.r.clamp(50.0, max_r);
+
+ let r = self.circular_network_layout.r;
+ let min_x = paginator_w + gap + r;
+ let max_x = (self.width - gap - r).max(min_x);
+ self.circular_network_layout.x = self.circular_network_layout.x.clamp(min_x, max_x);
+
+ let min_y = gap + r;
+ let max_y = (self.height - STATUS_H - gap - r).max(min_y);
+ self.circular_network_layout.y = self.circular_network_layout.y.clamp(min_y, max_y);
+
+ let cx = self.circular_network_layout.x;
+ let cy = self.circular_network_layout.y;
+
+ self.positions[LEFT_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
+ if let Some(menubar) = self.widgets[LEFT_MENUBAR_IDX].as_any_mut().downcast_mut::<clear_ui::widget::MenuBar>() {
+ menubar.set_curved_circle(None);
+ }
+ self.positions[BREADCRUMB_IDX] = (cx - r, cy - r + 45.0, 2.0 * r, BREADCRUMB_H);
+ self.positions[CONTENT_IDX] = (cx - r, cy - r + 45.0 + BREADCRUMB_H, 2.0 * r, 2.0 * r - (45.0 + BREADCRUMB_H));
+ self.positions[NETWORK_PANEL_IDX] = (cx - r, cy - r, 2.0 * r, 2.0 * r);
+ self.widgets[NETWORK_PANEL_IDX].set_rect(cx - r, cy - r, 2.0 * r, 2.0 * r);
+ if let Some(plate) = self.widgets[NETWORK_PANEL_IDX].as_any_mut().downcast_mut::<clear_ui::widget::Plate>() {
+ plate.set_curved_circle(Some((cx, cy, r)));
+ }
+ self.widgets[NETWORK_PANEL_IDX].set_drag_bounds(0.0, 0.0, self.width, self.height);
+ self.positions[SPLITTER1_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[SPLITTER2_IDX] = (s2_x, 0.0, s2_w, body_h);
+ self.positions[PARAM_IDX] = (col_r_x, 0.0, col_r_w, body_h);
+
+ self.positions[VIEWPORT_IDX] = (col_c_x, vp_y, col_c_w, vp_h);
+ self.positions[RIGHT_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
+
+ self.positions[SPREADSHEET_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[SPREADSHEET_IDX] = (col_c_x, sp_y, col_c_w, sp_h);
+
+ self.positions[CANVAS_IDX] = (0.0, 0.0, self.width, body_h);
+ self.positions[PARAM_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[STATUS_IDX] = (0.0, self.height - STATUS_H, self.width, STATUS_H);
+ self.positions[NODE_PALETTE_IDX] = (0.0, 0.0, self.width, self.height);
+
+ self.positions[PAGINATOR_IDX] = (0.0, 0.0, paginator_w, self.height - STATUS_H);
+ self.widgets[PAGINATOR_IDX].set_rect(0.0, 0.0, paginator_w, self.height - STATUS_H);
+
+ self.widgets[0].set_visible(false);
+ self.widgets[STATUS_IDX].set_visible(false);
+ self.widgets[CONTENT_IDX].set_visible(self.show_network);
+ self.widgets[NETWORK_PANEL_IDX].set_visible(self.show_network);
+ self.widgets[LEFT_MENUBAR_IDX].set_visible(false);
+ self.widgets[BREADCRUMB_IDX].set_visible(self.show_network);
+ self.widgets[SPLITTER1_IDX].set_visible(false);
+ self.widgets[SPLITTER2_IDX].set_visible(s2_w > 0.0);
+ self.widgets[VIEWPORT_IDX].set_visible(viewport_visible);
+ self.widgets[RIGHT_MENUBAR_IDX].set_visible(false);
+ self.widgets[PARAM_IDX].set_visible(right_visible);
+ self.widgets[PARAM_MENUBAR_IDX].set_visible(false);
+ self.widgets[SPREADSHEET_IDX].set_visible(spreadsheet_visible);
+ self.widgets[SPREADSHEET_MENUBAR_IDX].set_visible(false);
+ self.widgets[PAGINATOR_IDX].set_visible(false);
+ } else {
+ if !self.circular_network_pane && self.widgets[NETWORK_PANEL_IDX].is_dragging() {
+ let (px, py, _pw, _ph) = self.widgets[NETWORK_PANEL_IDX].rect();
+ self.floating_network_layout.0 = px;
+ self.floating_network_layout.1 = py;
+ }
+
+ let gap = 18.0_f32;
+ let (mut _fx, mut _fy, mut fw, mut _fh) = self.floating_network_layout;
+ fw = fw.clamp(150.0, (self.width - paginator_w - 2.0 * gap).max(150.0));
+ let fx = paginator_w + gap;
+ let fy = gap;
+ let fh = (self.height - STATUS_H - 2.0 * gap).max(100.0);
+ self.floating_network_layout = (fx, fy, fw, fh);
+
+ let param_w = self.floating_param_width.clamp(150.0, (self.width - paginator_w - 2.0 * gap).max(150.0));
+ self.floating_param_width = param_w;
+ let param_x = self.width - gap - param_w;
+ let param_y = gap;
+ let param_h = (self.height - STATUS_H - 2.0 * gap).max(100.0);
+
+ let ss_x = if self.show_network { fx + fw + gap } else { paginator_w + gap };
+ let ss_w_end = if self.show_parameters { param_x - gap } else { self.width - gap };
+ let ss_w = (ss_w_end - ss_x).max(150.0);
+ let ss_y_end = self.height - STATUS_H - gap;
+ let ss_h = self.floating_spreadsheet_height.clamp(100.0, (ss_y_end - gap).max(100.0));
+ let ss_y = ss_y_end - ss_h;
+ self.floating_spreadsheet_height = ss_h;
+
+ let viewport_visible = self.show_viewport;
+ let spreadsheet_visible = self.show_spreadsheet;
+ let right_visible = self.show_parameters;
+
+ let col_c_x = paginator_w;
+ let col_c_w = self.width - paginator_w;
+ let vp_y = 0.0;
+ let vp_h = if viewport_visible { body_h } else { 0.0 };
+
+ let (px, py, pw, ph) = if self.show_network {
+ (fx, fy, fw, fh)
+ } else {
+ (0.0, 0.0, 0.0, 0.0)
+ };
+
+ if let Some(menubar) = self.widgets[LEFT_MENUBAR_IDX].as_any_mut().downcast_mut::<clear_ui::widget::MenuBar>() {
+ menubar.set_curved_circle(None);
+ }
+
+ let mb_h = 0.0;
+ let bc_h = if self.show_network { BREADCRUMB_H } else { 0.0 };
+ let content_h = (ph - mb_h - bc_h).max(0.0);
+
+ self.positions[CONTENT_IDX] = (px, py + mb_h + bc_h, pw, content_h);
+ self.positions[NETWORK_PANEL_IDX] = (px, py, pw, ph);
+ self.widgets[NETWORK_PANEL_IDX].set_rect(px, py, pw, ph);
+ if let Some(plate) = self.widgets[NETWORK_PANEL_IDX].as_any_mut().downcast_mut::<clear_ui::widget::Plate>() {
+ plate.set_curved_circle(None);
+ }
+ self.positions[SPLITTER1_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[SPLITTER2_IDX] = (0.0, 0.0, 0.0, 0.0);
+ let p_rect = if self.show_parameters { (param_x, param_y, param_w, param_h) } else { (0.0, 0.0, 0.0, 0.0) };
+ self.positions[PARAM_IDX] = p_rect;
+ self.widgets[PARAM_IDX].set_rect(p_rect.0, p_rect.1, p_rect.2, p_rect.3);
+
+ self.positions[VIEWPORT_IDX] = (col_c_x, vp_y, col_c_w, vp_h);
+ self.positions[RIGHT_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[BREADCRUMB_IDX] = (px, py + mb_h, pw, bc_h);
+ self.positions[LEFT_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
+
+ self.positions[SPREADSHEET_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[SPREADSHEET_IDX] = if spreadsheet_visible { (ss_x, ss_y, ss_w, ss_h) } else { (0.0, 0.0, 0.0, 0.0) };
+ self.widgets[SPREADSHEET_IDX].set_rect(
+ self.positions[SPREADSHEET_IDX].0,
+ self.positions[SPREADSHEET_IDX].1,
+ self.positions[SPREADSHEET_IDX].2,
+ self.positions[SPREADSHEET_IDX].3,
+ );
+
+ self.positions[CANVAS_IDX] = (0.0, 0.0, self.width, body_h);
+ self.positions[PARAM_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[STATUS_IDX] = (0.0, self.height - STATUS_H, self.width, STATUS_H);
+ self.positions[NODE_PALETTE_IDX] = (0.0, 0.0, self.width, self.height);
+
+ self.positions[PAGINATOR_IDX] = (0.0, 0.0, paginator_w, self.height - STATUS_H);
+ self.widgets[PAGINATOR_IDX].set_rect(0.0, 0.0, paginator_w, self.height - STATUS_H);
+
+ self.widgets[0].set_visible(false);
+ self.widgets[STATUS_IDX].set_visible(false);
+ self.widgets[CONTENT_IDX].set_visible(self.show_network);
+ self.widgets[NETWORK_PANEL_IDX].set_visible(self.show_network);
+ self.widgets[LEFT_MENUBAR_IDX].set_visible(false);
+ self.widgets[BREADCRUMB_IDX].set_visible(self.show_network);
+ self.widgets[SPLITTER1_IDX].set_visible(false);
+ self.widgets[SPLITTER2_IDX].set_visible(false);
+ self.widgets[VIEWPORT_IDX].set_visible(viewport_visible);
+ self.widgets[RIGHT_MENUBAR_IDX].set_visible(false);
+ self.widgets[PARAM_IDX].set_visible(right_visible);
+ self.widgets[PARAM_MENUBAR_IDX].set_visible(false);
+ self.widgets[SPREADSHEET_IDX].set_visible(spreadsheet_visible);
+ self.widgets[SPREADSHEET_MENUBAR_IDX].set_visible(false);
+ self.widgets[PAGINATOR_IDX].set_visible(false);
+ }
+ }
+
+ if !self.is_detached_network {
+ let mut active_menubar = self.focused_pane;
+ if active_menubar == LEFT_MENUBAR_IDX && !self.show_network {
+ active_menubar = HEADER_IDX;
+ }
+ if active_menubar == RIGHT_MENUBAR_IDX && !self.show_viewport {
+ active_menubar = HEADER_IDX;
+ }
+ if active_menubar == PARAM_MENUBAR_IDX && !self.show_parameters {
+ active_menubar = HEADER_IDX;
+ }
+ if active_menubar == SPREADSHEET_MENUBAR_IDX && !self.show_spreadsheet {
+ active_menubar = HEADER_IDX;
+ }
+
+ if active_menubar != self.focused_pane {
+ self.focused_pane = active_menubar;
+ }
+
+ let menubars = [
+ HEADER_IDX,
+ LEFT_MENUBAR_IDX,
+ RIGHT_MENUBAR_IDX,
+ PARAM_MENUBAR_IDX,
+ SPREADSHEET_MENUBAR_IDX,
+ ];
+ for &idx in &menubars {
+ self.positions[idx] = (0.0, 0.0, 0.0, 0.0);
+ self.widgets[idx].set_visible(false);
+ }
+ }
+ self.positions[PARAM_PLATE_IDX] = self.positions[PARAM_IDX];
+ let p_visible = self.widgets[PARAM_IDX].visible();
+ self.widgets[PARAM_PLATE_IDX].set_visible(p_visible);
+ }
+
+
+ pub fn apply_layout(&mut self) {
+ for (i, pos) in self.positions.iter().enumerate() {
+ if let Some(widget) = self.widgets.get_mut(i) {
+ if widget.is_dragging() { continue; }
+ let (x, y, w, h) = *pos;
+ widget.set_rect(x, y, w, h);
+ }
+ }
+ self.update_recent_files_layout();
+ }
+
+ pub fn update_panel_bounds(&mut self) {
+ // Unbounded 2D canvas - no clamping
+ }
+
+ pub fn update_paginator(&mut self) {
+ let active_menubar = self.focused_pane;
+ let title = self.widgets[active_menubar].label().unwrap_or_default();
+ let mut label_name = if let Some(colon_idx) = title.find(':') {
+ title.split_at(colon_idx + 1).1.trim().to_uppercase()
+ } else {
+ title.to_uppercase()
+ };
+ if label_name.ends_with(" MENU BAR") {
+ label_name = label_name.replace(" MENU BAR", "");
+ }
+ self.page_selector_mut(PAGINATOR_IDX).set_sidebar_label(Some(label_name));
+
+ let menu_names = self.menu(active_menubar).menu_names();
+ let menu_items = self.menu(active_menubar).menu_items_list();
+ let menu_checked = self.menu(active_menubar).menu_checked_list();
+
+ let num_pages = menu_names.len();
+ self.page_selector_mut(PAGINATOR_IDX).set_pages_with_items(menu_names.clone(), menu_items.clone());
+
+ // Check if we need to rebuild paginator page widgets.
+ let need_rebuild = self.last_paginator_menubar != Some(active_menubar)
+ || self.paginator_page_widgets.len() != num_pages;
+
+ if need_rebuild {
+ self.last_paginator_menubar = Some(active_menubar);
+ self.paginator_page_widgets.clear();
+ self.paginator_page_widgets.resize_with(num_pages, Vec::new);
+
+ for (page_idx, page_name) in menu_names.iter().enumerate() {
+ if page_name == "Settings" {
+ if active_menubar == LEFT_MENUBAR_IDX {
+ // 0: Snap to Grid (Checkbox)
+ let mut cb = Checkbox::new().with_label("Snap to Grid");
+ cb.set_checked(self.grid_snap_enabled);
+ cb.set_rect(0.0, 0.0, 0.0, 42.0);
+ self.paginator_page_widgets[page_idx].push(Box::new(cb));
+
+ // 1: Grid Visible (Checkbox)
+ let mut cb = Checkbox::new().with_label("Grid Visible");
+ cb.set_checked(self.network_grid_visible);
+ cb.set_rect(0.0, 0.0, 0.0, 42.0);
+ self.paginator_page_widgets[page_idx].push(Box::new(cb));
+
+ // 2: Grid X (Spinbox, range 10-200)
+ let mut sb = Spinbox::new(self.grid_size_x as i32, 10, 200, 1).with_label("Grid X");
+ sb.set_rect(0.0, 0.0, 0.0, 42.0);
+ self.paginator_page_widgets[page_idx].push(Box::new(sb));
+
+ // 3: Grid Y (Spinbox, range 5-100)
+ let mut sb = Spinbox::new(self.grid_size_y as i32, 5, 100, 1).with_label("Grid Y");
+ sb.set_rect(0.0, 0.0, 0.0, 42.0);
+ self.paginator_page_widgets[page_idx].push(Box::new(sb));
+
+ // 4: Skipped Row H (Spinbox, range 0-150)
+ let mut sb = Spinbox::new(self.skipped_row_h as i32, 0, 150, 1).with_label("Skipped Row H");
+ sb.set_rect(0.0, 0.0, 0.0, 42.0);
+ self.paginator_page_widgets[page_idx].push(Box::new(sb));
+
+ // 5: Skipped Col W (Spinbox, range 0-150)
+ let mut sb = Spinbox::new(self.skipped_col_w as i32, 0, 150, 1).with_label("Skipped Col W");
+ sb.set_rect(0.0, 0.0, 0.0, 42.0);
+ self.paginator_page_widgets[page_idx].push(Box::new(sb));
+
+ // 6: Uniform Background (Checkbox)
+ let mut cb = Checkbox::new().with_label("Uniform Background");
+ cb.set_checked(self.uniform_background);
+ cb.set_rect(0.0, 0.0, 0.0, 42.0);
+ self.paginator_page_widgets[page_idx].push(Box::new(cb));
+
+ // 7: Opacity (Slider, range 0.0-1.0)
+ let mut sl = Slider::new().with_range(0.0, 1.0).with_value(self.network_opacity).with_label("Opacity").with_readout(true).with_scroll(true);
+ sl.set_rect(0.0, 0.0, 0.0, 42.0);
+ self.paginator_page_widgets[page_idx].push(Box::new(sl));
+
+ // 8: Node Color (ColorSelector)
+ let mut cs = ColorSelector::new([
+ (self.node_color[0] * 255.0) as u8,
+ (self.node_color[1] * 255.0) as u8,
+ (self.node_color[2] * 255.0) as u8,
+ ]).with_label("Node Color");
+ cs.set_rect(0.0, 0.0, 0.0, 42.0);
+ self.paginator_page_widgets[page_idx].push(Box::new(cs));
+
+ // 9: Cell Color (ColorSelector)
+ let mut cs = ColorSelector::new([
+ (self.cell_color[0] * 255.0) as u8,
+ (self.cell_color[1] * 255.0) as u8,
+ (self.cell_color[2] * 255.0) as u8,
+ ]).with_label("Cell Color");
+ cs.set_rect(0.0, 0.0, 0.0, 42.0);
+ self.paginator_page_widgets[page_idx].push(Box::new(cs));
+
+ // 10: Gap Color (ColorSelector)
+ let mut cs = ColorSelector::new([
+ (self.gap_color[0] * 255.0) as u8,
+ (self.gap_color[1] * 255.0) as u8,
+ (self.gap_color[2] * 255.0) as u8,
+ ]).with_label("Gap Color");
+ cs.set_rect(0.0, 0.0, 0.0, 42.0);
+ self.paginator_page_widgets[page_idx].push(Box::new(cs));
+ } else if active_menubar == RIGHT_MENUBAR_IDX {
+ // 0: Show Grid Guide (Checkbox)
+ let mut cb = Checkbox::new().with_label("Show Grid Guide");
+ cb.set_checked(self.show_grid);
+ cb.set_rect(0.0, 0.0, 0.0, 42.0);
+ self.paginator_page_widgets[page_idx].push(Box::new(cb));
+
+ // 1: Show Reference Cube (Checkbox)
+ let mut cb = Checkbox::new().with_label("Show Reference Cube");
+ cb.set_checked(self.show_cube);
+ cb.set_rect(0.0, 0.0, 0.0, 42.0);
+ self.paginator_page_widgets[page_idx].push(Box::new(cb));
+
+ // 2: Show Origin Axes (Checkbox)
+ let mut cb = Checkbox::new().with_label("Show Origin Axes");
+ cb.set_checked(self.show_origin);
+ cb.set_rect(0.0, 0.0, 0.0, 42.0);
+ self.paginator_page_widgets[page_idx].push(Box::new(cb));
+
+ // 3: Show Camera Pivot (Checkbox)
+ let mut cb = Checkbox::new().with_label("Show Camera Pivot");
+ cb.set_checked(self.show_camera_pivot);
+ cb.set_rect(0.0, 0.0, 0.0, 42.0);
+ self.paginator_page_widgets[page_idx].push(Box::new(cb));
+
+ // 4: Grid Thickness (Spinbox, range 2-200, decimals 3)
+ let mut sb = Spinbox::new((self.grid_thickness * 1000.0) as i32, 2, 200, 1).with_label("Grid Thickness").with_decimals(3);
+ sb.set_rect(0.0, 0.0, 0.0, 42.0);
+ self.paginator_page_widgets[page_idx].push(Box::new(sb));
+
+ // 5: Origin Guide Size (Spinbox, range 1-50, decimals 1)
+ let mut sb = Spinbox::new((self.origin_size * 10.0) as i32, 1, 50, 1).with_label("Origin Guide Size").with_decimals(1);
+ sb.set_rect(0.0, 0.0, 0.0, 42.0);
+ self.paginator_page_widgets[page_idx].push(Box::new(sb));
+
+ // 6: Camera Pivot Size (Spinbox, range 1-50, decimals 1)
+ let mut sb = Spinbox::new((self.camera_pivot_size * 10.0) as i32, 1, 50, 1).with_label("Camera Pivot Size").with_decimals(1);
+ sb.set_rect(0.0, 0.0, 0.0, 42.0);
+ self.paginator_page_widgets[page_idx].push(Box::new(sb));
+
+ // 7, 8, 9: BG Color R, G, B (Spinbox, range 0-255)
+ let mut sb = Spinbox::new((self.viewport_bg_color[0] * 255.0) as i32, 0, 255, 1).with_label("BG Color R");
+ sb.set_rect(0.0, 0.0, 0.0, 42.0);
+ self.paginator_page_widgets[page_idx].push(Box::new(sb));
+ let mut sb = Spinbox::new((self.viewport_bg_color[1] * 255.0) as i32, 0, 255, 1).with_label("BG Color G");
+ sb.set_rect(0.0, 0.0, 0.0, 42.0);
+ self.paginator_page_widgets[page_idx].push(Box::new(sb));
+ let mut sb = Spinbox::new((self.viewport_bg_color[2] * 255.0) as i32, 0, 255, 1).with_label("BG Color B");
+ sb.set_rect(0.0, 0.0, 0.0, 42.0);
+ self.paginator_page_widgets[page_idx].push(Box::new(sb));
+
+ // 10, 11, 12: Grid Color R, G, B (Spinbox, range 0-255)
+ let mut sb = Spinbox::new((self.grid_color[0] * 255.0) as i32, 0, 255, 1).with_label("Grid Color R");
+ sb.set_rect(0.0, 0.0, 0.0, 42.0);
+ self.paginator_page_widgets[page_idx].push(Box::new(sb));
+ let mut sb = Spinbox::new((self.grid_color[1] * 255.0) as i32, 0, 255, 1).with_label("Grid Color G");
+ sb.set_rect(0.0, 0.0, 0.0, 42.0);
+ self.paginator_page_widgets[page_idx].push(Box::new(sb));
+ let mut sb = Spinbox::new((self.grid_color[2] * 255.0) as i32, 0, 255, 1).with_label("Grid Color B");
+ sb.set_rect(0.0, 0.0, 0.0, 42.0);
+ self.paginator_page_widgets[page_idx].push(Box::new(sb));
+ }
+ } else {
+ let items = &menu_items[page_idx];
+ let checked_list = menu_checked.get(page_idx);
+ for (item_idx, item_name) in items.iter().enumerate() {
+ let is_checked = checked_list.and_then(|l| l.get(item_idx).copied().flatten());
+ if let Some(checked_val) = is_checked {
+ let mut cb = Checkbox::new().with_label(item_name);
+ cb.set_checked(checked_val);
+ self.paginator_page_widgets[page_idx].push(Box::new(cb));
+ } else {
+ let btn = Button::new(0.0, 0.0, 150.0, 24.0).with_label(item_name);
+ self.paginator_page_widgets[page_idx].push(Box::new(btn));
+ }
+ }
+
+ if menu_names[page_idx] == "File" {
+ let mut recent_lbl = Label::new("Recent Files").with_font_size(11.0).with_color([0xd4, 0xd4, 0xd4]);
+ recent_lbl.set_rect(0.0, 0.0, 150.0, 16.0);
+ self.paginator_page_widgets[page_idx].push(Box::new(recent_lbl));
+
+ let mut recent_list = ScrollingList::new(22.0, 2.0);
+ recent_list.set_rect(0.0, 0.0, 150.0, 100.0);
+ self.paginator_page_widgets[page_idx].push(Box::new(recent_list));
+ }
+ }
+ }
+
+ // Page widgets rebuild completed.
+ }
+
+ let sel_page = self.page_selector(PAGINATOR_IDX).selected_page();
+ self.widgets[PARAM_IDX].clear_children(&mut self.ui_context);
+ if !self.page_selector(PAGINATOR_IDX).is_page_hidden() {
+ if sel_page < self.paginator_page_widgets.len() {
+ for widget in &self.paginator_page_widgets[sel_page] {
+ let ptr = &**widget as *const (dyn Element + 'static) as *mut (dyn Element + 'static);
+ self.widgets[PARAM_IDX].add_child(ptr, &mut self.ui_context);
+ }
+ if menu_names.get(sel_page).map(|s| s.as_str()) == Some("File") {
+ for btn in &mut self.recent_files_buttons {
+ let ptr = btn as *mut Button as *mut (dyn Element + 'static);
+ self.widgets[PARAM_IDX].add_child(ptr, &mut self.ui_context);
+ }
+ }
+ }
+ }
+
+ let (px, py, pw, ph) = self.positions[PAGINATOR_IDX];
+ self.widgets[PAGINATOR_IDX].set_rect(px, py, pw, ph);
+
+ // Layout the parameters plate with the new children immediately.
+ let (ppx, ppy, ppw, pph) = self.positions[PARAM_IDX];
+ self.widgets[PARAM_IDX].set_rect(ppx, ppy, ppw, pph);
+
+ self.update_recent_files_layout();
+ }
+
+ pub fn sync_context_dropdowns(&mut self) {
+ let selected_idx = match self.focused_pane {
+ LEFT_MENUBAR_IDX => 0,
+ RIGHT_MENUBAR_IDX => 1,
+ PARAM_MENUBAR_IDX => 2,
+ SPREADSHEET_MENUBAR_IDX => 3,
+ HEADER_IDX => 4,
+ _ => return,
+ };
+ for &widget_idx in &[HEADER_IDX, LEFT_MENUBAR_IDX, RIGHT_MENUBAR_IDX, PARAM_MENUBAR_IDX, SPREADSHEET_MENUBAR_IDX, PAGINATOR_IDX] {
+ self.menu_mut(widget_idx).set_context_selected(selected_idx);
+ }
+ }
+
+ pub fn sync_pane_focus(&mut self) {
+ for &menubar_idx in &[HEADER_IDX, LEFT_MENUBAR_IDX, RIGHT_MENUBAR_IDX, PARAM_MENUBAR_IDX, SPREADSHEET_MENUBAR_IDX] {
+ self.widgets[menubar_idx].set_selected(menubar_idx == self.focused_pane);
+ }
+ if self.focused_pane != PARAM_MENUBAR_IDX {
+ self.widgets[PARAM_IDX].unfocus();
+ self.sync_parameters_to_project();
+ }
+ self.sync_context_dropdowns();
+ self.update_paginator();
+ }
+
+ pub fn sync_layout(&mut self) {
+ let left_visible = self.show_network && !self.circular_network_pane && !self.is_detached_network && !self.detached_circular_network;
+ let center_visible = self.show_viewport || self.show_spreadsheet;
+ let right_visible = self.show_parameters;
+
+ if left_visible && center_visible && self.widgets[SPLITTER1_IDX].visible() && self.widgets[SPLITTER1_IDX].rect().2 > 0.0 {
+ self.splitter_layout.splitter1_x = self.widgets[SPLITTER1_IDX].rect().0;
+ }
+ if right_visible && (center_visible || left_visible) && self.widgets[SPLITTER2_IDX].visible() && self.widgets[SPLITTER2_IDX].rect().2 > 0.0 {
+ self.splitter_layout.splitter2_x = self.widgets[SPLITTER2_IDX].rect().0;
+ }
+ self.rebuild_positions();
+ self.apply_layout();
+ self.update_panel_bounds();
+ self.sync_pane_focus();
+ }
+
+ pub fn execute_action(&mut self, action: Action) {
+ let mut settings_changed = false;
+ match action {
+ Action::ToggleGrid => {
+ self.show_grid = !self.show_grid;
+ let val = self.show_grid;
+ self.menu_mut(RIGHT_MENUBAR_IDX).set_item_checked(2, 0, val);
+ settings_changed = true;
+ }
+ Action::ToggleCube => {
+ self.show_cube = !self.show_cube;
+ let val = self.show_cube;
+ self.menu_mut(RIGHT_MENUBAR_IDX).set_item_checked(2, 1, val);
+ settings_changed = true;
+ }
+ Action::ToggleOrigin => {
+ self.show_origin = !self.show_origin;
+ let val = self.show_origin;
+ self.menu_mut(RIGHT_MENUBAR_IDX).set_item_checked(2, 2, val);
+ settings_changed = true;
+ }
+ Action::ToggleCameraPivot => {
+ self.show_camera_pivot = !self.show_camera_pivot;
+ let val = self.show_camera_pivot;
+ self.menu_mut(RIGHT_MENUBAR_IDX).set_item_checked(2, 3, val);
+ settings_changed = true;
+ }
+ Action::ToggleSquareViewport => {
+ self.square_viewport = !self.square_viewport;
+ settings_changed = true;
+ }
+ Action::ToggleConfigure => {
+ let target_pane = if self.show_network {
+ LEFT_MENUBAR_IDX
+ } else if self.show_viewport {
+ RIGHT_MENUBAR_IDX
+ } else {
+ LEFT_MENUBAR_IDX
+ };
+ self.focused_pane = target_pane;
+ self.page_selector_mut(PAGINATOR_IDX).set_page_hidden(false);
+ let page_idx = if target_pane == LEFT_MENUBAR_IDX { 3 } else { 4 };
+ self.page_selector_mut(PAGINATOR_IDX).set_selected_page(page_idx);
+ self.sync_pane_focus();
+ self.rebuild_positions();
+ self.apply_layout();
+ self.upload_vertices();
+ return;
+ }
+ Action::ToggleSpreadsheet => {
+ self.show_spreadsheet = !self.show_spreadsheet;
+ self.widgets[SPREADSHEET_IDX].set_visible(self.show_spreadsheet);
+ self.widgets[SPREADSHEET_MENUBAR_IDX].set_visible(self.show_spreadsheet);
+ let val = self.show_spreadsheet;
+ self.menu_mut(HEADER_IDX).set_item_checked(2, 7, val);
+ if !self.show_spreadsheet && self.focused_pane == SPREADSHEET_MENUBAR_IDX {
+ self.focused_pane = get_next_visible_pane(
+ self.focused_pane,
+ self.show_network,
+ self.show_viewport,
+ self.show_parameters,
+ self.show_spreadsheet,
+ false,
+ );
+ }
+ self.rebuild_positions();
+ self.apply_layout();
+ self.sync_pane_focus();
+ self.sync_nodes();
+ }
+ Action::ToggleCircularPane => {
+ self.circular_network_pane = !self.circular_network_pane;
+ let val = self.circular_network_pane;
+ self.menu_mut(LEFT_MENUBAR_IDX).set_item_checked(2, 2, val);
+ self.rebuild_positions();
+ self.apply_layout();
+ self.sync_grid_settings();
+ }
+ Action::DetachCircularWindow => {
+ let default_proj_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("default_project.json");
+ if let Err(e) = self.save_to_file(&default_proj_path) {
+ eprintln!("Failed to save default project before detaching: {:?}", e);
+ }
+
+ self.detached_circular_network = !self.detached_circular_network;
+ let val = self.detached_circular_network;
+ self.menu_mut(LEFT_MENUBAR_IDX).set_item_checked(2, 3, val);
+ self.menu_mut(HEADER_IDX).set_item_checked(2, 3, val);
+
+ if self.detached_circular_network {
+ let _ = std::process::Command::new(std::env::current_exe().unwrap())
+ .arg("--detached-network")
+ .spawn();
+ }
+
+ self.rebuild_positions();
+ self.apply_layout();
+ self.sync_grid_settings();
+ }
+ Action::Save => {
+ let path_opt = self.loaded_project_path.clone();
+ if let Some(path) = path_opt {
+ if let Err(e) = self.save_to_file(&path) {
+ eprintln!("Failed to save project: {:?}", e);
+ self.update_status_text(&format!("Failed to save: {:?}", e));
+ } else {
+ self.update_status_text(&format!("Project saved to {}", path.display()));
+ self.add_recent_file(path);
+ }
+ } else {
+ self.save_file_chooser();
+ }
+ }
+ }
+ if settings_changed {
+ self.save_settings();
+ }
+ }
+
+ pub fn read_panel_offsets(&mut self) {
+ let updated_nodes = self.graph().get_nodes();
+ let dir = self.current_dir_mut();
+ for (i, node) in updated_nodes.iter().enumerate() {
+ if let Some(child) = dir.children.get_mut(i) {
+ child.position = node.position;
+ }
+ }
+ if let Some(sel_idx) = self.graph().selected_node() {
+ if let Some(node) = updated_nodes.get(sel_idx) {
+ self.grid_cursor_col = node.position.0 as i32;
+ self.grid_cursor_row = node.position.1 as i32;
+ }
+ }
+ }
+
+ pub fn sync_cursor_and_selection(&mut self) {
+ if self.focused_widget != Some(CONTENT_IDX) {
+ return;
+ }
+ let dir = self.current_dir();
+ let mut node_at_cursor_idx = None;
+ for (i, node) in dir.children.iter().enumerate() {
+ if node.position.0 as i32 == self.grid_cursor_col && node.position.1 as i32 == self.grid_cursor_row {
+ node_at_cursor_idx = Some(i);
+ break;
+ }
+ }
+
+ if let Some(idx) = node_at_cursor_idx {
+ self.graph_mut().set_selected_node(Some(idx));
+ if self.focused_widget != Some(CONTENT_IDX) {
+ self.focused_widget = Some(CONTENT_IDX);
+ }
+ } else {
+ self.graph_mut().set_selected_node(None);
+ if self.focused_widget == Some(CONTENT_IDX) {
+ self.focused_widget = None;
+ }
+ }
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+ pub fn resize(&mut self, width: u32, height: u32) {
+ if width > 0 && height > 0 {
+ let old_width = self.width;
+ self.physical_width = width;
+ self.physical_height = height;
+ self.width = width as f32 / self.scale as f32;
+ self.height = height as f32 / self.scale as f32;
+ self.wgpu_adapter.resize(width, height);
+
+ let (tex, view) = self.create_depth_texture();
+ self.depth_texture = tex;
+ self.depth_texture_view = view;
+
+ let (b_tex, b_view) = self.create_backdrop_texture();
+ self.backdrop_texture = b_tex;
+ self.backdrop_texture_view = b_view;
+
+ self.backdrop_bind_group = self.wgpu_adapter.device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("Backdrop Bind Group"),
+ layout: &self.backdrop_bind_group_layout,
+ entries: &[
+ wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::TextureView(&self.backdrop_texture_view),
+ },
+ wgpu::BindGroupEntry {
+ binding: 1,
+ resource: wgpu::BindingResource::Sampler(&self.backdrop_sampler),
+ },
+ ],
+ });
+
+ if old_width > 0.0 {
+ let r = self.width / old_width;
+ self.splitter_layout.scale(r);
+ let body_h = self.body_h();
+ self.widgets[SPLITTER1_IDX].set_rect(self.splitter_layout.splitter1_x, HEADER_H, SPLITTER_W, body_h);
+ self.widgets[SPLITTER2_IDX].set_rect(self.splitter_layout.splitter2_x, HEADER_H, SPLITTER_W, body_h);
+ }
+
+ self.sync_layout();
+ self.read_panel_offsets();
+ self.keep_cursor_in_view();
+ self.upload_vertices();
+ self.viewport_dirty = true;
+ }
+ }
+
+ pub fn handle_event(&mut self, event: &WindowEvent) -> bool {
+ match event {
+ WindowEvent::MouseWheel { delta, phase, .. } => {
+ let dialog_open = self.node_palette_visible;
+ let in_network_pane = self.in_network_pane();
+ // eprintln!("DEBUG MOUSEWHEEL: delta={:?}, phase={:?}, cursor=({}, {}), in_network_pane={}", delta, phase, self.cursor_x, self.cursor_y, in_network_pane);
+ let node_area_y = self.positions[CONTENT_IDX].1;
+
+ let in_viewport = self.cursor_x >= self.content_right_x()
+ && self.cursor_x < self.splitter_layout.splitter2_x
+ && self.cursor_y >= node_area_y
+ && self.cursor_y < self.height - STATUS_H;
+
+ let mut focus_changed = false;
+ let mut new_pane = None;
+ if !dialog_open {
+ if in_network_pane {
+ new_pane = Some(LEFT_MENUBAR_IDX);
+ } else if in_viewport {
+ if self.show_spreadsheet && self.cursor_y >= self.positions[SPREADSHEET_IDX].1 {
+ new_pane = Some(SPREADSHEET_MENUBAR_IDX);
+ } else {
+ new_pane = Some(RIGHT_MENUBAR_IDX);
+ }
+ } else if self.cursor_x > self.splitter_layout.splitter2_x + SPLITTER_W && self.cursor_y >= node_area_y && self.cursor_y < self.height - STATUS_H {
+ new_pane = Some(PARAM_MENUBAR_IDX);
+ }
+ }
+ if let Some(pane_idx) = new_pane {
+ if self.focused_pane != pane_idx {
+ self.focused_pane = pane_idx;
+ self.sync_pane_focus();
+ focus_changed = true;
+ }
+ }
+
+ let mut handled = false;
+ let mut needs_sync_grid = false;
+ if !dialog_open && !self.modifiers.control_key() {
+ let ctx = &mut self.ui_context;
+ for (i, w) in self.widgets.iter_mut().enumerate() {
+ if w.mouse_wheel(delta, self.cursor_x, self.cursor_y, ctx) {
+ handled = true;
+ if i == CONTENT_IDX {
+ let active_node_area_y = self.positions[CONTENT_IDX].1;
+ let active_node_area_x = self.positions[CONTENT_IDX].0;
+ let (gx, gy) = w.as_graph_controller().expect("not a GraphController").grid_origin();
+ let prev_pan_x = self.pan_x;
+ let prev_pan_y = self.pan_y;
+ self.pan_x = gx - active_node_area_x;
+ self.pan_y = gy - active_node_area_y;
+
+ let dx = prev_pan_x - self.pan_x;
+ let dy = prev_pan_y - self.pan_y;
+
+ let dt_scroll = Instant::now().duration_since(self.last_frame).as_secs_f32().min(0.1);
+ let vel_x = if dt_scroll > 1e-4 { -dx / dt_scroll } else { -dx * 60.0 };
+ let vel_y = if dt_scroll > 1e-4 { -dy / dt_scroll } else { -dy * 60.0 };
+ self.pan_velocity_x = self.pan_velocity_x * 0.4 + vel_x * 0.6;
+ self.pan_velocity_y = self.pan_velocity_y * 0.4 + vel_y * 0.6;
+
+ self.is_scrolling_trackpad = match delta {
+ MouseScrollDelta::LineDelta(_, _) => false,
+ MouseScrollDelta::PixelDelta(_) => match phase {
+ TouchPhase::Started | TouchPhase::Moved => true,
+ TouchPhase::Ended | TouchPhase::Cancelled => false,
+ }
+ };
+ if self.is_scrolling_trackpad {
+ self.last_scroll_time = Instant::now();
+ self.scroll_accum_x += dx;
+ self.scroll_accum_y += dy;
+ }
+ needs_sync_grid = true;
+ }
+ }
+ }
+ }
+ if needs_sync_grid {
+ self.sync_grid_settings();
+ }
+
+ let result = if handled {
+ self.sync_parameters_to_project();
+
+ // Sync Parameters pane with selected node
+ let params = if !self.is_detached_network {
+ if let Some(slot_idx) = self.graph().selected_node() {
+ let dir = self.current_dir();
+ if slot_idx < dir.children.len() {
+ param_display(&dir.children[slot_idx].params)
+ } else {
+ vec![]
+ }
+ } else {
+ vec![]
+ }
+ } else {
+ vec![]
+ };
+ self.param_mut().set_display_params(¶ms);
+
+
+ self.sync_layout();
+ self.read_panel_offsets();
+ self.upload_vertices();
+ true
+ } else if !dialog_open && in_network_pane {
+ if self.modifiers.control_key() {
+ let factor = match delta {
+ MouseScrollDelta::LineDelta(_x, y) => {
+ if *y > 0.0 { 1.15 } else if *y < 0.0 { 1.0 / 1.15 } else { 1.0 }
+ }
+ MouseScrollDelta::PixelDelta(pos) => {
+ let dy = pos.y as f32;
+ (1.0 + dy / 100.0).clamp(0.8, 1.25)
+ }
+ };
+ if factor != 1.0 {
+ self.zoom(factor, Some((self.cursor_x, self.cursor_y)));
+ true
+ } else {
+ false
+ }
+ } else {
+ match delta {
+ MouseScrollDelta::LineDelta(x, y) => {
+ let dx = *x * 30.0 * self.scroll_speed;
+ let dy = *y * 30.0 * self.scroll_speed;
+ self.pan_x -= dx;
+ self.pan_y -= dy;
+ self.is_scrolling_trackpad = false;
+ let dt_scroll = Instant::now().duration_since(self.last_frame).as_secs_f32().min(0.1);
+ let vel_x = if dt_scroll > 1e-4 { -dx / dt_scroll } else { -dx * 60.0 };
+ let vel_y = if dt_scroll > 1e-4 { -dy / dt_scroll } else { -dy * 60.0 };
+ self.pan_velocity_x = self.pan_velocity_x * 0.4 + vel_x * 0.6;
+ self.pan_velocity_y = self.pan_velocity_y * 0.4 + vel_y * 0.6;
+ self.sync_grid_settings();
+ true
+ }
+ MouseScrollDelta::PixelDelta(pos) => {
+ let dx = (pos.x as f32 / self.scale as f32) * self.scroll_speed;
+ let dy = (pos.y as f32 / self.scale as f32) * self.scroll_speed;
+ self.pan_x -= dx;
+ self.pan_y -= dy;
+ self.is_scrolling_trackpad = match phase {
+ TouchPhase::Started | TouchPhase::Moved => true,
+ TouchPhase::Ended | TouchPhase::Cancelled => false,
+ };
+ self.last_scroll_time = Instant::now();
+ self.scroll_accum_x += dx;
+ self.scroll_accum_y += dy;
+ self.sync_grid_settings();
+ true
+ }
+ }
+ }
+ } else if !dialog_open && in_viewport {
+ if self.modifiers.control_key() {
+ match delta {
+ MouseScrollDelta::LineDelta(_x, y) => {
+ let dy = *y * 0.15;
+ self.viewport_zoom *= (-dy).exp();
+ self.viewport_zoom = self.viewport_zoom.clamp(0.05, 20.0);
+
+ self.is_zooming_viewport = false;
+ let dt_scroll = Instant::now().duration_since(self.last_frame).as_secs_f32().min(0.1);
+ let vel_zoom = if dt_scroll > 1e-4 { dy / dt_scroll } else { dy * 60.0 };
+ self.zoom_velocity = self.zoom_velocity * 0.4 + vel_zoom * 0.6;
+
+ true
+ }
+ MouseScrollDelta::PixelDelta(pos) => {
+ let dy = (pos.y as f32 / self.scale as f32) * 0.005;
+ self.viewport_zoom *= (-dy).exp();
+ self.viewport_zoom = self.viewport_zoom.clamp(0.05, 20.0);
+
+ self.is_zooming_viewport = match phase {
+ TouchPhase::Started | TouchPhase::Moved => true,
+ TouchPhase::Ended | TouchPhase::Cancelled => false,
+ };
+ self.last_zoom_time = Instant::now();
+ self.zoom_accum += dy;
+
+ true
+ }
+ }
+ } else {
+ match delta {
+ MouseScrollDelta::LineDelta(x, y) => {
+ self.scroll_lock = 0;
+ let dx = *x * 0.05;
+ let dy = *y * 0.05;
+ if self.active_camera != "Default Camera" {
+ self.update_active_camera_rotation(dx, -dy);
+ } else {
+ self.rotation_y += dx;
+ self.rotation_x -= dy;
+ }
+
+ self.is_rotating_viewport = false;
+ let dt_scroll = Instant::now().duration_since(self.last_frame).as_secs_f32().min(0.1);
+ let vel_yaw = if dt_scroll > 1e-4 { dx / dt_scroll } else { dx * 60.0 };
+ let vel_pitch = if dt_scroll > 1e-4 { -dy / dt_scroll } else { -dy * 60.0 };
+ self.rotate_velocity_yaw = self.rotate_velocity_yaw * 0.4 + vel_yaw * 0.6;
+ self.rotate_velocity_pitch = self.rotate_velocity_pitch * 0.4 + vel_pitch * 0.6;
+
+ true
+ }
+ MouseScrollDelta::PixelDelta(pos) => {
+ let mut dx = (pos.x as f32 / self.scale as f32) * 0.005;
+ let mut dy = (pos.y as f32 / self.scale as f32) * 0.005;
+
+ if *phase == TouchPhase::Started {
+ self.scroll_lock = 0;
+ self.rotate_accum_yaw = 0.0;
+ self.rotate_accum_pitch = 0.0;
+ }
+
+ if self.scroll_lock == 0 {
+ self.rotate_accum_yaw += dx;
+ self.rotate_accum_pitch -= dy;
+ if self.rotate_accum_yaw.abs() > 0.002 || self.rotate_accum_pitch.abs() > 0.002 {
+ if self.rotate_accum_pitch.abs() > 1.2 * self.rotate_accum_yaw.abs() {
+ self.scroll_lock = 2; // lock vertical
+ } else if self.rotate_accum_yaw.abs() > 1.2 * self.rotate_accum_pitch.abs() {
+ self.scroll_lock = 1; // lock horizontal
+ }
+ }
+ } else {
+ if self.scroll_lock == 1 {
+ dy = 0.0;
+ self.rotate_accum_yaw += dx;
+ } else {
+ dx = 0.0;
+ self.rotate_accum_pitch -= dy;
+ }
+ }
+
+ if self.active_camera != "Default Camera" {
+ self.update_active_camera_rotation(dx, -dy);
+ } else {
+ self.rotation_y += dx;
+ self.rotation_x -= dy;
+ }
+
+ self.is_rotating_viewport = match phase {
+ TouchPhase::Started | TouchPhase::Moved => true,
+ TouchPhase::Ended | TouchPhase::Cancelled => {
+ self.scroll_lock = 0;
+ false
+ }
+ };
+ self.last_rotate_time = Instant::now();
+
+ true
+ }
+ }
+ }
+ } else {
+ false
+ };
+
+ if focus_changed {
+ self.sync_layout();
+ self.read_panel_offsets();
+ self.upload_vertices();
+ true
+ } else {
+ result
+ }
+ }
+ WindowEvent::PinchGesture { delta, .. } => {
+ let dialog_open = self.node_palette_visible;
+ let in_network_pane = self.in_network_pane();
+
+ if !dialog_open && in_network_pane {
+ if delta.is_finite() && *delta != 0.0 {
+ let factor = (1.0 + *delta as f32).clamp(0.8, 1.25);
+ self.zoom(factor, Some((self.cursor_x, self.cursor_y)));
+ true
+ } else {
+ false
+ }
+ } else {
+ false
+ }
+ }
+ WindowEvent::CursorMoved { position, .. } => {
+ self.cursor_x = position.x as f32;
+ self.cursor_y = position.y as f32;
+ let mut changed = false;
+
+ if self.is_panning {
+ let dx = self.cursor_x - self.pan_start_cx;
+ let dy = self.cursor_y - self.pan_start_cy;
+ self.pan_x = self.pan_start_x + dx;
+ self.pan_y = self.pan_start_y + dy;
+ self.sync_grid_settings();
+ self.rebuild_positions();
+ self.apply_layout();
+ self.update_panel_bounds();
+ changed = true;
+ } else {
+ if let Some(idx) = self.drag_widget {
+ if idx == NETWORK_PANEL_IDX && self.is_resizing_network.is_some() {
+ let dir = self.is_resizing_network.unwrap();
+ let dx = self.cursor_x - self.drag_start_mouse.0;
+ let dy = self.cursor_y - self.drag_start_mouse.1;
+ let (sx, sy, sw, sh) = self.drag_start_rect;
+
+ let mut fx = sx;
+ let mut fy = sy;
+ let mut fw = sw;
+ let mut fh = sh;
+
+ if dir.left {
+ let new_w = (sw - dx).max(150.0);
+ fx = sx + sw - new_w;
+ fw = new_w;
+ } else if dir.right {
+ fw = (sw + dx).max(150.0);
+ }
+
+ if dir.top {
+ let new_h = (sh - dy).max(100.0);
+ fy = sy + sh - new_h;
+ fh = new_h;
+ } else if dir.bottom {
+ fh = (sh + dy).max(100.0);
+ }
+
+ self.floating_network_layout = (fx, fy, fw, fh);
+ self.rebuild_positions();
+ self.apply_layout();
+ self.sync_grid_settings();
+ changed = true;
+ } else if idx == PARAM_IDX && self.is_resizing_param {
+ let dx = self.cursor_x - self.drag_start_mouse.0;
+ let sw = self.drag_start_param_w;
+ let new_w = (sw - dx).max(150.0);
+ self.floating_param_width = new_w;
+ self.rebuild_positions();
+ self.apply_layout();
+ changed = true;
+ } else if idx == SPREADSHEET_IDX && self.is_resizing_spreadsheet {
+ let dy = self.cursor_y - self.drag_start_mouse.1;
+ let sh = self.drag_start_spreadsheet_h;
+ let new_h = (sh - dy).max(100.0);
+ self.floating_spreadsheet_height = new_h;
+ self.rebuild_positions();
+ self.apply_layout();
+ changed = true;
+ } else {
+ self.widgets[idx].set_modifiers(self.modifiers.control_key(), self.modifiers.shift_key(), self.modifiers.alt_key());
+ if self.widgets[idx].drag_update(self.cursor_x, self.cursor_y) {
+ changed = true;
+ if idx == NETWORK_PANEL_IDX {
+ self.sync_grid_settings();
+ }
+ }
+ }
+ }
+
+ if self.drag_widget.is_none() {
+ for i in 0..self.widgets.len() {
+ let (cx, cy) = (self.cursor_x, self.cursor_y);
+ let is_network_part = i == CONTENT_IDX || i == LEFT_MENUBAR_IDX || i == BREADCRUMB_IDX || i == NETWORK_PANEL_IDX;
+ let inside = if self.circular_network_pane && is_network_part {
+ if i == CONTENT_IDX {
+ self.circular_network_layout.hit_test_content(cx, cy, 0.0, BREADCRUMB_H)
+ } else if i == LEFT_MENUBAR_IDX {
+ false
+ } else if i == BREADCRUMB_IDX {
+ self.circular_network_layout.hit_test_breadcrumb(cx, cy, 0.0, BREADCRUMB_H)
+ } else if i == NETWORK_PANEL_IDX {
+ self.widgets[NETWORK_PANEL_IDX].hit_test(cx, cy, &self.ui_context)
+ } else {
+ false
+ }
+ } else {
+ self.widgets[i].hit_test(cx, cy, &self.ui_context)
+ };
+ let (tx, ty) = if inside { (cx, cy) } else { (-9999.0, -9999.0) };
+ if self.widgets[i].cursor_moved(tx, ty, &mut self.ui_context) {
+ changed = true;
+ }
+ }
+ }
+ }
+ changed
+ }
+ WindowEvent::MouseInput { state: btn_state, button, .. } => {
+ println!("DEBUG: MouseInput state={:?} button={:?} cursor=({}, {})", btn_state, button, self.cursor_x, self.cursor_y);
+ if *btn_state == ElementState::Pressed {
+ self.pan_velocity_x = 0.0;
+ self.pan_velocity_y = 0.0;
+ self.is_scrolling_trackpad = false;
+ self.scroll_accum_x = 0.0;
+ self.scroll_accum_y = 0.0;
+
+ self.rotate_velocity_yaw = 0.0;
+ self.rotate_velocity_pitch = 0.0;
+ self.is_rotating_viewport = false;
+ self.scroll_lock = 0;
+ self.rotate_accum_yaw = 0.0;
+ self.rotate_accum_pitch = 0.0;
+
+ self.zoom_velocity = 0.0;
+ self.is_zooming_viewport = false;
+ self.zoom_accum = 0.0;
+ }
+ let dialog_open = self.node_palette_visible;
+ let in_network_pane = self.in_network_pane();
+ let node_area_x = self.positions[CONTENT_IDX].0;
+ let node_area_y = self.positions[CONTENT_IDX].1;
+
+ let is_pan_trigger = !dialog_open
+ && in_network_pane
+ && (*button == MouseButton::Middle
+ || (*button == MouseButton::Left && self.space_pressed));
+
+ if is_pan_trigger {
+ match btn_state {
+ ElementState::Pressed => {
+ self.is_panning = true;
+ self.pan_start_cx = self.cursor_x;
+ self.pan_start_cy = self.cursor_y;
+ self.pan_start_x = self.pan_x;
+ self.pan_start_y = self.pan_y;
+ self.pan_velocity_x = 0.0;
+ self.pan_velocity_y = 0.0;
+ self.last_frame_pan_x = self.pan_x;
+ self.last_frame_pan_y = self.pan_y;
+ return true;
+ }
+ ElementState::Released => {
+ if self.is_panning {
+ self.is_panning = false;
+ self.sync_layout();
+ self.read_panel_offsets();
+ self.upload_vertices();
+ return true;
+ }
+ }
+ }
+ }
+
+ if self.is_panning && *btn_state == ElementState::Released {
+ self.is_panning = false;
+ self.sync_layout();
+ self.read_panel_offsets();
+ self.upload_vertices();
+ return true;
+ }
+
+ if *button != MouseButton::Left && *button != MouseButton::Right { return false; }
+ let mut changed = false;
+ let old_focus = self.focused_widget;
+ let was_page_hidden = self.page_selector(PAGINATOR_IDX).is_page_hidden();
+
+ let hits_widget = |state: &State, i: usize, x: f32, y: f32| -> bool {
+ if state.circular_network_pane && (i == CONTENT_IDX || i == LEFT_MENUBAR_IDX || i == BREADCRUMB_IDX) {
+ if i == CONTENT_IDX {
+ state.circular_network_layout.hit_test_content(x, y, 0.0, BREADCRUMB_H)
+ } else if i == LEFT_MENUBAR_IDX {
+ false
+ } else if i == BREADCRUMB_IDX {
+ state.circular_network_layout.hit_test_breadcrumb(x, y, 0.0, BREADCRUMB_H)
+ } else {
+ false
+ }
+ } else {
+ state.widgets[i].hit_test(x, y, &state.ui_context)
+ }
+ };
+
+ let in_circle_network_pane = if self.circular_network_pane {
+ self.circular_network_layout.hit_test_content(self.cursor_x, self.cursor_y, 0.0, BREADCRUMB_H)
+ } else {
+ in_network_pane
+ };
+
+ match btn_state {
+ ElementState::Pressed => {
+ let hits_any_menu = (0..self.widgets.len()).any(|i| {
+ hits_widget(self, i, self.cursor_x, self.cursor_y)
+ && self.menu(i).get_menu_items_at(self.cursor_x, self.cursor_y).is_some()
+ });
+
+ if !hits_any_menu {
+ if let Some(pid) = self.active_menu_cloud_pid {
+ let is_running = unsafe {
+ libc::kill(pid as libc::pid_t, 0) == 0
+ };
+ if is_running {
+ unsafe {
+ libc::kill(pid as libc::pid_t, libc::SIGTERM);
+ }
+ }
+ self.active_menu_cloud_pid = None;
+ self.active_menu_cloud_idx = None;
+ }
+ }
+
+ if *button == MouseButton::Left && self.circular_network_pane {
+ let on_border = self.circular_network_layout.hit_test_border(self.cursor_x, self.cursor_y, 12.0);
+ let hit_menubar = false;
+ if hit_menubar {
+ if self.menu(LEFT_MENUBAR_IDX).get_menu_items_at(self.cursor_x, self.cursor_y).is_some() {
+ self.focused_pane = LEFT_MENUBAR_IDX;
+ self.widgets[LEFT_MENUBAR_IDX].set_modifiers(self.modifiers.control_key(), self.modifiers.shift_key(), self.modifiers.alt_key());
+ if self.widgets[LEFT_MENUBAR_IDX].mouse_input(*button, *btn_state, self.cursor_x, self.cursor_y, &mut self.ui_context) {
+ changed = true;
+ }
+ }
+ }
+ if on_border || hit_menubar {
+ self.drag_widget = Some(NETWORK_PANEL_IDX);
+ self.widgets[NETWORK_PANEL_IDX].set_modifiers(self.modifiers.control_key(), self.modifiers.shift_key(), self.modifiers.alt_key());
+ self.widgets[NETWORK_PANEL_IDX].drag_begin(self.cursor_x, self.cursor_y);
+ self.focused_pane = LEFT_MENUBAR_IDX;
+ if let Some(old) = self.focused_widget {
+ self.widgets[old].unfocus();
+ self.focused_widget = None;
+ }
+ self.widgets[PARAM_IDX].unfocus();
+ self.sync_parameters_to_project();
+ return true;
+ }
+ }
+
+ if *button == MouseButton::Left && !self.circular_network_pane && self.show_network {
+ let (fx, fy, fw, fh) = self.floating_network_layout;
+ let cx = self.cursor_x;
+ let cy = self.cursor_y;
+
+ let margin = 8.0_f32;
+ let on_left = false;
+ let on_right = cx >= fx + fw - margin && cx <= fx + fw + margin && cy >= fy - margin && cy <= fy + fh + margin;
+ let on_top = false;
+ let on_bottom = false;
+
+ if on_left || on_right || on_top || on_bottom {
+ self.is_resizing_network = Some(ResizeDirection {
+ left: on_left,
+ right: on_right,
+ top: on_top,
+ bottom: on_bottom,
+ });
+ self.drag_widget = Some(NETWORK_PANEL_IDX);
+ self.drag_start_rect = self.floating_network_layout;
+ self.drag_start_mouse = (cx, cy);
+ self.focused_pane = LEFT_MENUBAR_IDX;
+ if let Some(old) = self.focused_widget {
+ self.widgets[old].unfocus();
+ self.focused_widget = None;
+ }
+ self.widgets[PARAM_IDX].unfocus();
+ self.sync_parameters_to_project();
+ return true;
+ } else if cx >= fx && cx < fx + fw && cy >= fy && cy < fy + (if self.show_network { BREADCRUMB_H } else { 0.0 }) {
+ if self.widgets[BREADCRUMB_IDX].mouse_input(*button, *btn_state, cx, cy, &mut self.ui_context) {
+ return true;
+ }
+ self.is_resizing_network = None;
+ self.drag_widget = Some(NETWORK_PANEL_IDX);
+ self.widgets[NETWORK_PANEL_IDX].set_modifiers(self.modifiers.control_key(), self.modifiers.shift_key(), self.modifiers.alt_key());
+ self.widgets[NETWORK_PANEL_IDX].drag_begin(cx, cy);
+ self.focused_pane = LEFT_MENUBAR_IDX;
+ if let Some(old) = self.focused_widget {
+ self.widgets[old].unfocus();
+ self.focused_widget = None;
+ }
+ self.widgets[PARAM_IDX].unfocus();
+ self.sync_parameters_to_project();
+ return true;
+ }
+ }
+
+ if *button == MouseButton::Left && self.show_parameters {
+ let gap = 18.0_f32;
+ let param_w = self.floating_param_width;
+ let param_x = self.width - gap - param_w;
+ let param_y = HEADER_H + gap;
+ let param_h = (self.height - HEADER_H - STATUS_H - 2.0 * gap).max(100.0);
+ let cx = self.cursor_x;
+ let cy = self.cursor_y;
+
+ let margin = 8.0_f32;
+ let on_left = cx >= param_x - margin && cx <= param_x + margin && cy >= param_y - margin && cy <= param_y + param_h + margin;
+
+ if on_left {
+ self.is_resizing_param = true;
+ self.drag_widget = Some(PARAM_IDX);
+ self.drag_start_param_w = param_w;
+ self.drag_start_mouse = (cx, cy);
+ self.focused_pane = PARAM_MENUBAR_IDX;
+ if let Some(old) = self.focused_widget {
+ self.widgets[old].unfocus();
+ self.focused_widget = None;
+ }
+ return true;
+ }
+ }
+
+ if *button == MouseButton::Left && self.show_spreadsheet {
+ let gap = 18.0_f32;
+ let fx = gap;
+ let (mut _n_fx, mut _n_fy, mut fw, mut _n_fh) = self.floating_network_layout;
+ fw = fw.clamp(150.0, (self.width - 2.0 * gap).max(150.0));
+
+ let param_w = self.floating_param_width.clamp(150.0, (self.width - 2.0 * gap).max(150.0));
+ let param_x = self.width - gap - param_w;
+
+ let ss_x = if self.show_network { fx + fw + gap } else { gap };
+ let ss_w_end = if self.show_parameters { param_x - gap } else { self.width - gap };
+ let ss_w = (ss_w_end - ss_x).max(150.0);
+
+ let ss_y_end = self.height - STATUS_H - gap;
+ let ss_h = self.floating_spreadsheet_height.clamp(100.0, (ss_y_end - HEADER_H - gap).max(100.0));
+ let ss_y = ss_y_end - ss_h;
+
+ let cx = self.cursor_x;
+ let cy = self.cursor_y;
+ let margin = 8.0_f32;
+ let on_top = cx >= ss_x && cx <= ss_x + ss_w && cy >= ss_y - margin && cy <= ss_y + margin;
+
+ if on_top {
+ self.is_resizing_spreadsheet = true;
+ self.drag_widget = Some(SPREADSHEET_IDX);
+ self.drag_start_spreadsheet_h = ss_h;
+ self.drag_start_mouse = (cx, cy);
+ self.focused_pane = SPREADSHEET_MENUBAR_IDX;
+ if let Some(old) = self.focused_widget {
+ self.widgets[old].unfocus();
+ self.focused_widget = None;
+ }
+ return true;
+ }
+ }
+
+ if *button == MouseButton::Right {
+ if !dialog_open && in_circle_network_pane {
+ let col = ((self.cursor_x - node_area_x - self.pan_x) / (self.grid_size_x + self.skipped_col_w)).floor() as i32;
+ let row = ((self.cursor_y - node_area_y - self.pan_y) / (self.grid_size_y + self.skipped_row_h)).floor() as i32;
+ self.grid_cursor_col = col;
+ self.grid_cursor_row = row;
+ self.open_node_palette();
+ return true;
+ }
+ return false;
+ }
+ let mut click_target = None;
+ for i in 0..self.widgets.len() {
+ if self.menu(i).is_menu_open() && hits_widget(self, i, self.cursor_x, self.cursor_y) {
+ click_target = Some(i);
+ break;
+ }
+ }
+ if click_target.is_none() {
+ let mut hit_order: Vec<usize> = (0..self.widgets.len()).collect();
+ hit_order.sort_by_key(|&i| {
+ let z = if i == NETWORK_PANEL_IDX || i == PARAM_PLATE_IDX {
+ -5
+ } else if i == VIEWPORT_IDX || i == PARAM_IDX {
+ -4
+ } else {
+ self.widgets[i].z_index()
+ };
+ -z
+ });
+ click_target = hit_order.into_iter()
+ .find(|&i| hits_widget(self, i, self.cursor_x, self.cursor_y));
+ }
+
+ // Determine new focused pane
+ let mut new_pane = None;
+ if let Some(i) = click_target {
+ if i == LEFT_MENUBAR_IDX || i == CONTENT_IDX || i == CANVAS_IDX || i == BREADCRUMB_IDX || i == NETWORK_PANEL_IDX {
+ new_pane = Some(LEFT_MENUBAR_IDX);
+ } else if i == RIGHT_MENUBAR_IDX || i == VIEWPORT_IDX {
+ new_pane = Some(RIGHT_MENUBAR_IDX);
+ } else if i == PARAM_MENUBAR_IDX || i == PARAM_IDX || i == PARAM_PLATE_IDX {
+ new_pane = Some(PARAM_MENUBAR_IDX);
+ } else if i == SPREADSHEET_MENUBAR_IDX || i == SPREADSHEET_IDX {
+ new_pane = Some(SPREADSHEET_MENUBAR_IDX);
+ } else if i == HEADER_IDX {
+ new_pane = Some(HEADER_IDX);
+ }
+
+ // If clicked on a blank spot of any pane menubar, switch focus to main menubar (HEADER_IDX)
+ if (i == LEFT_MENUBAR_IDX || i == RIGHT_MENUBAR_IDX || i == PARAM_MENUBAR_IDX || i == SPREADSHEET_MENUBAR_IDX)
+ && self.menu(i).get_menu_items_at(self.cursor_x, self.cursor_y).is_none()
+ {
+ new_pane = Some(HEADER_IDX);
+ }
+ } else {
+ if self.circular_network_pane {
+ if self.cursor_x > self.splitter_layout.splitter2_x + SPLITTER_W {
+ new_pane = Some(PARAM_MENUBAR_IDX);
+ } else {
+ if self.show_spreadsheet && self.cursor_y >= self.positions[SPREADSHEET_IDX].1 {
+ new_pane = Some(SPREADSHEET_MENUBAR_IDX);
+ } else {
+ new_pane = Some(RIGHT_MENUBAR_IDX);
+ }
+ }
+ } else {
+ if self.cursor_x < self.splitter_layout.splitter1_x {
+ new_pane = Some(LEFT_MENUBAR_IDX);
+ } else if self.cursor_x > self.splitter_layout.splitter2_x + SPLITTER_W {
+ new_pane = Some(PARAM_MENUBAR_IDX);
+ } else {
+ if self.show_spreadsheet && self.cursor_y >= self.positions[SPREADSHEET_IDX].1 {
+ new_pane = Some(SPREADSHEET_MENUBAR_IDX);
+ } else {
+ new_pane = Some(RIGHT_MENUBAR_IDX);
+ }
+ }
+ }
+ }
+ if let Some(pane_idx) = new_pane {
+ if self.focused_pane != pane_idx {
+ self.focused_pane = pane_idx;
+ changed = true;
+ }
+ }
+
+ if let Some(old) = self.focused_widget {
+ if click_target != Some(old) && click_target != Some(PARAM_IDX) {
+ self.widgets[old].unfocus();
+ self.focused_widget = None;
+ }
+ }
+ if click_target != Some(PARAM_IDX) {
+ self.widgets[PARAM_IDX].unfocus();
+ self.sync_parameters_to_project();
+ }
+ if click_target.is_none() && !dialog_open && in_circle_network_pane {
+ let col = ((self.cursor_x - node_area_x - self.pan_x) / (self.grid_size_x + self.skipped_col_w)).floor() as i32;
+ let row = ((self.cursor_y - node_area_y - self.pan_y) / (self.grid_size_y + self.skipped_row_h)).floor() as i32;
+ self.grid_cursor_col = col;
+ self.grid_cursor_row = row;
+ changed = true;
+ }
+ if let Some(i) = click_target {
+ self.widgets[i].set_modifiers(self.modifiers.control_key(), self.modifiers.shift_key(), self.modifiers.alt_key());
+ if self.widgets[i].mouse_input(*button, *btn_state, self.cursor_x, self.cursor_y, &mut self.ui_context) {
+ changed = true;
+ if i == PARAM_IDX {
+ self.sync_parameters_to_project();
+ }
+ if i == PAGINATOR_IDX {
+ self.update_paginator();
+ self.rebuild_positions();
+ self.apply_layout();
+ }
+ } else if i == PAGINATOR_IDX {
+ if *button == MouseButton::Left {
+ self.focused_pane = HEADER_IDX;
+ self.sync_pane_focus();
+ changed = true;
+ }
+ }
+ if self.widgets[i].draggable() {
+ self.widgets[i].set_modifiers(self.modifiers.control_key(), self.modifiers.shift_key(), self.modifiers.alt_key());
+ self.widgets[i].drag_begin(self.cursor_x, self.cursor_y);
+ self.drag_widget = Some(i);
+ }
+ if i != PARAM_IDX {
+ self.widgets[i].focus();
+ self.focused_widget = Some(i);
+ if self.menu(i).is_menu_bar() && !self.widgets[i].focused(&self.ui_context) {
+ self.widgets[i].unfocus();
+ self.focused_widget = None;
+ }
+ }
+ if i == CONTENT_IDX {
+ if let Some(slot_idx) = self.graph().selected_node() {
+ let dir = self.current_dir();
+ if slot_idx < dir.children.len() {
+ let pos = dir.children[slot_idx].position;
+ self.grid_cursor_col = pos.0 as i32;
+ self.grid_cursor_row = pos.1 as i32;
+ }
+ } else {
+ let col = ((self.cursor_x - node_area_x - self.pan_x) / (self.grid_size_x + self.skipped_col_w)).floor() as i32;
+ let row = ((self.cursor_y - node_area_y - self.pan_y) / (self.grid_size_y + self.skipped_row_h)).floor() as i32;
+ self.grid_cursor_col = col;
+ self.grid_cursor_row = row;
+ changed = true;
+ }
+ if let Some(dir_idx) = self.graph().double_clicked_node() {
+ self.graph_mut().clear_double_clicked_node();
+ let dir = self.current_dir();
+ if dir_idx < dir.children.len() && (dir.children[dir_idx].node_type == "node" || dir.children[dir_idx].node_type == "opencl" || !dir.children[dir_idx].children.is_empty()) {
+ self.current_path.push(dir_idx);
+ self.on_path_changed();
+ changed = true;
+ }
+ }
+ }
+ }
+ self.sync_pane_focus();
+ }
+ ElementState::Released => {
+ if self.drag_widget.is_some() {
+ let idx = self.drag_widget.unwrap();
+ if idx == NETWORK_PANEL_IDX {
+ self.widgets[idx].drag_end();
+ self.is_resizing_network = None;
+ self.sync_layout();
+ self.read_panel_offsets();
+ self.upload_vertices();
+ } else if idx == PARAM_IDX {
+ self.widgets[idx].drag_end();
+ self.is_resizing_param = false;
+ self.sync_layout();
+ self.upload_vertices();
+ } else if idx == SPREADSHEET_IDX {
+ self.widgets[idx].drag_end();
+ self.is_resizing_spreadsheet = false;
+ self.sync_layout();
+ self.upload_vertices();
+ } else if idx == CONTENT_IDX {
+ self.widgets[idx].drag_end();
+ let updated_nodes = self.graph().get_nodes();
+ let dir = self.current_dir_mut();
+ for (i, node) in updated_nodes.iter().enumerate() {
+ if let Some(child) = dir.children.get_mut(i) {
+ child.position = node.position;
+ }
+ }
+ if let Some(sel_idx) = self.graph().selected_node() {
+ if let Some(node) = updated_nodes.get(sel_idx) {
+ self.grid_cursor_col = node.position.0 as i32;
+ self.grid_cursor_row = node.position.1 as i32;
+ }
+ }
+ self.rebuild_positions();
+ self.apply_layout();
+ self.update_panel_bounds();
+ self.upload_vertices();
+ } else {
+ self.widgets[idx].drag_end();
+ }
+ self.drag_widget = None;
+ changed = true;
+ }
+ let ctx = &mut self.ui_context;
+ for w in &mut self.widgets {
+ w.set_modifiers(self.modifiers.control_key(), self.modifiers.shift_key(), self.modifiers.alt_key());
+ if w.mouse_input(*button, *btn_state, self.cursor_x, self.cursor_y, ctx) {
+ changed = true;
+ }
+ }
+ }
+ }
+
+
+ if let Some((i, visible)) = self.graph_mut().take_node_geom_toggle() {
+ self.current_dir_mut().children[i].geometry_visible = visible;
+ self.rebuild_scene_geometry();
+ changed = true;
+ }
+
+ if self.page_selector(PAGINATOR_IDX).is_page_hidden() != was_page_hidden {
+ self.rebuild_positions();
+ self.apply_layout();
+ self.sync_pane_focus();
+ self.sync_nodes();
+ changed = true;
+ }
+
+ if self.focused_widget != old_focus {
+ changed = true;
+ }
+ changed
+ }
+ WindowEvent::ModifiersChanged(mods) => {
+ self.modifiers = mods.state();
+ false
+ }
+ WindowEvent::KeyboardInput { event, .. } => {
+ if event.logical_key == Key::Named(NamedKey::Space) {
+ self.space_pressed = event.state == ElementState::Pressed;
+ }
+
+ if event.state == ElementState::Pressed {
+ if event.logical_key == Key::Named(NamedKey::Tab) && self.modifiers.control_key() && !self.modifiers.alt_key() && !self.modifiers.super_key() {
+ self.focused_pane = get_next_visible_pane(
+ self.focused_pane,
+ self.show_network,
+ self.show_viewport,
+ self.show_parameters,
+ self.show_spreadsheet,
+ self.modifiers.shift_key(),
+ );
+ self.sync_pane_focus();
+ return true;
+ }
+ }
+
+ if self.widgets[PARAM_IDX].keyboard_input(event, &mut self.ui_context) {
+ self.sync_parameters_to_project();
+ return true;
+ }
+
+ if self.node_palette_visible {
+ return self.handle_node_palette_key(event);
+ }
+ let mut changed = false;
+ if event.state == ElementState::Pressed {
+ let is_plain_key = !self.modifiers.control_key() && !self.modifiers.alt_key() && !self.modifiers.super_key();
+ let is_alt_key = self.modifiers.alt_key() && !self.modifiers.control_key() && !self.modifiers.super_key();
+
+ let mut delta = None;
+
+ match &event.logical_key {
+ Key::Named(NamedKey::ArrowUp) => {
+ delta = Some((0, -1));
+ }
+ Key::Named(NamedKey::ArrowDown) => {
+ delta = Some((0, 1));
+ }
+ Key::Named(NamedKey::ArrowLeft) => {
+ delta = Some((-1, 0));
+ }
+ Key::Named(NamedKey::ArrowRight) => {
+ delta = Some((1, 0));
+ }
+ Key::Character(s) => {
+ match s.as_str() {
+ "k" | "K" if is_plain_key || (is_alt_key && self.focused_pane == LEFT_MENUBAR_IDX) => {
+ delta = Some((0, -1));
+ }
+ "j" | "J" if is_plain_key || (is_alt_key && self.focused_pane == LEFT_MENUBAR_IDX) => {
+ delta = Some((0, 1));
+ }
+ "h" | "H" if is_plain_key || (is_alt_key && self.focused_pane == LEFT_MENUBAR_IDX) => {
+ delta = Some((-1, 0));
+ }
+ "l" | "L" if is_plain_key || (is_alt_key && self.focused_pane == LEFT_MENUBAR_IDX) => {
+ delta = Some((1, 0));
+ }
+ _ => {}
+ }
+ }
+ _ => {}
+ }
+
+ if let Some((dc, dr)) = delta {
+ self.pan_velocity_x = 0.0;
+ self.pan_velocity_y = 0.0;
+ self.is_scrolling_trackpad = false;
+ self.scroll_accum_x = 0.0;
+ self.scroll_accum_y = 0.0;
+ if is_alt_key {
+ let active_nodes = self.current_dir().children.len();
+ let node_idx_at_cursor = self.current_dir().children.iter().take(active_nodes).position(|child| {
+ child.position.0 as i32 == self.grid_cursor_col && child.position.1 as i32 == self.grid_cursor_row
+ });
+ if let Some(idx) = node_idx_at_cursor {
+ let new_x = self.current_dir().children[idx].position.0 + dc as f32;
+ let new_y = self.current_dir().children[idx].position.1 + dr as f32;
+ self.current_dir_mut().children[idx].position = (new_x, new_y);
+ self.sync_nodes();
+ self.sync_layout();
+ self.upload_vertices();
+ }
+ }
+ self.grid_cursor_col += dc;
+ self.grid_cursor_row += dr;
+ self.sync_cursor_and_selection();
+ changed = true;
+ } else {
+ if let Key::Character(s) = &event.logical_key {
+ if is_plain_key {
+ match s.as_str() {
+ "e" | "E" => {
+ if self.focused_widget == Some(CONTENT_IDX) {
+ if let Some(slot_idx) = self.graph().selected_node() {
+ let dir = self.current_dir();
+ if slot_idx < dir.children.len() {
+ let visible = !dir.children[slot_idx].geometry_visible;
+ self.current_dir_mut().children[slot_idx].geometry_visible = visible;
+ self.sync_nodes();
+ self.rebuild_scene_geometry();
+ changed = true;
+ }
+ }
+ }
+ }
+ "r" | "R" => {
+ if self.focused_pane == RIGHT_MENUBAR_IDX {
+ if self.active_camera != "Default Camera" {
+ self.update_active_camera_rotation_reset();
+ } else {
+ self.rotation_y = 0.0;
+ self.rotation_x = 0.0;
+ }
+ self.viewport_zoom = 1.0;
+ self.rotate_velocity_yaw = 0.0;
+ self.rotate_velocity_pitch = 0.0;
+ self.zoom_velocity = 0.0;
+ self.is_rotating_viewport = false;
+ self.is_zooming_viewport = false;
+ self.scroll_lock = 0;
+ changed = true;
+ }
+ }
+ "u" | "U" => {
+ if self.focused_pane == LEFT_MENUBAR_IDX {
+ if !self.current_path.is_empty() {
+ self.current_path.pop();
+ self.on_path_changed();
+ changed = true;
+ }
+ }
+ }
+ "i" | "I" => {
+ if self.focused_pane == LEFT_MENUBAR_IDX {
+ if self.focused_widget == Some(CONTENT_IDX) {
+ if let Some(slot_idx) = self.graph().selected_node() {
+ let dir = self.current_dir();
+ if slot_idx < dir.children.len() && (dir.children[slot_idx].node_type == "node" || dir.children[slot_idx].node_type == "opencl" || !dir.children[slot_idx].children.is_empty()) {
+ self.current_path.push(slot_idx);
+ self.on_path_changed();
+ changed = true;
+ }
+ }
+ }
+ }
+ }
+ "-" => {
+ self.zoom(1.0 / 1.15, None);
+ changed = true;
+ }
+ "=" | "+" => {
+ self.zoom(1.15, None);
+ changed = true;
+ }
+ "f" | "F" => {
+ if self.focused_pane == LEFT_MENUBAR_IDX {
+ let active_nodes = self.current_dir().children.len();
+ if active_nodes == 0 {
+ self.grid_size_x = 80.0;
+ self.grid_size_y = 40.0;
+ self.skipped_col_w = 20.0;
+ self.skipped_row_h = 20.0;
+ self.pan_x = 20.0;
+ self.pan_y = 20.0;
+ } else {
+ let base_gx = 80.0;
+ let base_gy = 40.0;
+ let base_col_w = 20.0;
+ let base_row_h = 20.0;
+
+ let mut b_xmin = f32::MAX;
+ let mut b_xmax = f32::MIN;
+ let mut b_ymin = f32::MAX;
+ let mut b_ymax = f32::MIN;
+
+ for slot_idx in 0..active_nodes {
+ let (col, row) = self.current_dir().children[slot_idx].position;
+ let x_min = col * (base_gx + base_col_w);
+ let x_max = x_min + base_gx;
+ let y_min = row * (base_gy + base_row_h);
+ let y_max = y_min + base_gy;
+
+ if x_min < b_xmin { b_xmin = x_min; }
+ if x_max > b_xmax { b_xmax = x_max; }
+ if y_min < b_ymin { b_ymin = y_min; }
+ if y_max > b_ymax { b_ymax = y_max; }
+ }
+
+ let w_base = b_xmax - b_xmin;
+ let h_base = b_ymax - b_ymin;
+
+ let (_px, _py, pw, ph) = self.positions[CONTENT_IDX];
+ let viewport_w = pw;
+ let viewport_h = ph;
+
+ let padding = 40.0;
+ let padded_w = (viewport_w - 2.0 * padding).max(10.0);
+ let padded_h = (viewport_h - 2.0 * padding).max(10.0);
+
+ let fx = padded_w / w_base;
+ let fy = padded_h / h_base;
+ let mut f = fx.min(fy);
+
+ f = f.min(1.0).max(30.0 / base_gx);
+
+ self.grid_size_x = (base_gx * f).clamp(30.0, 500.0);
+ self.grid_size_y = (base_gy * f).clamp(15.0, 250.0);
+ self.skipped_col_w = base_col_w * f;
+ self.skipped_row_h = base_row_h * f;
+
+ let mut actual_xmin = f32::MAX;
+ let mut actual_xmax = f32::MIN;
+ let mut actual_ymin = f32::MAX;
+ let mut actual_ymax = f32::MIN;
+
+ for slot_idx in 0..active_nodes {
+ let (col, row) = self.current_dir().children[slot_idx].position;
+ let x_min = col * (self.grid_size_x + self.skipped_col_w);
+ let x_max = x_min + self.grid_size_x;
+ let y_min = row * (self.grid_size_y + self.skipped_row_h);
+ let y_max = y_min + self.grid_size_y;
+
+ if x_min < actual_xmin { actual_xmin = x_min; }
+ if x_max > actual_xmax { actual_xmax = x_max; }
+ if y_min < actual_ymin { actual_ymin = y_min; }
+ if y_max > actual_ymax { actual_ymax = y_max; }
+ }
+
+ let actual_w = actual_xmax - actual_xmin;
+ let actual_h = actual_ymax - actual_ymin;
+
+ self.pan_x = (viewport_w - actual_w) / 2.0 - actual_xmin;
+ self.pan_y = (viewport_h - actual_h) / 2.0 - actual_ymin;
+ }
+
+ self.sync_grid_settings();
+ self.rebuild_positions();
+ self.apply_layout();
+ self.update_panel_bounds();
+ self.upload_vertices();
+ changed = true;
+ }
+ }
+ _ => {}
+ }
+ }
+ }
+ }
+
+ if changed {
+ self.keep_cursor_in_view();
+ }
+
+ if !changed && event.state == ElementState::Pressed {
+ if event.logical_key == Key::Named(NamedKey::Tab) {
+ self.open_node_palette();
+ return true;
+ }
+ if let Some(action) = self.shortcut_manager.match_action(&self.modifiers, &event.logical_key) {
+ self.pending_action = Some(action);
+ return true;
+ }
+ }
+ }
+ if changed {
+ true
+ } else if let Some(idx) = self.focused_widget {
+ self.widgets[idx].keyboard_input(event, &mut self.ui_context)
+ } else { false }
+ }
+ }
+ }
+
+ pub fn render(&mut self) -> bool {
+ let now = Instant::now();
+ let dt = now.duration_since(self.last_frame).as_secs_f32().min(0.1);
+ self.last_frame = now;
+
+ if now.duration_since(self.last_config_read).as_secs_f32() > 2.0 {
+ self.last_config_read = now;
+ let config_paths = [
+ "/home/lsgalante/.config/cce/config.toml",
+ "/home/lsgalante/.config/ccec/config.toml",
+ ];
+ let mut current_mod_time = None;
+ for path in &config_paths {
+ if let Ok(m) = std::fs::metadata(path) {
+ if let Ok(t) = m.modified() {
+ current_mod_time = Some(t);
+ break;
+ }
+ }
+ }
+ if current_mod_time != self.last_config_mod_time {
+ self.last_config_mod_time = current_mod_time;
+ clear_ui::layout::reload_config();
+ self.update_inertial_settings();
+ self.rebuild_positions();
+ self.apply_layout();
+ self.upload_vertices();
+ } else {
+ self.update_inertial_settings();
+ }
+ let design_path = DesignSettings::file_path();
+ if let Ok(m) = std::fs::metadata(&design_path) {
+ if let Ok(mod_time) = m.modified() {
+ if Some(mod_time) != self.last_design_mod_time {
+ self.last_design_mod_time = Some(mod_time);
+ let settings = DesignSettings::load();
+ self.square_viewport = settings.square_viewport;
+ self.grid_snap_enabled = settings.grid_snap_enabled;
+ self.network_grid_visible = settings.network_grid_enabled;
+ self.grid_size_x = settings.grid_size_x;
+ self.grid_size_y = settings.grid_size_y;
+ self.skipped_row_h = settings.skipped_row_h;
+ self.skipped_col_w = settings.skipped_col_w;
+ self.grid_thickness = settings.grid_thickness;
+ self.show_grid = settings.show_grid_enabled;
+ self.show_cube = settings.show_cube_enabled;
+ self.show_origin = settings.show_origin_enabled;
+ self.show_camera_pivot = settings.show_camera_pivot_enabled;
+ self.viewport_bg_color = settings.viewport_bg_color;
+ self.node_color = settings.node_color;
+ self.grid_color = settings.grid_color;
+ self.origin_size = settings.origin_size;
+ self.camera_pivot_size = settings.camera_pivot_size;
+ self.uniform_background = settings.uniform_background;
+ self.network_opacity = settings.network_opacity;
+ self.cell_color = settings.cell_color;
+ self.gap_color = settings.gap_color;
+
+ colors::set_node_color([self.node_color[0], self.node_color[1], self.node_color[2], 1.0]);
+
+ self.update_origin_geometry();
+ self.update_grid_geometry();
+ self.update_pivot_geometry();
+ self.sync_grid_settings();
+ self.upload_vertices();
+ }
+ }
+ }
+ }
+
+ // Panning velocity tracking
+ if self.is_panning {
+ if dt > 1e-5 {
+ let diff_x = self.pan_x - self.last_frame_pan_x;
+ let diff_y = self.pan_y - self.last_frame_pan_y;
+ self.pan_velocity_x = self.pan_velocity_x * 0.4 + (diff_x / dt) * 0.6;
+ self.pan_velocity_y = self.pan_velocity_y * 0.4 + (diff_y / dt) * 0.6;
+ }
+ self.last_frame_pan_x = self.pan_x;
+ self.last_frame_pan_y = self.pan_y;
+ }
+
+ // Trackpad scrolling velocity tracking & timeout detection
+ if self.is_scrolling_trackpad {
+ if now.duration_since(self.last_scroll_time).as_secs_f32() > 0.05 {
+ self.is_scrolling_trackpad = false;
+ } else if dt > 1e-5 {
+ let vel_x = -self.scroll_accum_x / dt;
+ let vel_y = -self.scroll_accum_y / dt;
+ self.pan_velocity_x = self.pan_velocity_x * 0.4 + vel_x * 0.6;
+ self.pan_velocity_y = self.pan_velocity_y * 0.4 + vel_y * 0.6;
+ }
+ self.scroll_accum_x = 0.0;
+ self.scroll_accum_y = 0.0;
+ }
+
+ // Viewport rotation velocity tracking & timeout detection
+ if self.is_rotating_viewport {
+ if now.duration_since(self.last_rotate_time).as_secs_f32() > 0.05 {
+ self.is_rotating_viewport = false;
+ self.scroll_lock = 0;
+ } else if dt > 1e-5 {
+ let vel_yaw = self.rotate_accum_yaw / dt;
+ let vel_pitch = self.rotate_accum_pitch / dt;
+ self.rotate_velocity_yaw = self.rotate_velocity_yaw * 0.4 + vel_yaw * 0.6;
+ self.rotate_velocity_pitch = self.rotate_velocity_pitch * 0.4 + vel_pitch * 0.6;
+ }
+ self.rotate_accum_yaw = 0.0;
+ self.rotate_accum_pitch = 0.0;
+ }
+
+ // Viewport zoom velocity tracking & timeout detection
+ if self.is_zooming_viewport {
+ if now.duration_since(self.last_zoom_time).as_secs_f32() > 0.05 {
+ self.is_zooming_viewport = false;
+ } else if dt > 1e-5 {
+ let vel_zoom = self.zoom_accum / dt;
+ self.zoom_velocity = self.zoom_velocity * 0.4 + vel_zoom * 0.6;
+ }
+ self.zoom_accum = 0.0;
+ }
+
+ let mut tick_changed = false;
+ let ctx = &mut self.ui_context;
+ for w in &mut self.widgets {
+ if w.tick(dt, ctx) {
+ tick_changed = true;
+ }
+ }
+ if clear_ui::widget::hover_animation::tick(dt) {
+ tick_changed = true;
+ }
+ self.update_recent_files_layout();
+
+ // Panning kinetic slide
+ if !self.is_panning && !self.is_scrolling_trackpad && (self.pan_velocity_x.abs() > 0.01 || self.pan_velocity_y.abs() > 0.01) {
+ if !self.inertial_scroll_enabled {
+ self.pan_velocity_x = 0.0;
+ self.pan_velocity_y = 0.0;
+ } else {
+ self.pan_x += self.pan_velocity_x * dt;
+ self.pan_y += self.pan_velocity_y * dt;
+
+ // Apply dynamic friction decay
+ let decay = self.inertial_scroll_friction.powf(dt * 60.0);
+ self.pan_velocity_x *= decay;
+ self.pan_velocity_y *= decay;
+
+ // If velocity magnitude is below 10.0 px/sec, stop completely.
+ if self.pan_velocity_x.hypot(self.pan_velocity_y) < 10.0 {
+ self.pan_velocity_x = 0.0;
+ self.pan_velocity_y = 0.0;
+ }
+
+ self.sync_grid_settings();
+ self.rebuild_positions();
+ self.apply_layout();
+ self.update_panel_bounds();
+ tick_changed = true;
+ }
+ }
+
+ // Viewport rotation kinetic slide
+ if !self.is_rotating_viewport && (self.rotate_velocity_yaw.abs() > 0.001 || self.rotate_velocity_pitch.abs() > 0.001) {
+ let dx = self.rotate_velocity_yaw * dt;
+ let dy = self.rotate_velocity_pitch * dt;
+ if self.active_camera != "Default Camera" {
+ self.update_active_camera_rotation(dx, dy);
+ } else {
+ self.rotation_y += dx;
+ self.rotation_x += dy;
+ }
+
+ // Apply friction decay
+ let friction = 5.0_f32;
+ let decay = (-friction * dt).exp();
+ self.rotate_velocity_yaw *= decay;
+ self.rotate_velocity_pitch *= decay;
+
+ if self.rotate_velocity_yaw.abs() < 0.01 { self.rotate_velocity_yaw = 0.0; }
+ if self.rotate_velocity_pitch.abs() < 0.01 { self.rotate_velocity_pitch = 0.0; }
+ tick_changed = true;
+ }
+
+ // Viewport zoom kinetic slide
+ if !self.is_zooming_viewport && self.zoom_velocity.abs() > 0.001 {
+ let d_zoom = self.zoom_velocity * dt;
+ self.viewport_zoom *= (-d_zoom).exp();
+ self.viewport_zoom = self.viewport_zoom.clamp(0.05, 20.0);
+
+ // Apply friction decay
+ let friction = 5.0_f32;
+ let decay = (-friction * dt).exp();
+ self.zoom_velocity *= decay;
+
+ if self.zoom_velocity.abs() < 0.01 {
+ self.zoom_velocity = 0.0;
+ }
+ tick_changed = true;
+ }
+
+ if tick_changed {
+ self.upload_vertices();
+ }
+
+ let mut panned = false;
+ if let Some(idx) = self.drag_widget {
+ if idx == CONTENT_IDX {
+ let (px, py, pw, ph) = self.positions[CONTENT_IDX];
+ let margin = 30.0_f32;
+ let pan_speed = 5.0_f32;
+
+ if self.cursor_x >= px - 50.0 && self.cursor_x < px + pw + 50.0
+ && self.cursor_y >= py - 50.0 && self.cursor_y < py + ph + 50.0
+ {
+ if self.cursor_x < px + margin {
+ self.pan_x += pan_speed;
+ panned = true;
+ } else if self.cursor_x > px + pw - margin {
+ self.pan_x -= pan_speed;
+ panned = true;
+ }
+
+ if self.cursor_y < py + margin {
+ self.pan_y += pan_speed;
+ panned = true;
+ } else if self.cursor_y > py + ph - margin {
+ self.pan_y -= pan_speed;
+ panned = true;
+ }
+ }
+ }
+ }
+
+ if panned {
+ self.pan_velocity_x = 0.0;
+ self.pan_velocity_y = 0.0;
+ self.is_scrolling_trackpad = false;
+ self.scroll_accum_x = 0.0;
+ self.scroll_accum_y = 0.0;
+ self.sync_grid_settings();
+ if let Some(idx) = self.drag_widget {
+ self.widgets[idx].set_modifiers(self.modifiers.control_key(), self.modifiers.shift_key(), self.modifiers.alt_key());
+ self.widgets[idx].drag_update(self.cursor_x, self.cursor_y);
+ }
+ self.sync_layout();
+ self.read_panel_offsets();
+ self.upload_vertices();
+ }
+
+ self.prepare_text();
+
+ let output = match self.wgpu_adapter.surface.get_current_texture() {
+ Ok(t) => t,
+ Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
+ self.wgpu_adapter.surface.configure(&self.wgpu_adapter.device, &self.wgpu_adapter.config);
+ return false;
+ }
+ Err(wgpu::SurfaceError::Timeout) => return false,
+ Err(e) => { eprintln!("Surface error: {e:?}"); return false; }
+ };
+
+ let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
+ let mut encoder = self.wgpu_adapter.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
+ label: Some("Encoder"),
+ });
+
+ // 3D canvas render pass (background layer)
+ if !self.is_detached_network && self.show_viewport {
+ let cx_logical = 0.0;
+ let cy_logical = HEADER_H;
+ let cw_logical = self.width;
+ let ch_logical = self.body_h();
+ let mut cw = (cw_logical * self.scale as f32) as u32;
+ let mut ch = (ch_logical * self.scale as f32) as u32;
+ let mut sx = (cx_logical * self.scale as f32) as u32;
+ let mut sy = (cy_logical * self.scale as f32) as u32;
+
+ if self.square_viewport {
+ let s = cw.min(ch);
+ sx += (cw - s) / 2;
+ sy += (ch - s) / 2;
+ cw = s;
+ ch = s;
+ }
+
+ if cw > 0 && ch > 0 {
+ let aspect = cw as f32 / ch as f32;
+
+ let proj = Mat4::perspective_rh(0.9, aspect, 0.1, 100.0);
+ let mut camera_pos = Vec3::new(2.5, 1.8, 2.5);
+ let mut rx = 0.0f32;
+ let mut ry = 0.0f32;
+ let mut rz = 0.0f32;
+ let mut pivot = Vec3::ZERO;
+ 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 == "Position" {
+ let parts: Vec<&str> = p.default
+ .split(|c| c == ':' || c == ',' || c == ' ')
+ .filter(|s| !s.is_empty())
+ .collect();
+ if parts.len() >= 3 {
+ if let (Ok(vx), Ok(vy), Ok(vz)) = (parts[0].parse::<f32>(), parts[1].parse::<f32>(), parts[2].parse::<f32>()) {
+ cx = vx;
+ cy = vy;
+ cz = vz;
+ }
+ }
+ } else if p.name == "Rotation" {
+ let parts: Vec<&str> = p.default
+ .split(|c| c == ':' || c == ',' || c == ' ')
+ .filter(|s| !s.is_empty())
+ .collect();
+ if parts.len() >= 3 {
+ if let (Ok(vx), Ok(vy), Ok(vz)) = (parts[0].parse::<f32>(), parts[1].parse::<f32>(), parts[2].parse::<f32>()) {
+ rx = vx;
+ ry = vy;
+ rz = vz;
+ }
+ }
+ } else if p.name == "Pivot" {
+ let parts: Vec<&str> = p.default
+ .split(|c| c == ':' || c == ',' || c == ' ')
+ .filter(|s| !s.is_empty())
+ .collect();
+ if parts.len() >= 3 {
+ if let (Ok(vx), Ok(vy), Ok(vz)) = (parts[0].parse::<f32>(), parts[1].parse::<f32>(), parts[2].parse::<f32>()) {
+ pivot = Vec3::new(vx, vy, vz);
+ }
+ }
+ }
+ }
+ camera_pos = Vec3::new(cx, cy, cz);
+ }
+ }
+
+ let viewport_changed = self.viewport_dirty
+ || self.last_viewport_camera_pos != camera_pos
+ || self.last_viewport_camera_rx != rx
+ || self.last_viewport_camera_ry != ry
+ || self.last_viewport_camera_rz != rz
+ || self.last_viewport_pivot != pivot
+ || self.last_viewport_zoom != self.viewport_zoom
+ || self.last_viewport_rotation_x != self.rotation_x
+ || self.last_viewport_rotation_y != self.rotation_y
+ || self.last_viewport_bg_color != self.viewport_bg_color
+ || self.last_viewport_show_grid != self.show_grid
+ || self.last_viewport_show_cube != self.show_cube
+ || self.last_viewport_show_origin != self.show_origin
+ || self.last_viewport_show_camera_pivot != self.show_camera_pivot
+ || self.last_viewport_width != cw
+ || self.last_viewport_height != ch
+ || self.last_viewport_active_camera != self.active_camera
+ || self.last_viewport_show_viewport != self.show_viewport;
+
+ if viewport_changed {
+ let base_offset = camera_pos - pivot;
+ let distance = base_offset.length();
+ let yaw0 = base_offset.x.atan2(base_offset.z);
+ let pitch0 = (base_offset.y / distance.max(1e-5)).asin();
+
+ let total_ry = ry.to_radians() + yaw0;
+ let total_rx = rx.to_radians() + pitch0;
+
+ let view_rot_pos = Mat4::from_rotation_y(total_ry) * Mat4::from_rotation_x(-total_rx);
+ let camera_up = view_rot_pos.transform_vector3(Vec3::Y);
+ let camera_world_pos = pivot + view_rot_pos.transform_vector3(Vec3::new(0.0, 0.0, distance) * self.viewport_zoom);
+ let view_mat = Mat4::from_rotation_z(rz.to_radians()) * Mat4::look_at_rh(camera_world_pos, pivot, camera_up);
+ let model = Mat4::from_rotation_y(self.rotation_y) * Mat4::from_rotation_x(self.rotation_x);
+ let mvp = proj * view_mat * model;
+ self.wgpu_adapter.queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[mvp.to_cols_array_2d()]));
+
+ let mvp_grid = proj * view_mat * model;
+ self.wgpu_adapter.queue.write_buffer(&self.uniform_buffer_grid, 0, bytemuck::cast_slice(&[mvp_grid.to_cols_array_2d()]));
+
+ let cam_angle_y = camera_pos.x.atan2(camera_pos.z);
+ let rot_angle = if self.active_camera != "Default Camera" {
+ total_ry
+ } else {
+ self.rotation_y + cam_angle_y
+ };
+ let model_pivot = Mat4::from_translation(pivot) * Mat4::from_rotation_y(rot_angle);
+ let mvp_pivot = proj * view_mat * model_pivot;
+ self.wgpu_adapter.queue.write_buffer(&self.uniform_buffer_pivot, 0, bytemuck::cast_slice(&[mvp_pivot.to_cols_array_2d()]));
+
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some("3D Render Pass"),
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+ view: &self.backdrop_texture_view,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Clear(wgpu::Color {
+ r: self.viewport_bg_color[0] as f64,
+ g: self.viewport_bg_color[1] as f64,
+ b: self.viewport_bg_color[2] as f64,
+ a: 1.0,
+ }),
+ store: wgpu::StoreOp::Store,
+ },
+ })],
+ depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
+ view: &self.depth_texture_view,
+ depth_ops: Some(wgpu::Operations {
+ load: wgpu::LoadOp::Clear(1.0),
+ store: wgpu::StoreOp::Discard,
+ }),
+ stencil_ops: None,
+ }),
+ timestamp_writes: None,
+ occlusion_query_set: None,
+ });
+
+ pass.set_scissor_rect(sx, sy, cw, ch);
+ pass.set_pipeline(&self.pipeline_3d);
+
+ if self.show_grid {
+ pass.set_bind_group(0, &self.bind_group_grid, &[]);
+ pass.set_vertex_buffer(0, self.vertex_buffer_grid.slice(..));
+ pass.draw(0..self.vertex_count_grid, 0..1);
+ }
+
+ if self.show_origin {
+ pass.set_bind_group(0, &self.bind_group_3d, &[]);
+ pass.set_vertex_buffer(0, self.vertex_buffer_origin.slice(..));
+ pass.draw(0..self.vertex_count_origin, 0..1);
+ }
+
+ if self.show_camera_pivot {
+ pass.set_bind_group(0, &self.bind_group_pivot, &[]);
+ pass.set_vertex_buffer(0, self.vertex_buffer_pivot.slice(..));
+ pass.draw(0..self.vertex_count_pivot, 0..1);
+ }
+
+ pass.set_bind_group(0, &self.bind_group_3d, &[]);
+
+ if self.show_cube {
+ pass.set_vertex_buffer(0, self.vertex_buffer_3d.slice(..));
+ pass.draw(0..self.vertex_count_3d, 0..1);
+ }
+
+ if self.vertex_count_spheres > 0 {
+ pass.set_vertex_buffer(0, self.vertex_buffer_spheres.slice(..));
+ pass.draw(0..self.vertex_count_spheres, 0..1);
+ }
+
+ // Update viewport cache
+ self.last_viewport_camera_pos = camera_pos;
+ self.last_viewport_camera_rx = rx;
+ self.last_viewport_camera_ry = ry;
+ self.last_viewport_camera_rz = rz;
+ self.last_viewport_pivot = pivot;
+ self.last_viewport_zoom = self.viewport_zoom;
+ self.last_viewport_rotation_x = self.rotation_x;
+ self.last_viewport_rotation_y = self.rotation_y;
+ self.last_viewport_bg_color = self.viewport_bg_color;
+ self.last_viewport_show_grid = self.show_grid;
+ self.last_viewport_show_cube = self.show_cube;
+ self.last_viewport_show_origin = self.show_origin;
+ self.last_viewport_show_camera_pivot = self.show_camera_pivot;
+ self.last_viewport_width = cw;
+ self.last_viewport_height = ch;
+ self.last_viewport_active_camera = self.active_camera.clone();
+ self.last_viewport_show_viewport = self.show_viewport;
+ self.viewport_dirty = false;
+ }
+ } else {
+ let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some("Backdrop Clear Pass"),
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+ view: &self.backdrop_texture_view,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Clear(wgpu::Color {
+ r: self.viewport_bg_color[0] as f64,
+ g: self.viewport_bg_color[1] as f64,
+ b: self.viewport_bg_color[2] as f64,
+ a: 1.0,
+ }),
+ store: wgpu::StoreOp::Store,
+ },
+ })],
+ depth_stencil_attachment: None,
+ timestamp_writes: None,
+ occlusion_query_set: None,
+ });
+ }
+ } else {
+ let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some("Backdrop Clear Pass"),
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+ view: &self.backdrop_texture_view,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }),
+ store: wgpu::StoreOp::Store,
+ },
+ })],
+ depth_stencil_attachment: None,
+ timestamp_writes: None,
+ occlusion_query_set: None,
+ });
+ }
+
+ // Copy backdrop to output swapchain texture
+ if !self.is_detached_network {
+ encoder.copy_texture_to_texture(
+ wgpu::ImageCopyTexture {
+ texture: &self.backdrop_texture,
+ mip_level: 0,
+ origin: wgpu::Origin3d::ZERO,
+ aspect: wgpu::TextureAspect::All,
+ },
+ wgpu::ImageCopyTexture {
+ texture: &output.texture,
+ mip_level: 0,
+ origin: wgpu::Origin3d::ZERO,
+ aspect: wgpu::TextureAspect::All,
+ },
+ wgpu::Extent3d {
+ width: self.physical_width,
+ height: self.physical_height,
+ depth_or_array_layers: 1,
+ },
+ );
+ }
+
+ // UI render pass (foreground layer)
+ {
+ let load_op = if self.is_detached_network {
+ wgpu::LoadOp::Clear(wgpu::Color { r: 0.0, g: 0.0, b: 0.0, a: 0.0 })
+ } else {
+ wgpu::LoadOp::Load
+ };
+
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some("UI Render Pass"),
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+ view: &view,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: load_op,
+ store: wgpu::StoreOp::Store,
+ },
+ })],
+ depth_stencil_attachment: None,
+ timestamp_writes: None,
+ occlusion_query_set: None,
+ });
+
+ pass.set_pipeline(&self.render_pipeline);
+ pass.set_bind_group(0, &self.backdrop_bind_group, &[]);
+ pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
+ pass.draw(0..self.vertex_count, 0..1);
+
+ if self.textured_vertex_count > 0 {
+ // eprintln!("DEBUG_RENDER: textured_vertex_count={}", self.textured_vertex_count);
+ pass.set_pipeline(&self.curved_text_pipeline);
+ pass.set_bind_group(0, &self.curved_text_bind_group, &[]);
+ pass.set_vertex_buffer(0, self.textured_vertex_buffer.slice(..));
+ pass.draw(0..self.textured_vertex_count, 0..1);
+ }
+
+ self.wgpu_adapter.text_renderer.render(&self.wgpu_adapter.text_atlas, &self.wgpu_adapter.text_viewport, &mut pass).unwrap();
+
+ }
+
+ self.wgpu_adapter.queue.submit(std::iter::once(encoder.finish()));
+ output.present();
+ tick_changed || panned
+ }
+}
+
diff --git a/src/geometry.rs b/src/geometry.rs
index 557aca9..976c17a 100644
--- a/src/geometry.rs
+++ b/src/geometry.rs
@@ -1,4 +1,4 @@
-use super::{FsNode, ParamDef};
+use crate::app::{FsNode, ParamDef};
use std::collections::HashMap;
use opencl3::platform::get_platforms;
use opencl3::device::{Device, CL_DEVICE_TYPE_GPU, CL_DEVICE_TYPE_CPU};
@@ -256,6 +256,16 @@ pub fn find_node_by_name<'a>(root: &'a FsNode, name: &str) -> Option<&'a FsNode>
}
pub fn generate_single_node_geometry(root: &FsNode, target: &FsNode, visited: &mut Vec<String>) -> Option<Geometry> {
+ let mut err = None;
+ generate_single_node_geometry_with_errors(root, target, visited, &mut err)
+}
+
+pub fn generate_single_node_geometry_with_errors(
+ root: &FsNode,
+ target: &FsNode,
+ visited: &mut Vec<String>,
+ ocl_error: &mut Option<String>,
+) -> Option<Geometry> {
if visited.contains(&target.name) {
return None;
}
@@ -289,7 +299,9 @@ pub fn generate_single_node_geometry(root: &FsNode, target: &FsNode, visited: &m
}
Some(geom)
} else if target.node_type.eq_ignore_ascii_case("transform") {
- resolve_transform_geometry(root, target, visited)
+ resolve_transform_geometry_with_errors(root, target, visited, ocl_error)
+ } else if target.node_type.eq_ignore_ascii_case("opencl") {
+ resolve_opencl_geometry_with_errors(root, target, visited, ocl_error)
} else {
None
};
@@ -299,12 +311,22 @@ pub fn generate_single_node_geometry(root: &FsNode, target: &FsNode, visited: &m
}
pub fn resolve_transform_geometry(root: &FsNode, target: &FsNode, visited: &mut Vec<String>) -> Option<Geometry> {
+ let mut err = None;
+ resolve_transform_geometry_with_errors(root, target, visited, &mut err)
+}
+
+pub fn resolve_transform_geometry_with_errors(
+ root: &FsNode,
+ target: &FsNode,
+ visited: &mut Vec<String>,
+ ocl_error: &mut Option<String>,
+) -> Option<Geometry> {
let input_name = node_param_str(target, "Input", "");
if input_name.is_empty() {
return None;
}
let input_node = find_node_by_name(root, &input_name)?;
- let mut geom = generate_single_node_geometry(root, input_node, visited)?;
+ let mut geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error)?;
let translation = node_param_vec3(target, "Translation", Vec3::ZERO);
for v in &mut geom.vertices {
v.pos[0] += translation.x;
@@ -314,6 +336,30 @@ pub fn resolve_transform_geometry(root: &FsNode, target: &FsNode, visited: &mut
Some(geom)
}
+pub fn resolve_opencl_geometry_with_errors(
+ root: &FsNode,
+ target: &FsNode,
+ visited: &mut Vec<String>,
+ ocl_error: &mut Option<String>,
+) -> Option<Geometry> {
+ let input_name = node_param_str(target, "Input", "");
+ let mut geom = if !input_name.is_empty() {
+ let input_node = find_node_by_name(root, &input_name)?;
+ generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error)?
+ } else {
+ Geometry::default()
+ };
+ let code = node_param_str(target, "Code", "");
+ if !code.is_empty() {
+ if let Err(e) = run_opencl_kernel(&code, &mut geom) {
+ if ocl_error.is_none() {
+ *ocl_error = Some(e);
+ }
+ }
+ }
+ Some(geom)
+}
+
struct OpenClCache {
device: opencl3::device::Device,
@@ -558,7 +604,12 @@ pub fn run_opencl_kernel(code: &str, geom: &mut Geometry) -> Result<(), String>
}
pub fn network_sphere_vertices(root: &FsNode) -> Geometry {
- fn visit(root: &FsNode, node: &FsNode, count: &mut usize, out: &mut Geometry) {
+ let mut err = None;
+ network_sphere_vertices_with_errors(root, &mut err)
+}
+
+pub fn network_sphere_vertices_with_errors(root: &FsNode, ocl_error: &mut Option<String>) -> Geometry {
+ fn visit(root: &FsNode, node: &FsNode, count: &mut usize, out: &mut Geometry, ocl_error: &mut Option<String>) {
if node.node_type.eq_ignore_ascii_case("sphere") {
let idx = *count;
*count += 1;
@@ -598,20 +649,29 @@ pub fn network_sphere_vertices(root: &FsNode) -> Geometry {
*count += 1;
if node.geometry_visible {
let mut visited = Vec::new();
- if let Some(geom) = resolve_transform_geometry(root, node, &mut visited) {
+ if let Some(geom) = resolve_transform_geometry_with_errors(root, node, &mut visited, ocl_error) {
+ out.merge(geom);
+ }
+ }
+ } else if node.node_type.eq_ignore_ascii_case("opencl") {
+ let idx = *count;
+ *count += 1;
+ if node.geometry_visible {
+ let mut visited = Vec::new();
+ if let Some(geom) = resolve_opencl_geometry_with_errors(root, node, &mut visited, ocl_error) {
out.merge(geom);
}
}
}
for child in &node.children {
- visit(root, child, count, out);
+ visit(root, child, count, out, ocl_error);
}
}
let mut out = Geometry::new();
let mut count = 0;
for child in &root.children {
- visit(root, child, &mut count, &mut out);
+ visit(root, child, &mut count, &mut out, ocl_error);
}
out
}
@@ -622,7 +682,8 @@ pub fn find_sphere_index(root: &FsNode, target: &FsNode) -> Option<usize> {
if node.node_type.eq_ignore_ascii_case("sphere")
|| node.node_type.eq_ignore_ascii_case("line")
|| node.node_type.eq_ignore_ascii_case("add")
- || node.node_type.eq_ignore_ascii_case("transform") {
+ || node.node_type.eq_ignore_ascii_case("transform")
+ || node.node_type.eq_ignore_ascii_case("opencl") {
let idx = *count;
*count += 1;
if is_target {
@@ -1135,6 +1196,90 @@ mod tests {
let geom_loop = resolve_transform_geometry(&root_loop, &transform_loop, &mut visited);
assert!(geom_loop.is_none());
}
+
+ #[test]
+ fn test_opencl_local_node() {
+ if opencl3::platform::get_platforms().unwrap_or_default().is_empty() {
+ println!("Skipping OpenCL local node test: No OpenCL platforms found");
+ return;
+ }
+
+ let sphere = FsNode {
+ name: "Sphere 1".to_string(),
+ node_type: "sphere".to_string(),
+ children: vec![],
+ params: vec![
+ ParamDef {
+ name: "Radius".to_string(),
+ label: String::new(),
+ param_type: "slider".to_string(),
+ default: "0.5".to_string(),
+ options: vec![],
+ min: None,
+ max: None,
+ step: None,
+ }
+ ],
+ geometry_visible: true,
+ position: (0.0, 0.0),
+ };
+
+ let opencl_node = FsNode {
+ name: "OpenCL 1".to_string(),
+ node_type: "opencl".to_string(),
+ children: vec![],
+ params: vec![
+ ParamDef {
+ name: "Input".to_string(),
+ label: String::new(),
+ param_type: "text".to_string(),
+ default: "Sphere 1".to_string(),
+ options: vec![],
+ min: None,
+ max: None,
+ step: None,
+ },
+ ParamDef {
+ name: "Code".to_string(),
+ label: String::new(),
+ param_type: "code".to_string(),
+ default: r#"
+ __kernel void process(__global float* pos, __global float* col, int count) {
+ int id = get_global_id(0);
+ if (id < count) {
+ pos[id * 3 + 1] += 2.0f;
+ }
+ }
+ "#.to_string(),
+ options: vec![],
+ min: None,
+ max: None,
+ step: None,
+ }
+ ],
+ geometry_visible: true,
+ position: (0.0, 0.0),
+ };
+
+ let root = FsNode {
+ name: "root".to_string(),
+ node_type: "node".to_string(),
+ children: vec![sphere, opencl_node.clone()],
+ params: vec![],
+ geometry_visible: true,
+ position: (0.0, 0.0),
+ };
+
+ let mut visited = Vec::new();
+ let mut err = None;
+ let geom = resolve_opencl_geometry_with_errors(&root, &opencl_node, &mut visited, &mut err).unwrap();
+ assert!(!geom.vertices.is_empty());
+ assert!(err.is_none());
+
+ // The sphere should be translated up by 2.0 on the y axis compared to the standard sphere (which centers around y=0.55 for index 0)
+ let avg_y = geom.vertices.iter().map(|v| v.pos[1]).sum::<f32>() / geom.vertices.len() as f32;
+ assert!((avg_y - 2.55).abs() < 0.01);
+ }
}
diff --git a/src/graphics.rs b/src/graphics.rs
new file mode 100644
index 0000000..e40ffaf
--- /dev/null
+++ b/src/graphics.rs
@@ -0,0 +1,27 @@
+use clear_ui::engine::Vertex;
+
+#[repr(C)]
+#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
+pub struct TexturedVertex {
+ pub position: [f32; 2],
+ pub tex_coords: [f32; 2],
+ pub color: [f32; 4],
+ pub clip_circle: [f32; 3],
+}
+
+impl TexturedVertex {
+ const ATTRIBS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
+ 0 => Float32x2,
+ 1 => Float32x2,
+ 2 => Float32x4,
+ 3 => Float32x3,
+ ];
+
+ pub fn desc() -> wgpu::VertexBufferLayout<'static> {
+ wgpu::VertexBufferLayout {
+ array_stride: std::mem::size_of::<TexturedVertex>() as wgpu::BufferAddress,
+ step_mode: wgpu::VertexStepMode::Vertex,
+ attributes: &Self::ATTRIBS,
+ }
+ }
+}
diff --git a/src/main.rs b/src/main.rs
index 63eb850..8fdad18 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -35,7800 +35,27 @@ use wayland_client::{
};
use calloop_wayland_source::WaylandSource;
-mod geometry;
-use geometry::*;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum TouchPhase {
- Started,
- Moved,
- Ended,
- Cancelled,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
-struct ModifiersState {
- ctrl: bool,
- alt: bool,
- shift: bool,
- logo: bool,
-}
-
-impl ModifiersState {
- fn control_key(&self) -> bool { self.ctrl }
- fn alt_key(&self) -> bool { self.alt }
- fn shift_key(&self) -> bool { self.shift }
- fn super_key(&self) -> bool { self.logo }
- fn state(&self) -> Self { *self }
-}
-
-#[derive(Debug, Clone, Copy)]
-struct LocalPosition {
- x: f64,
- y: f64,
-}
-
-enum WindowEvent {
- MouseWheel { delta: clear_ui::widget::MouseScrollDelta, phase: TouchPhase },
- PinchGesture { delta: f64 },
- CursorMoved { position: LocalPosition },
- MouseInput { state: clear_ui::widget::ElementState, button: clear_ui::widget::MouseButton },
- ModifiersChanged(ModifiersState),
- KeyboardInput { event: clear_ui::widget::KeyEvent },
-}
-
use wgpu::util::DeviceExt;
-
-use clear_ui::widget::{Breadcrumb, Canvas, MenuBar, Plate, ParametersBg, Splitter, Spreadsheet, StatusBar, TextLabel, ViewportBg, Element, GraphNode, Graph, Paginator, Button, Checkbox, Slider, Spinbox, ScrollingList, Label, ColorSelector};
+use clear_ui::widget::{Breadcrumb, Canvas, MenuBar, Plate, ParametersBg, Splitter, Spreadsheet, StatusBar, TextLabel, ViewportBg, Element, GraphNode, Graph, Paginator, Button, Checkbox, Slider, Spinbox, ScrollingList, Label, ColorSelector, Switcher};
use clear_ui::colors;
-
-use glyphon::{
- Attrs, Buffer, Cache, FontSystem, Metrics, Resolution, SwashCache, TextArea, TextAtlas,
- TextBounds, TextRenderer, Viewport,
-};
-
+use glyphon::{Attrs, Buffer, Cache, FontSystem, Metrics, Resolution, SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport};
use glam::{Mat4, Vec3};
-const HEADER_IDX: usize = 0;
-const CONTENT_IDX: usize = 1;
-const SPLITTER1_IDX: usize = 2;
-const VIEWPORT_IDX: usize = 3;
-const SPLITTER2_IDX: usize = 4;
-const PARAM_PLATE_IDX: usize = 5;
-const PARAM_IDX: usize = 6;
-const CANVAS_IDX: usize = 7;
-const LEFT_MENUBAR_IDX: usize = 8;
-const RIGHT_MENUBAR_IDX: usize = 9;
-const PARAM_MENUBAR_IDX: usize = 10;
-const STATUS_IDX: usize = 11;
-const BREADCRUMB_IDX: usize = 12;
-const NODE_PALETTE_IDX: usize = 13;
-const SPREADSHEET_IDX: usize = 14;
-const SPREADSHEET_MENUBAR_IDX: usize = 15;
-const NETWORK_PANEL_IDX: usize = 16;
-const PAGINATOR_IDX: usize = 17;
-
-
-const HEADER_H: f32 = 36.0;
-const STATUS_H: f32 = 0.0;
-const MENUBAR_H: f32 = 26.0;
-const SPLITTER_W: f32 = 6.0;
-
-const MIN_COLUMN: f32 = 120.0;
-const BREADCRUMB_H: f32 = 24.0;
-
-#[derive(Clone, Deserialize, Serialize)]
-struct ParamDef {
- name: String,
- #[serde(default)]
- label: String,
- #[serde(rename = "type")]
- #[serde(default = "default_param_type")]
- param_type: String,
- #[serde(default)]
- default: String,
- #[serde(default)]
- options: Vec<String>,
- #[serde(default)]
- min: Option<f32>,
- #[serde(default)]
- max: Option<f32>,
- #[serde(default)]
- step: Option<f32>,
-}
-
-fn default_param_type() -> String { "string".to_string() }
-
-#[derive(Clone, Deserialize, Serialize)]
-struct FsNode {
- name: String,
- #[serde(rename = "type")]
- #[serde(default = "default_node_type")]
- node_type: String,
- #[serde(default)]
- children: Vec<FsNode>,
- #[serde(default)]
- params: Vec<ParamDef>,
- #[serde(skip)]
- #[serde(default = "default_node_geometry_visible")]
- geometry_visible: bool,
- #[serde(default = "default_node_position")]
- position: (f32, f32),
-}
-
-fn default_node_type() -> String { "node".to_string() }
-fn default_node_geometry_visible() -> bool { true }
-fn default_node_position() -> (f32, f32) { (0.0, 0.0) }
-
-#[derive(Clone, Deserialize, Serialize, Default)]
-struct ProjectViewState {
- #[serde(default = "default_camera")]
- active_camera: String,
- #[serde(default)]
- pan: (f32, f32),
- #[serde(default)]
- current_path: Vec<usize>,
- #[serde(default)]
- selected_node: Option<usize>,
-}
-
-fn default_camera() -> String {
- "Default Camera".to_string()
-}
-
-#[derive(Clone, Deserialize, Serialize)]
-struct Project {
- name: String,
- root: FsNode,
- #[serde(default)]
- view_state: ProjectViewState,
-}
-
-#[derive(Debug, Deserialize, Serialize)]
-#[serde(tag = "action", rename_all = "snake_case")]
-enum HttpAction {
- Up,
- Enter { slot: usize },
- SetParam { slot: usize, name: String, value: String },
- ResetCamera,
- Load { path: String },
- Save { path: String },
- ToggleGeometry { slot: usize },
- AddNode { template_name: String, name: Option<String>, x: f32, y: f32 },
- DeleteNode { slot: usize },
- RenameNode { slot: usize, new_name: String },
- MoveNode { slot: usize, x: f32, y: f32 },
- AddParam { slot: usize, name: String, param_type: String, default: String },
- DeleteParam { slot: usize, name: String },
- ToggleCircularPane,
- MenuClick { widget_idx: usize, menu_idx: usize, item_idx: usize },
- MenuClosed { widget_idx: usize, menu_idx: usize },
- SelectPage { page: usize },
-}
-
-#[derive(Debug)]
-enum CustomEvent {
- GetState(std::sync::mpsc::Sender<String>),
- PostAction(HttpAction, std::sync::mpsc::Sender<Result<String, String>>),
-}
-
-#[derive(Clone)]
-struct NodeTemplate {
- label: String,
- node: FsNode,
-}
-
-fn param_display(params: &[ParamDef]) -> Vec<(String, String, String)> {
- params.iter().map(|p| {
- let key = if p.label.is_empty() { &p.name } else { &p.label };
- let value = if p.param_type == "choice" && !p.options.is_empty() && p.default.is_empty() {
- p.options[0].clone()
- } else {
- p.default.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 if p.param_type == "float3" {
- let min = p.min.unwrap_or(-10.0);
- let max = p.max.unwrap_or(10.0);
- format!("float3:{}:{}", min, max)
- } else if p.param_type == "spinbox" {
- let min = p.min.unwrap_or(1.0) as i32;
- let max = p.max.unwrap_or(10000.0) as i32;
- let step = p.step.unwrap_or(1.0) as i32;
- format!("spinbox:{}:{}:{}", min, max, step)
- } else {
- p.param_type.clone()
- };
- (key.clone(), value, ptype)
- }).collect()
-}
-
-fn flatten_node_templates(root: &FsNode) -> Vec<NodeTemplate> {
- fn visit(node: &FsNode, path: &mut Vec<String>, out: &mut Vec<NodeTemplate>) {
- if !node.name.is_empty() {
- path.push(node.name.clone());
- out.push(NodeTemplate { label: path.join(" / "), node: node.clone() });
- }
- for child in &node.children {
- visit(child, path, out);
- }
- if !node.name.is_empty() {
- path.pop();
- }
- }
-
- let mut out = Vec::new();
- let mut path = Vec::new();
- for child in &root.children {
- visit(child, &mut path, &mut out);
- }
- out
-}
-
-fn load_fs_tree() -> FsNode {
- let nodes_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("nodes");
- let mut children = Vec::new();
- if let Ok(entries) = fs::read_dir(&nodes_dir) {
- let mut paths: Vec<_> = entries
- .flatten()
- .map(|e| e.path())
- .filter(|p| p.extension().and_then(|e| e.to_str()) == Some("json"))
- .collect();
- paths.sort();
- for path in paths {
- if let Ok(content) = fs::read_to_string(&path) {
- if let Ok(node) = serde_json::from_str::<FsNode>(&content) {
- children.push(node);
- }
- }
- }
- }
- FsNode {
- name: String::new(),
- node_type: default_node_type(),
- children,
- params: vec![],
- geometry_visible: true,
- position: (0.0, 0.0),
- }
-}
-
-fn default_grid_thickness() -> f32 { 0.03 }
-fn default_show_camera_pivot() -> bool { false }
-fn default_camera_pivot_size() -> f32 { 1.0 }
-fn default_node_color() -> [f32; 3] { [0.10, 0.45, 0.70] }
-fn default_grid_color() -> [f32; 3] { [0.35, 0.35, 0.40] }
-fn default_uniform_background() -> bool { false }
-fn default_network_opacity() -> f32 { 0.95 }
-fn default_cell_color() -> [f32; 3] { [0.13, 0.13, 0.16] }
-fn default_gap_color() -> [f32; 3] { [0.07, 0.07, 0.09] }
-
-#[derive(Serialize, Deserialize, Clone, Debug)]
-struct DesignSettings {
- grid_snap_enabled: bool,
- network_grid_enabled: bool,
- grid_size_x: f32,
- grid_size_y: f32,
- skipped_row_h: f32,
- skipped_col_w: f32,
- show_grid_enabled: bool,
- show_cube_enabled: bool,
- show_origin_enabled: bool,
- origin_size: f32,
- viewport_bg_color: [f32; 3],
- square_viewport: bool,
- #[serde(default = "default_grid_thickness")]
- grid_thickness: f32,
- #[serde(default = "default_show_camera_pivot")]
- show_camera_pivot_enabled: bool,
- #[serde(default = "default_camera_pivot_size")]
- camera_pivot_size: f32,
- #[serde(default = "default_node_color")]
- node_color: [f32; 3],
- #[serde(default = "default_grid_color")]
- grid_color: [f32; 3],
- #[serde(default = "default_uniform_background")]
- uniform_background: bool,
- #[serde(default = "default_network_opacity")]
- network_opacity: f32,
- #[serde(default = "default_cell_color")]
- cell_color: [f32; 3],
- #[serde(default = "default_gap_color")]
- gap_color: [f32; 3],
-}
-
-impl Default for DesignSettings {
- fn default() -> Self {
- Self {
- grid_snap_enabled: true,
- network_grid_enabled: true,
- grid_size_x: 80.0,
- grid_size_y: 40.0,
- skipped_row_h: 20.0,
- skipped_col_w: 20.0,
- show_grid_enabled: true,
- show_cube_enabled: false,
- show_origin_enabled: true,
- origin_size: 1.0,
- viewport_bg_color: [0.05, 0.05, 0.10],
- square_viewport: false,
- grid_thickness: 0.03,
- show_camera_pivot_enabled: false,
- camera_pivot_size: 1.0,
- node_color: default_node_color(),
- grid_color: default_grid_color(),
- uniform_background: false,
- network_opacity: 0.95,
- cell_color: default_cell_color(),
- gap_color: default_gap_color(),
- }
- }
-}
-
-impl DesignSettings {
- fn file_path() -> std::path::PathBuf {
- let home = std::env::var("HOME").unwrap_or_else(|_| "/home/lsgalante".to_string());
- let mut path = std::path::PathBuf::from(home);
- path.push(".config");
- path.push("cce");
- path.push("design.json");
- path
- }
-
- fn load() -> Self {
- let path = Self::file_path();
- if let Ok(content) = fs::read_to_string(&path) {
- if let Ok(settings) = serde_json::from_str::<Self>(&content) {
- return settings;
- }
- }
- Self::default()
- }
-
- fn save(&self) {
- let path = Self::file_path();
- if let Some(parent) = path.parent() {
- let _ = fs::create_dir_all(parent);
- }
- if let Ok(content) = serde_json::to_string_pretty(self) {
- let _ = fs::write(path, content);
- }
- }
-}
-
-struct NodePalette {
- x: f32, y: f32, w: f32, h: f32,
- visible: bool,
- query: String,
- items: Vec<String>,
- selected: usize,
-}
-
-impl NodePalette {
- fn new() -> Self {
- Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, visible: false, query: String::new(), items: Vec::new(), selected: 0 }
- }
-
- fn panel_rect(&self) -> (f32, f32, f32, f32) {
- let pw = 460.0_f32.min(self.w - 40.0).max(280.0);
- let ph = 360.0_f32.min(self.h - 80.0).max(240.0);
- (self.x + (self.w - pw) / 2.0, self.y + 76.0, pw, ph)
- }
-}
-
-impl Element for NodePalette {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
- fn hit_test(&self, px: f32, py: f32, ctx: &clear_ui::context::UiContext) -> bool {
- if ctx.is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
- return false;
- }
- self.visible && px >= self.x && px <= self.x + self.w && py >= self.y && py <= self.y + self.h
- }
- fn set_visible(&mut self, visible: bool) { self.visible = visible; }
- fn visible(&self) -> bool { self.visible }
- fn take_click(&mut self) -> bool { self.visible }
- fn set_palette_state(&mut self, visible: bool, query: &str, items: &[String], selected: usize) {
- self.visible = visible;
- self.query = query.to_string();
- self.items = items.to_vec();
- self.selected = selected;
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- if !self.visible { return Vec::new(); }
- let mut quads = Vec::new();
- quads.push((self.x, self.y, self.w, self.h, [0.0, 0.0, 0.0, 0.45]));
- let (px, py, pw, ph) = self.panel_rect();
- quads.push((px, py, pw, ph, colors::PANEL_MENU_BG));
- quads.push((px + 16.0, py + 48.0, pw - 32.0, 32.0, [0.10, 0.10, 0.14, 1.0]));
- let list_y = py + 92.0;
- let row_h = 24.0;
- let visible_rows = ((ph - 120.0) / row_h).floor().max(0.0) as usize;
- let start = self.selected.saturating_sub(visible_rows.saturating_sub(1));
- for (row, item_idx) in (start..self.items.len().min(start + visible_rows)).enumerate() {
- let y = list_y + row as f32 * row_h;
- let bg = if item_idx == self.selected { [0.24, 0.33, 0.55, 0.85] } else { [0.14, 0.14, 0.19, 0.55] };
- quads.push((px + 16.0, y, pw - 32.0, row_h - 2.0, bg));
- }
- quads
- }
-
- fn text_labels(&self) -> Vec<TextLabel> {
- if !self.visible { return Vec::new(); }
- let (px, py, _pw, ph) = self.panel_rect();
- let mut labels = Vec::new();
- labels.push(TextLabel { text: "Add Node".into(), x: px + 16.0, y: py + 14.0, font_size: 15.0, color: [0xdd, 0xdd, 0xe6] });
- labels.push(TextLabel { text: "Type to search, Enter to place, Esc to close".into(), x: px + 100.0, y: py + 17.0, font_size: 11.0, color: [0x88, 0x88, 0x99] });
- let query = if self.query.is_empty() { "Search nodes...".to_string() } else { self.query.clone() };
- let query_color = if self.query.is_empty() { [0x66, 0x66, 0x77] } else { [0xdd, 0xdd, 0xe6] };
- labels.push(TextLabel { text: query, x: px + 28.0, y: py + 57.0, font_size: 13.0, color: query_color });
- let row_h = 24.0;
- let visible_rows = ((ph - 120.0) / row_h).floor().max(0.0) as usize;
- if self.items.is_empty() {
- labels.push(TextLabel { text: "No matching nodes".into(), x: px + 28.0, y: py + 100.0, font_size: 12.0, color: [0xaa, 0xaa, 0xbb] });
- } else {
- let start = self.selected.saturating_sub(visible_rows.saturating_sub(1));
- for (row, item_idx) in (start..self.items.len().min(start + visible_rows)).enumerate() {
- labels.push(TextLabel {
- text: self.items[item_idx].clone(),
- x: px + 28.0,
- y: py + 98.0 + row as f32 * row_h,
- font_size: 12.0,
- color: if item_idx == self.selected { [0xff, 0xff, 0xdd] } else { [0xcc, 0xcc, 0xd8] },
- });
- }
- }
- labels
- }
-
- fn z_index(&self) -> i32 {
- 200
- }
-}
-
-mod shortcut;
-use shortcut::{Shortcut, ShortcutManager, Action};
-
-use clear_ui::engine::{
- Vertex, push_widget_vertices,
- push_extra_quad_vertices,
- push_extra_quad_vertices_clipped, push_arc_background_vertices,
- quad_vertices_with_clip,
-};
-
-#[repr(C)]
-#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
-struct TexturedVertex {
- position: [f32; 2],
- tex_coords: [f32; 2],
- color: [f32; 4],
- clip_circle: [f32; 3],
-}
-
-impl TexturedVertex {
- const ATTRIBS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
- 0 => Float32x2,
- 1 => Float32x2,
- 2 => Float32x4,
- 3 => Float32x3,
- ];
-
- fn desc() -> wgpu::VertexBufferLayout<'static> {
- wgpu::VertexBufferLayout {
- array_stride: std::mem::size_of::<TexturedVertex>() as wgpu::BufferAddress,
- step_mode: wgpu::VertexStepMode::Vertex,
- attributes: &Self::ATTRIBS,
- }
- }
-}
-
-
-fn make_text_buffer(font_system: &mut FontSystem, text: &str, size: f32) -> Buffer {
- let metrics = Metrics::new(size, size * 1.4);
- let mut buffer = Buffer::new(font_system, metrics);
- buffer.set_text(font_system, text, Attrs::new(), glyphon::Shaping::Advanced);
- buffer.shape_until_scroll(font_system, true);
- buffer
-}
-
-fn make_text_buffer_with_font(font_system: &mut FontSystem, text: &str, size: f32, font: Option<&str>) -> Buffer {
- let metrics = Metrics::new(size, size * 1.4);
- let mut buffer = Buffer::new(font_system, metrics);
- let mut attrs = Attrs::new();
- let family_name = font.map(|f| clear_ui::layout::parse_font_string(f).0);
- if let Some(ref name) = family_name {
- let family = match name.as_str() {
- "monospace" => glyphon::Family::Monospace,
- "sans-serif" => glyphon::Family::SansSerif,
- "serif" => glyphon::Family::Serif,
- _ => glyphon::Family::Name(name),
- };
- attrs = attrs.family(family);
- }
- buffer.set_text(font_system, text, attrs, glyphon::Shaping::Advanced);
- buffer.shape_until_scroll(font_system, true);
- buffer
-}
-
-fn get_next_visible_pane(
- current_pane: usize,
- show_network: bool,
- show_viewport: bool,
- show_parameters: bool,
- show_spreadsheet: bool,
- shift_pressed: bool,
-) -> usize {
- let mut visible_panes = Vec::new();
- if show_network {
- visible_panes.push(LEFT_MENUBAR_IDX);
- }
- if show_viewport {
- visible_panes.push(RIGHT_MENUBAR_IDX);
- }
- if show_parameters {
- visible_panes.push(PARAM_MENUBAR_IDX);
- }
- if show_spreadsheet {
- visible_panes.push(SPREADSHEET_MENUBAR_IDX);
- }
- if visible_panes.is_empty() {
- return LEFT_MENUBAR_IDX;
- }
-
- let current_pos = visible_panes.iter().position(|&x| x == current_pane).unwrap_or(0);
- let next_pos = if shift_pressed {
- (current_pos + visible_panes.len() - 1) % visible_panes.len()
- } else {
- (current_pos + 1) % visible_panes.len()
- };
- visible_panes[next_pos]
-}
-
-fn push_circle_vertices(
- cx: f32, cy: f32, r: f32,
- sw: f32, sh: f32,
- color: [f32; 4],
- segments: usize,
- clip_circle: [f32; 3],
- out: &mut Vec<Vertex>,
-) {
- for i in 0..segments {
- let theta1 = (i as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
- let theta2 = ((i + 1) as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
- let x0 = cx;
- let y0 = cy;
- let x1 = cx + r * theta1.cos();
- let y1 = cy + r * theta1.sin();
- let x2 = cx + r * theta2.cos();
- let y2 = cy + r * theta2.sin();
-
- let ndc_x0 = (x0 / sw) * 2.0 - 1.0;
- let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
- let ndc_x1 = (x1 / sw) * 2.0 - 1.0;
- let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
- let ndc_x2 = (x2 / sw) * 2.0 - 1.0;
- let ndc_y2 = 1.0 - (y2 / sh) * 2.0;
-
- out.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
- out.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
- out.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
- }
-}
-
-fn push_circle_border_vertices(
- cx: f32, cy: f32, r: f32,
- thickness: f32,
- sw: f32, sh: f32,
- color: [f32; 4],
- segments: usize,
- clip_circle: [f32; 3],
- out: &mut Vec<Vertex>,
-) {
- for i in 0..segments {
- let theta1 = (i as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
- let theta2 = ((i + 1) as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
-
- let x0 = cx + (r - thickness) * theta1.cos();
- let y0 = cy + (r - thickness) * theta1.sin();
- let x1 = cx + r * theta1.cos();
- let y1 = cy + r * theta1.sin();
-
- let x2 = cx + r * theta2.cos();
- let y2 = cy + r * theta2.sin();
- let x3 = cx + (r - thickness) * theta2.cos();
- let y3 = cy + (r - thickness) * theta2.sin();
-
- let ndc_x0 = (x0 / sw) * 2.0 - 1.0; let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
- let ndc_x1 = (x1 / sw) * 2.0 - 1.0; let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
- let ndc_x2 = (x2 / sw) * 2.0 - 1.0; let ndc_y2 = 1.0 - (y2 / sh) * 2.0;
- let ndc_x3 = (x3 / sw) * 2.0 - 1.0; let ndc_y3 = 1.0 - (y3 / sh) * 2.0;
-
- out.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
- out.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
- out.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
-
- out.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
- out.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
- out.push(Vertex { position: [ndc_x3, ndc_y3], color, clip_circle });
- }
-}
-
-fn push_quad_vertices_clipped(
- x: f32, y: f32, w: f32, h: f32,
- surface_w: f32, surface_h: f32,
- color: [f32; 4],
- clip: (f32, f32, f32, f32),
- clip_circle: [f32; 3],
- out: &mut Vec<Vertex>,
-) {
- let (cx0, cy0, cx1, cy1) = clip;
- let ix0 = x.max(cx0);
- let iy0 = y.max(cy0);
- let ix1 = (x + w).min(cx1);
- let iy1 = (y + h).min(cy1);
- if ix1 <= ix0 || iy1 <= iy0 {
- return;
- }
- out.extend_from_slice(&quad_vertices_with_clip(ix0, iy0, ix1 - ix0, iy1 - iy0, surface_w, surface_h, color, clip_circle));
-}
-
-#[derive(Clone, Copy, Debug, PartialEq)]
-struct ResizeDirection {
- left: bool,
- right: bool,
- top: bool,
- bottom: bool,
-}
-
-struct State {
- wgpu_adapter: clear_ui::backend::WgpuAdapter,
- render_pipeline: wgpu::RenderPipeline,
- vertex_buffer: wgpu::Buffer,
- window: XdgWindow,
- wl_surface: wl_surface::WlSurface,
- vertex_count: u32,
- vertex_data: Vec<Vertex>,
-
- pipeline_3d: wgpu::RenderPipeline,
- bind_group_3d: wgpu::BindGroup,
- bind_group_layout_3d: wgpu::BindGroupLayout,
- uniform_buffer: wgpu::Buffer,
- bind_group_grid: wgpu::BindGroup,
- uniform_buffer_grid: wgpu::Buffer,
- bind_group_pivot: wgpu::BindGroup,
- uniform_buffer_pivot: wgpu::Buffer,
- vertex_buffer_3d: wgpu::Buffer,
- vertex_count_3d: u32,
- vertex_buffer_spheres: wgpu::Buffer,
- vertex_count_spheres: u32,
- vertex_buffer_grid: wgpu::Buffer,
- vertex_count_grid: u32,
- depth_texture: wgpu::Texture,
- depth_texture_view: wgpu::TextureView,
- backdrop_texture: wgpu::Texture,
- backdrop_texture_view: wgpu::TextureView,
- backdrop_sampler: wgpu::Sampler,
- backdrop_bind_group_layout: wgpu::BindGroupLayout,
- backdrop_bind_group: wgpu::BindGroup,
- rotation_y: f32,
- rotation_x: f32,
- is_rotating_viewport: bool,
- scroll_lock: u8, // 0 = None, 1 = Horizontal, 2 = Vertical
- last_rotate_time: Instant,
- rotate_accum_yaw: f32,
- rotate_accum_pitch: f32,
- rotate_velocity_yaw: f32,
- rotate_velocity_pitch: f32,
- show_grid: bool,
- show_cube: bool,
- show_origin: bool,
- show_camera_pivot: bool,
- viewport_bg_color: [f32; 3],
- node_color: [f32; 3],
- grid_color: [f32; 3],
- cell_color: [f32; 3],
- gap_color: [f32; 3],
- vertex_buffer_origin: wgpu::Buffer,
- vertex_count_origin: u32,
- vertex_buffer_pivot: wgpu::Buffer,
- vertex_count_pivot: u32,
- origin_size: f32,
- camera_pivot_size: f32,
-
- fs_root: FsNode,
- node_templates: Vec<NodeTemplate>,
- current_path: Vec<usize>,
- last_click: Option<(Instant, usize)>,
- last_frame: Instant,
-
- shortcut_manager: ShortcutManager,
- pending_action: Option<Action>,
- exit_requested: bool,
-
- widgets: Vec<Box<dyn Element>>,
- positions: Vec<(f32, f32, f32, f32)>,
- splitter_layout: clear_ui::layout::SplitterLayout,
- node_palette_visible: bool,
- node_palette_query: String,
- node_palette_filtered: Vec<usize>,
- node_palette_selected: usize,
-
- curved_text_texture: wgpu::Texture,
- curved_text_texture_view: wgpu::TextureView,
- curved_text_sampler: wgpu::Sampler,
- curved_text_bind_group: wgpu::BindGroup,
- curved_text_pipeline: wgpu::RenderPipeline,
- curved_text_atlas: TextAtlas,
- curved_text_renderer: TextRenderer,
- curved_text_viewport: Viewport,
- textured_vertex_buffer: wgpu::Buffer,
- textured_vertex_count: u32,
-
-
- drag_widget: Option<usize>,
- focused_widget: Option<usize>,
-
- cursor_x: f32,
- cursor_y: f32,
- grid_cursor_col: i32,
- grid_cursor_row: i32,
- modifiers: ModifiersState,
-
- width: f32,
- height: f32,
- physical_width: u32,
- physical_height: u32,
- scale: f64,
- square_viewport: bool,
- grid_snap_enabled: bool,
- network_grid_visible: bool,
- grid_size_x: f32,
- grid_size_y: f32,
- skipped_row_h: f32,
- skipped_col_w: f32,
-
- pan_x: f32,
- pan_y: f32,
- pan_velocity_x: f32,
- pan_velocity_y: f32,
- last_frame_pan_x: f32,
- last_frame_pan_y: f32,
- is_panning: bool,
- pan_start_cx: f32,
- pan_start_cy: f32,
- pan_start_x: f32,
- pan_start_y: f32,
- space_pressed: bool,
- active_camera: String,
- show_network: bool,
- show_viewport: bool,
- show_parameters: bool,
- show_spreadsheet: bool,
- is_scrolling_trackpad: bool,
- last_scroll_time: Instant,
- scroll_accum_x: f32,
- scroll_accum_y: f32,
- last_spreadsheet_node_name: Option<String>,
- last_spreadsheet_node_params: Option<Vec<(String, String)>>,
- viewport_zoom: f32,
- is_zooming_viewport: bool,
- last_zoom_time: Instant,
- zoom_accum: f32,
- zoom_velocity: f32,
- grid_thickness: f32,
- focused_pane: usize,
- inertial_scroll_enabled: bool,
- inertial_scroll_friction: f32,
- scroll_speed: f32,
- last_config_read: Instant,
- circular_network_pane: bool,
- circular_network_layout: clear_ui::layout::CircularPaneLayout,
- is_detached_network: bool,
- detached_circular_network: bool,
- last_project_mod_time: Option<std::time::SystemTime>,
- last_project_check: std::time::Instant,
- last_inspector_check: std::time::Instant,
- last_inspector_update: std::time::Instant,
- needs_autosave: bool,
- last_autosave_time: std::time::Instant,
- pub window_x: i32,
- pub window_y: i32,
- pub active_menu_cloud_pid: Option<u32>,
- pub active_menu_cloud_idx: Option<(usize, usize)>,
- uniform_background: bool,
- network_opacity: f32,
- last_design_mod_time: Option<std::time::SystemTime>,
- last_config_mod_time: Option<std::time::SystemTime>,
- floating_network_layout: (f32, f32, f32, f32),
- is_resizing_network: Option<ResizeDirection>,
- drag_start_rect: (f32, f32, f32, f32),
- drag_start_mouse: (f32, f32),
- floating_param_width: f32,
- is_resizing_param: bool,
- drag_start_param_w: f32,
- floating_spreadsheet_height: f32,
- is_resizing_spreadsheet: bool,
- drag_start_spreadsheet_h: f32,
- paginator_page_widgets: Vec<Vec<Box<dyn Element>>>,
- last_paginator_menubar: Option<usize>,
- loaded_project_path: Option<std::path::PathBuf>,
- last_saved_root_json: String,
- recent_files: Vec<std::path::PathBuf>,
- recent_files_list: ScrollingList,
- recent_files_buttons: Vec<Button>,
- ui_context: clear_ui::context::UiContext,
-}
-
-impl State {
- fn has_unsaved_changes(&self) -> bool {
- if let Ok(current_json) = serde_json::to_string(&self.fs_root) {
- current_json != self.last_saved_root_json
- } else {
- false
- }
- }
-
- fn update_window_title(&self) {
- let base_title = if self.is_detached_network {
- "Network Pane"
- } else {
- "Clear Design Interface"
- };
-
- let mut title = base_title.to_string();
-
- if let Some(path) = &self.loaded_project_path {
- if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
- title.push_str(" - ");
- title.push_str(filename);
- }
- }
-
- if self.has_unsaved_changes() {
- title.push_str("*");
- }
-
- self.window.set_title(&title);
- }
-
- fn get_recent_files_path() -> Option<std::path::PathBuf> {
- std::env::var("HOME").ok().map(|h| {
- let mut path = std::path::PathBuf::from(h);
- path.push(".config");
- path.push("cce-design-interface");
- path.push("recent_files.json");
- path
- })
- }
-
- fn load_recent_files() -> Vec<std::path::PathBuf> {
- if let Some(path) = Self::get_recent_files_path() {
- if let Ok(file) = std::fs::File::open(path) {
- if let Ok(list) = serde_json::from_reader::<_, Vec<String>>(file) {
- return list.into_iter().map(std::path::PathBuf::from).collect();
- }
- }
- }
- Vec::new()
- }
-
- fn save_recent_files(files: &[std::path::PathBuf]) {
- if let Some(path) = Self::get_recent_files_path() {
- if let Some(parent) = path.parent() {
- let _ = std::fs::create_dir_all(parent);
- }
- if let Ok(file) = std::fs::File::create(path) {
- let list: Vec<String> = files.iter().map(|p| p.to_string_lossy().to_string()).collect();
- let _ = serde_json::to_writer_pretty(file, &list);
- }
- }
- }
-
- fn add_recent_file(&mut self, path: std::path::PathBuf) {
- let abs_path = std::fs::canonicalize(&path).unwrap_or(path);
- self.recent_files.retain(|p| p != &abs_path);
- self.recent_files.insert(0, abs_path);
- self.recent_files.truncate(10);
- Self::save_recent_files(&self.recent_files);
- self.rebuild_recent_buttons();
- self.update_paginator();
- }
-
- fn rebuild_recent_buttons(&mut self) {
- self.recent_files_buttons.clear();
- for file in &self.recent_files {
- let label = file.file_name()
- .map(|n| n.to_string_lossy().to_string())
- .unwrap_or_else(|| file.to_string_lossy().to_string());
- let btn = Button::new_list_row(0.0, 0.0, 0.0, 0.0).with_label(&label);
- self.recent_files_buttons.push(btn);
- }
- }
-
- fn update_recent_files_layout(&mut self) {
- let active_menubar = self.focused_pane;
- let menu_names = self.widgets[active_menubar].menu_names();
- let file_page_idx = match menu_names.iter().position(|name| name == "File") {
- Some(idx) => idx,
- None => return,
- };
-
- if file_page_idx >= self.paginator_page_widgets.len() {
- return;
- }
-
- let page = &mut self.paginator_page_widgets[file_page_idx];
- let menu_items = self.widgets[active_menubar].menu_items_list();
- if file_page_idx >= menu_items.len() {
- return;
- }
- let num_base_items = menu_items[file_page_idx].len();
-
- let list_idx = num_base_items + 1;
- if list_idx >= page.len() {
- return;
- }
-
- let (list_x, list_y, list_w, list_h) = page[list_idx].rect();
- if list_h <= 0.0 {
- return;
- }
-
- let count = self.recent_files.len();
- page[list_idx].update_bounds(count, list_y, list_h);
-
- let btn_h = 22.0;
- let inner_x = list_x + 4.0;
- let inner_w = list_w - 16.0;
-
- for (idx, btn) in self.recent_files_buttons.iter_mut().enumerate() {
- if let Some(draw_y) = page[list_idx].get_item_draw_y(idx, 0.0) {
- btn.set_rect(inner_x, draw_y, inner_w, btn_h);
- } else {
- btn.set_rect(-9999.0, -9999.0, 0.0, 0.0);
- }
- }
- }
-
- fn save_settings(&mut self) {
- let settings = DesignSettings {
- grid_snap_enabled: self.grid_snap_enabled,
- network_grid_enabled: self.network_grid_visible,
- grid_size_x: self.grid_size_x,
- grid_size_y: self.grid_size_y,
- skipped_row_h: self.skipped_row_h,
- skipped_col_w: self.skipped_col_w,
- show_grid_enabled: self.show_grid,
- show_cube_enabled: self.show_cube,
- show_origin_enabled: self.show_origin,
- origin_size: self.origin_size,
- viewport_bg_color: self.viewport_bg_color,
- node_color: self.node_color,
- grid_color: self.grid_color,
- cell_color: self.cell_color,
- gap_color: self.gap_color,
- square_viewport: self.square_viewport,
- grid_thickness: self.grid_thickness,
- show_camera_pivot_enabled: self.show_camera_pivot,
- camera_pivot_size: self.camera_pivot_size,
- uniform_background: self.uniform_background,
- network_opacity: self.network_opacity,
- };
- settings.save();
- self.last_design_mod_time = {
- let design_path = DesignSettings::file_path();
- std::fs::metadata(&design_path).and_then(|m| m.modified()).ok()
- };
- }
-
- fn sync_settings_from_paginator(&mut self) {
- let active_menubar = self.focused_pane;
- let page_idx = if active_menubar == LEFT_MENUBAR_IDX {
- 3
- } else if active_menubar == RIGHT_MENUBAR_IDX {
- 4
- } else {
- return;
- };
-
- if page_idx >= self.paginator_page_widgets.len() {
- return;
- }
-
- let mut left_values = None;
- let mut right_values = None;
-
- {
- let widgets = &self.paginator_page_widgets[page_idx];
- if widgets.is_empty() {
- return;
- }
-
- if active_menubar == LEFT_MENUBAR_IDX {
- if widgets.len() >= 11 {
- let snap = widgets[0].value();
- let vis = widgets[1].value();
- let gx = widgets[2].value();
- let gy = widgets[3].value();
- let srh = widgets[4].value();
- let scw = widgets[5].value();
- let uni = widgets[6].value();
- let op = widgets[7].value();
- let node_col = widgets[8].color_u8();
- let cell_col = widgets[9].color_u8();
- let gap_col = widgets[10].color_u8();
- left_values = Some((snap, vis, gx, gy, srh, scw, uni, op, node_col, cell_col, gap_col));
- }
- } else if active_menubar == RIGHT_MENUBAR_IDX {
- if widgets.len() >= 13 {
- let sg = widgets[0].value();
- let sc = widgets[1].value();
- let so = widgets[2].value();
- let cp = widgets[3].value();
- let gt = widgets[4].value();
- let os = widgets[5].value();
- let cps = widgets[6].value();
- let bgr = widgets[7].value();
- let bgg = widgets[8].value();
- let bgb = widgets[9].value();
- let gcr = widgets[10].value();
- let gcg = widgets[11].value();
- let gcb = widgets[12].value();
- right_values = Some((sg, sc, so, cp, gt, os, cps, bgr, bgg, bgb, gcr, gcg, gcb));
- }
- }
- }
-
- let mut changed = false;
-
- if let Some((snap, vis, gx, gy, srh, scw, uni, op, node_col, cell_col, gap_col)) = left_values {
- // LEFT pane Settings
- // 0: Snap to Grid (Checkbox)
- let snap = snap == 1;
- if self.grid_snap_enabled != snap {
- self.grid_snap_enabled = snap;
- changed = true;
- }
- // 1: Grid Visible (Checkbox)
- let vis = vis == 1;
- if self.network_grid_visible != vis {
- self.network_grid_visible = vis;
- changed = true;
- }
- // 2: Grid X (Spinbox)
- let gx = gx as f32;
- if self.grid_size_x != gx {
- self.grid_size_x = gx;
- changed = true;
- }
- // 3: Grid Y (Spinbox)
- let gy = gy as f32;
- if self.grid_size_y != gy {
- self.grid_size_y = gy;
- changed = true;
- }
- // 4: Skipped Row H (Spinbox)
- let srh = srh as f32;
- if self.skipped_row_h != srh {
- self.skipped_row_h = srh;
- changed = true;
- }
- // 5: Skipped Col W (Spinbox)
- let scw = scw as f32;
- if self.skipped_col_w != scw {
- self.skipped_col_w = scw;
- changed = true;
- }
- // 6: Uniform Background (Checkbox)
- let uni = uni == 1;
- if self.uniform_background != uni {
- self.uniform_background = uni;
- changed = true;
- }
- // 7: Opacity (Slider, 0..100)
- let op = op as f32 / 100.0;
- if (self.network_opacity - op).abs() > 0.001 {
- self.network_opacity = op;
- changed = true;
- }
- // 8: Node Color (ColorSelector)
- if let Some(col) = node_col {
- let nr = col[0] as f32 / 255.0;
- let ng = col[1] as f32 / 255.0;
- let nb = col[2] as f32 / 255.0;
- if (self.node_color[0] - nr).abs() > 0.001
- || (self.node_color[1] - ng).abs() > 0.001
- || (self.node_color[2] - nb).abs() > 0.001
- {
- self.node_color = [nr, ng, nb];
- colors::set_node_color([nr, ng, nb, 1.0]);
- changed = true;
- }
- }
- // 9: Cell Color (ColorSelector)
- if let Some(col) = cell_col {
- let cr = col[0] as f32 / 255.0;
- let cg = col[1] as f32 / 255.0;
- let cb = col[2] as f32 / 255.0;
- if (self.cell_color[0] - cr).abs() > 0.001
- || (self.cell_color[1] - cg).abs() > 0.001
- || (self.cell_color[2] - cb).abs() > 0.001
- {
- self.cell_color = [cr, cg, cb];
- changed = true;
- }
- }
- // 10: Gap Color (ColorSelector)
- if let Some(col) = gap_col {
- let gr = col[0] as f32 / 255.0;
- let gg = col[1] as f32 / 255.0;
- let gb = col[2] as f32 / 255.0;
- if (self.gap_color[0] - gr).abs() > 0.001
- || (self.gap_color[1] - gg).abs() > 0.001
- || (self.gap_color[2] - gb).abs() > 0.001
- {
- self.gap_color = [gr, gg, gb];
- changed = true;
- }
- }
- } else if let Some((sg, sc, so, cp, gt, os, cps, bgr, bgg, bgb, gcr, gcg, gcb)) = right_values {
- // RIGHT pane Settings
- // 0: Show Grid Guide (Checkbox)
- let sg = sg == 1;
- if self.show_grid != sg {
- self.show_grid = sg;
- self.widgets[RIGHT_MENUBAR_IDX].set_item_checked(2, 0, sg);
- changed = true;
- }
- // 1: Show Reference Cube (Checkbox)
- let sc = sc == 1;
- if self.show_cube != sc {
- self.show_cube = sc;
- self.widgets[RIGHT_MENUBAR_IDX].set_item_checked(2, 1, sc);
- changed = true;
- }
- // 2: Show Origin Axes (Checkbox)
- let so = so == 1;
- if self.show_origin != so {
- self.show_origin = so;
- self.widgets[RIGHT_MENUBAR_IDX].set_item_checked(2, 2, so);
- changed = true;
- }
- // 3: Show Camera Pivot (Checkbox)
- let cp = cp == 1;
- if self.show_camera_pivot != cp {
- self.show_camera_pivot = cp;
- self.widgets[RIGHT_MENUBAR_IDX].set_item_checked(2, 3, cp);
- changed = true;
- }
- // 4: Grid Thickness (Spinbox, decimals 3)
- let gt = gt as f32 / 1000.0;
- if (self.grid_thickness - gt).abs() > 0.0001 {
- self.grid_thickness = gt;
- self.update_grid_geometry();
- changed = true;
- }
- // 5: Origin Guide Size (Spinbox, decimals 1)
- let os = os as f32 / 10.0;
- if (self.origin_size - os).abs() > 0.001 {
- self.origin_size = os;
- self.update_origin_geometry();
- changed = true;
- }
- // 6: Camera Pivot Size (Spinbox, decimals 1)
- let cps = cps as f32 / 10.0;
- if (self.camera_pivot_size - cps).abs() > 0.001 {
- self.camera_pivot_size = cps;
- self.update_pivot_geometry();
- changed = true;
- }
- // 7, 8, 9: BG Color (R, G, B Spinboxes, 0..255)
- let bgr = bgr as f32 / 255.0;
- let bgg = bgg as f32 / 255.0;
- let bgb = bgb as f32 / 255.0;
- if (self.viewport_bg_color[0] - bgr).abs() > 0.001
- || (self.viewport_bg_color[1] - bgg).abs() > 0.001
- || (self.viewport_bg_color[2] - bgb).abs() > 0.001
- {
- self.viewport_bg_color = [bgr, bgg, bgb];
- changed = true;
- }
- // 10, 11, 12: Grid Color (R, G, B Spinboxes, 0..255)
- let gcr = gcr as f32 / 255.0;
- let gcg = gcg as f32 / 255.0;
- let gcb = gcb as f32 / 255.0;
- if (self.grid_color[0] - gcr).abs() > 0.001
- || (self.grid_color[1] - gcg).abs() > 0.001
- || (self.grid_color[2] - gcb).abs() > 0.001
- {
- self.grid_color = [gcr, gcg, gcb];
- self.update_grid_geometry();
- changed = true;
- }
- }
-
- if changed {
- self.sync_grid_settings();
- self.save_settings();
- self.upload_vertices();
- }
- }
-
- fn update_grid_geometry(&mut self) {
- let grid_verts = grid_vertices(self.grid_thickness, self.grid_color);
- self.wgpu_adapter.queue.write_buffer(&self.vertex_buffer_grid, 0, bytemuck::cast_slice(&grid_verts));
- }
-
- fn update_origin_geometry(&mut self) {
- let origin_verts = origin_vectors_vertices(self.origin_size);
- self.wgpu_adapter.queue.write_buffer(&self.vertex_buffer_origin, 0, bytemuck::cast_slice(&origin_verts));
- }
-
- fn update_pivot_geometry(&mut self) {
- let pivot_verts = camera_pivot_vertices(self.camera_pivot_size);
- self.wgpu_adapter.queue.write_buffer(&self.vertex_buffer_pivot, 0, bytemuck::cast_slice(&pivot_verts));
- }
-
- fn body_h(&self) -> f32 { self.height - HEADER_H - STATUS_H }
-
- fn get_col_geometries(&self) -> (f32, f32, f32, f32, f32, f32) {
- let left_visible = self.show_network && !self.circular_network_pane && !self.is_detached_network && !self.detached_circular_network;
- let center_visible = self.show_viewport || self.show_spreadsheet;
- let right_visible = self.show_parameters;
-
- let (col_l_x, col_l_w, col_c_x, col_c_w, col_r_x, col_r_w) =
- match (left_visible, center_visible, right_visible) {
- (true, true, true) => {
- let l_w = self.splitter_layout.splitter1_x;
- let c_x = l_w + SPLITTER_W;
- let c_w = self.splitter_layout.splitter2_x - c_x;
- let r_x = self.splitter_layout.splitter2_x + SPLITTER_W;
- let r_w = (self.width - r_x).max(0.0);
- (0.0, l_w, c_x, c_w, r_x, r_w)
- }
- (true, true, false) => {
- let l_w = self.splitter_layout.splitter1_x;
- let c_x = l_w + SPLITTER_W;
- let c_w = (self.width - c_x).max(0.0);
- (0.0, l_w, c_x, c_w, 0.0, 0.0)
- }
- (true, false, true) => {
- let l_w = self.splitter_layout.splitter2_x;
- let r_x = l_w + SPLITTER_W;
- let r_w = (self.width - r_x).max(0.0);
- (0.0, l_w, 0.0, 0.0, r_x, r_w)
- }
- (false, true, true) => {
- let c_w = self.splitter_layout.splitter2_x;
- let r_x = c_w + SPLITTER_W;
- let r_w = (self.width - r_x).max(0.0);
- (0.0, 0.0, 0.0, c_w, r_x, r_w)
- }
- (true, false, false) => {
- (0.0, self.width, 0.0, 0.0, 0.0, 0.0)
- }
- (false, true, false) => {
- (0.0, 0.0, 0.0, self.width, 0.0, 0.0)
- }
- (false, false, true) => {
- (0.0, 0.0, 0.0, 0.0, 0.0, self.width)
- }
- (false, false, false) => {
- (0.0, 0.0, 0.0, self.width, 0.0, 0.0)
- }
- };
- (col_l_x, col_l_w, col_c_x, col_c_w, col_r_x, col_r_w)
- }
-
- fn content_left_w(&self) -> f32 { self.get_col_geometries().1 }
-
- fn content_right_x(&self) -> f32 { self.get_col_geometries().2 }
-
- fn viewport_w(&self) -> f32 { self.get_col_geometries().3 }
-
- fn param_x(&self) -> f32 { self.get_col_geometries().4 }
-
- fn param_w(&self) -> f32 { self.get_col_geometries().5 }
-
- fn in_network_pane(&self) -> bool {
- if self.circular_network_pane {
- self.circular_network_layout.hit_test_content(self.cursor_x, self.cursor_y, 0.0, BREADCRUMB_H)
- } else {
- let (cx, cy, cw, ch) = self.positions[CONTENT_IDX];
- self.cursor_x >= cx
- && self.cursor_x < cx + cw
- && self.cursor_y >= cy
- && self.cursor_y < cy + ch
- }
- }
-
- fn clamp_splitters(&mut self) {
- if self.is_detached_network {
- return;
- }
- self.splitter_layout.clamp(self.width, self.detached_circular_network);
- }
-
- fn current_dir(&self) -> &FsNode {
- let mut node = &self.fs_root;
- for &i in &self.current_path {
- node = &node.children[i];
- }
- node
- }
-
- fn current_dir_mut(&mut self) -> &mut FsNode {
- let mut node = &mut self.fs_root;
- for &i in &self.current_path {
- node = &mut node.children[i];
- }
- node
- }
-
- fn sync_parameters_to_project(&mut self) {
- if !self.is_detached_network {
- if let Some(slot_idx) = self.widgets[CONTENT_IDX].selected_node() {
- let updated_params = self.widgets[PARAM_IDX].node_params();
- let dir = self.current_dir_mut();
- if let Some(child) = dir.children.get_mut(slot_idx) {
- let mut param_changed = false;
- for (u_name, u_val, _) in &updated_params {
- if let Some(p) = child.params.iter_mut().find(|p| p.name == *u_name) {
- if p.default != *u_val {
- p.default = u_val.clone();
- param_changed = true;
- }
- }
- }
- if param_changed {
- self.rebuild_scene_geometry();
- self.sync_nodes();
- }
- }
- }
- }
- }
-
- fn current_path_names(&self) -> Vec<String> {
- let mut node = &self.fs_root;
- let mut names = Vec::new();
- for &i in &self.current_path {
- if let Some(child) = node.children.get(i) {
- names.push(child.name.clone());
- node = child;
- }
- }
- names
- }
-
- fn refresh_node_palette(&mut self) {
- let q = self.node_palette_query.to_lowercase();
- self.node_palette_filtered = self.node_templates.iter().enumerate()
- .filter_map(|(i, t)| {
- if q.is_empty() || t.label.to_lowercase().contains(&q) { Some(i) } else { None }
- })
- .collect();
- if self.node_palette_selected >= self.node_palette_filtered.len() {
- self.node_palette_selected = self.node_palette_filtered.len().saturating_sub(1);
- }
- let items: Vec<String> = self.node_palette_filtered.iter()
- .map(|&i| self.node_templates[i].label.clone())
- .collect();
- self.widgets[NODE_PALETTE_IDX].set_palette_state(
- self.node_palette_visible,
- &self.node_palette_query,
- &items,
- self.node_palette_selected,
- );
- }
-
- fn open_file_chooser(&self) {
- std::thread::spawn(move || {
- use std::process::{Command, Stdio};
- use std::io::Write;
-
- // Find executable path
- let exe_path = if let Ok(cur_exe) = std::env::current_exe() {
- let sibling = cur_exe.with_file_name("clear-filesystem-interface");
- if sibling.exists() {
- sibling
- } else {
- std::path::PathBuf::from("clear-filesystem-interface")
- }
- } else {
- std::path::PathBuf::from("clear-filesystem-interface")
- };
-
- let child = match Command::new(exe_path)
- .arg("--select")
- .stdout(Stdio::piped())
- .spawn()
- {
- Ok(c) => c,
- Err(e) => {
- eprintln!("Failed to spawn clear-filesystem-interface: {:?}", e);
- return;
- }
- };
-
- let output = match child.wait_with_output() {
- Ok(o) => o,
- Err(e) => {
- eprintln!("Failed to wait for clear-filesystem-interface: {:?}", e);
- return;
- }
- };
-
- if output.status.success() {
- let selected = String::from_utf8_lossy(&output.stdout).trim().to_string();
- if !selected.is_empty() {
- let body = format!(
- "{{\"action\":\"load\",\"path\":\"{}\"}}",
- selected.replace('\\', "\\\\").replace('"', "\\\"")
- );
- let req = format!(
- "POST /action HTTP/1.1\r\n\
- Host: 127.0.0.1:3000\r\n\
- Content-Type: application/json\r\n\
- Content-Length: {}\r\n\
- Connection: close\r\n\r\n\
- {}",
- body.len(),
- body
- );
- if let Ok(mut stream) = std::net::TcpStream::connect("127.0.0.1:3000") {
- let _ = stream.write_all(req.as_bytes());
- let _ = stream.flush();
- }
- }
- }
- });
- }
-
- fn save_file_chooser(&self) {
- std::thread::spawn(move || {
- use std::process::{Command, Stdio};
- use std::io::Write;
-
- // Find executable path
- let exe_path = if let Ok(cur_exe) = std::env::current_exe() {
- let sibling = cur_exe.with_file_name("clear-filesystem-interface");
- if sibling.exists() {
- sibling
- } else {
- std::path::PathBuf::from("clear-filesystem-interface")
- }
- } else {
- std::path::PathBuf::from("clear-filesystem-interface")
- };
-
- let child = match Command::new(exe_path)
- .arg("--save")
- .stdout(Stdio::piped())
- .spawn()
- {
- Ok(c) => c,
- Err(e) => {
- eprintln!("Failed to spawn clear-filesystem-interface: {:?}", e);
- return;
- }
- };
-
- let output = match child.wait_with_output() {
- Ok(o) => o,
- Err(e) => {
- eprintln!("Failed to wait for clear-filesystem-interface: {:?}", e);
- return;
- }
- };
-
- if output.status.success() {
- let selected = String::from_utf8_lossy(&output.stdout).trim().to_string();
- if !selected.is_empty() {
- let body = format!(
- "{{\"action\":\"save\",\"path\":\"{}\"}}",
- selected.replace('\\', "\\\\").replace('"', "\\\"")
- );
- let req = format!(
- "POST /action HTTP/1.1\r\n\
- Host: 127.0.0.1:3000\r\n\
- Content-Type: application/json\r\n\
- Content-Length: {}\r\n\
- Connection: close\r\n\r\n\
- {}",
- body.len(),
- body
- );
- if let Ok(mut stream) = std::net::TcpStream::connect("127.0.0.1:3000") {
- let _ = stream.write_all(req.as_bytes());
- let _ = stream.flush();
- }
- }
- }
- });
- }
-
- fn open_node_palette(&mut self) {
- self.node_palette_visible = true;
- self.node_palette_query = String::new();
- self.node_palette_selected = 0;
- self.refresh_node_palette();
- self.upload_vertices();
- }
-
- fn close_node_palette(&mut self) {
- self.node_palette_visible = false;
- self.refresh_node_palette();
- self.upload_vertices();
- }
-
-
- fn place_selected_node(&mut self) -> bool {
- let Some(&template_idx) = self.node_palette_filtered.get(self.node_palette_selected) else { return false; };
- let mut node = self.node_templates[template_idx].node.clone();
- let (nx, ny) = self.find_empty_cell(self.grid_cursor_col as f32, self.grid_cursor_row as f32, None);
- node.position = (nx, ny);
- self.current_dir_mut().children.push(node);
- self.close_node_palette();
- self.sync_nodes();
- self.rebuild_positions();
- self.apply_layout();
- self.update_panel_bounds();
- self.upload_vertices();
- true
- }
-
-
- fn handle_node_palette_key(&mut self, event: &KeyEvent) -> bool {
- if event.state != ElementState::Pressed { return false; }
- match &event.logical_key {
- Key::Named(NamedKey::Escape) => {
- self.close_node_palette();
- true
- }
- Key::Named(NamedKey::Enter) => self.place_selected_node(),
- Key::Named(NamedKey::ArrowDown) => {
- if !self.node_palette_filtered.is_empty() {
- self.node_palette_selected = (self.node_palette_selected + 1).min(self.node_palette_filtered.len() - 1);
- self.refresh_node_palette();
- self.upload_vertices();
- }
- true
- }
- Key::Named(NamedKey::ArrowUp) => {
- if self.node_palette_selected > 0 {
- self.node_palette_selected -= 1;
- self.refresh_node_palette();
- self.upload_vertices();
- }
- true
- }
- Key::Named(NamedKey::Backspace) => {
- self.node_palette_query.pop();
- self.refresh_node_palette();
- self.upload_vertices();
- true
- }
- Key::Named(NamedKey::Tab) => {
- self.close_node_palette();
- true
- }
- _ => {
- if let Some(text) = &event.text {
- for ch in text.chars().filter(|c| !c.is_control()) {
- self.node_palette_query.push(ch);
- }
- self.refresh_node_palette();
- self.upload_vertices();
- true
- } else {
- false
- }
- }
- }
- }
-
-fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String>>) {
- let mut headers = vec![
- "Vertex".to_string(),
- "Pos.x".to_string(),
- "Pos.y".to_string(),
- "Pos.z".to_string(),
- "Col.r".to_string(),
- "Col.g".to_string(),
- "Col.b".to_string(),
- ];
-
- let mut custom_keys = std::collections::BTreeSet::new();
- for v in &geom.vertices {
- for k in v.attributes.keys() {
- custom_keys.insert(k.clone());
- }
- }
- let custom_keys: Vec<String> = custom_keys.into_iter().collect();
-
- for key in &custom_keys {
- if let Some(val) = geom.vertices.iter().find_map(|v| v.attributes.get(key)) {
- match val {
- GAttribute::Float(_) => {
- headers.push(key.clone());
- }
- GAttribute::Float2(_) => {
- headers.push(format!("{}.x", key));
- headers.push(format!("{}.y", key));
- }
- GAttribute::Float3(_) => {
- headers.push(format!("{}.x", key));
- headers.push(format!("{}.y", key));
- headers.push(format!("{}.z", key));
- }
- GAttribute::Float4(_) => {
- headers.push(format!("{}.x", key));
- headers.push(format!("{}.y", key));
- headers.push(format!("{}.z", key));
- headers.push(format!("{}.w", key));
- }
- }
- }
- }
-
- let mut rows = Vec::new();
- for (i, v) in geom.vertices.iter().enumerate() {
- let mut row = vec![
- i.to_string(),
- format!("{:.4}", v.pos[0]),
- format!("{:.4}", v.pos[1]),
- format!("{:.4}", v.pos[2]),
- format!("{:.4}", v.col[0]),
- format!("{:.4}", v.col[1]),
- format!("{:.4}", v.col[2]),
- ];
-
- for key in &custom_keys {
- if let Some(val) = v.attributes.get(key) {
- match val {
- GAttribute::Float(f) => {
- row.push(format!("{:.4}", f));
- }
- GAttribute::Float2(arr) => {
- row.push(format!("{:.4}", arr[0]));
- row.push(format!("{:.4}", arr[1]));
- }
- GAttribute::Float3(arr) => {
- row.push(format!("{:.4}", arr[0]));
- row.push(format!("{:.4}", arr[1]));
- row.push(format!("{:.4}", arr[2]));
- }
- GAttribute::Float4(arr) => {
- row.push(format!("{:.4}", arr[0]));
- row.push(format!("{:.4}", arr[1]));
- row.push(format!("{:.4}", arr[2]));
- row.push(format!("{:.4}", arr[3]));
- }
- }
- } else {
- if let Some(val) = geom.vertices.iter().find_map(|v| v.attributes.get(key)) {
- let count = match val {
- GAttribute::Float(_) => 1,
- GAttribute::Float2(_) => 2,
- GAttribute::Float3(_) => 3,
- GAttribute::Float4(_) => 4,
- };
- for _ in 0..count {
- row.push("-".to_string());
- }
- }
- }
- }
- rows.push(row);
- }
-
- (headers, rows)
-}
-
- fn update_active_camera_rotation(&mut self, d_yaw: f32, d_pitch: f32) -> bool {
- if self.active_camera == "Default Camera" {
- return false;
- }
- let camera_name = self.active_camera.clone();
- let dir = self.current_dir_mut();
- if let Some(node) = dir.children.iter_mut().find(|c| c.node_type == "camera" && c.name == camera_name) {
- if let Some(p) = node.params.iter_mut().find(|p| p.name == "Rotation") {
- let parts: Vec<&str> = p.default
- .split(|c| c == ':' || c == ',' || c == ' ')
- .filter(|s| !s.is_empty())
- .collect();
- let mut rx = 0.0f32;
- let mut ry = 0.0f32;
- let mut rz = 0.0f32;
- if parts.len() >= 3 {
- if let (Ok(vx), Ok(vy), Ok(vz)) = (parts[0].parse::<f32>(), parts[1].parse::<f32>(), parts[2].parse::<f32>()) {
- rx = vx;
- ry = vy;
- rz = vz;
- }
- }
- ry += d_yaw.to_degrees();
- rx -= d_pitch.to_degrees();
- while rx > 180.0 { rx -= 360.0; }
- while rx < -180.0 { rx += 360.0; }
- while ry > 180.0 { ry -= 360.0; }
- while ry < -180.0 { ry += 360.0; }
- p.default = format!("{:.2}:{:.2}:{:.2}", rx, ry, rz);
-
- self.sync_nodes();
- let params = if !self.is_detached_network {
- self.widgets[CONTENT_IDX].selected_node().and_then(|sel_idx| {
- let dir = self.current_dir();
- if sel_idx < dir.children.len() {
- Some(param_display(&dir.children[sel_idx].params))
- } else { None }
- }).unwrap_or_default()
- } else {
- vec![]
- };
- self.widgets[PARAM_IDX].set_display_params(¶ms);
-
- return true;
- }
- }
- false
- }
-
- fn update_active_camera_rotation_reset(&mut self) -> bool {
- if self.active_camera == "Default Camera" {
- return false;
- }
- let camera_name = self.active_camera.clone();
- let dir = self.current_dir_mut();
- if let Some(node) = dir.children.iter_mut().find(|c| c.node_type == "camera" && c.name == camera_name) {
- if let Some(p) = node.params.iter_mut().find(|p| p.name == "Rotation") {
- p.default = "0.00:0.00:0.00".to_string();
- self.sync_nodes();
- let params = if !self.is_detached_network {
- self.widgets[CONTENT_IDX].selected_node().and_then(|sel_idx| {
- let dir = self.current_dir();
- if sel_idx < dir.children.len() {
- Some(param_display(&dir.children[sel_idx].params))
- } else { None }
- }).unwrap_or_default()
- } else {
- vec![]
- };
- self.widgets[PARAM_IDX].set_display_params(¶ms);
- return true;
- }
- }
- false
- }
-
- fn on_path_changed(&mut self) {
- self.widgets[CONTENT_IDX].set_selected_node(None);
- self.drag_widget = None;
- self.focused_widget = None;
- self.pan_x = 0.0;
- self.pan_y = 0.0;
- self.pan_velocity_x = 0.0;
- self.pan_velocity_y = 0.0;
- self.last_frame_pan_x = 0.0;
- self.last_frame_pan_y = 0.0;
- self.is_scrolling_trackpad = false;
- self.scroll_accum_x = 0.0;
- self.scroll_accum_y = 0.0;
- self.grid_cursor_col = 0;
- self.grid_cursor_row = 0;
- self.sync_grid_settings();
- self.sync_nodes();
- self.rebuild_scene_geometry();
- self.rebuild_positions();
- self.apply_layout();
- self.update_panel_bounds();
- self.upload_vertices();
- }
-
- fn find_empty_cell(&self, start_x: f32, start_y: f32, skip_idx: Option<usize>) -> (f32, f32) {
- let x = start_x;
- let mut y = start_y;
- let dir = self.current_dir();
- loop {
- let occupied = dir.children.iter().enumerate().any(|(idx, c)| {
- if Some(idx) == skip_idx {
- false
- } else {
- (c.position.0 - x).abs() < 0.01 && (c.position.1 - y).abs() < 0.01
- }
- });
- if occupied {
- y += 1.0;
- } else {
- break;
- }
- }
- (x, y)
- }
-
- fn sync_nodes(&mut self) {
- let graph_nodes: Vec<GraphNode> = self.current_dir().children.iter().map(|c| {
- GraphNode {
- name: c.name.clone(),
- position: c.position,
- parameters: param_display(&c.params),
- geom_visible: c.geometry_visible,
- }
- }).collect();
- self.widgets[CONTENT_IDX].set_nodes(&graph_nodes);
-
- 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);
-
- let mut selected_node = None;
- if !self.is_detached_network {
- if let Some(slot_idx) = self.widgets[CONTENT_IDX].selected_node() {
- let dir = self.current_dir();
- if slot_idx < dir.children.len() {
- selected_node = Some(&dir.children[slot_idx]);
- }
- }
- }
-
-
- let mut cache_hit = false;
- let mut current_name = None;
- let mut current_params = None;
-
- if let Some(node) = selected_node {
- current_name = Some(node.name.clone());
- current_params = Some(node.params.iter().map(|p| (p.name.clone(), p.default.clone())).collect::<Vec<_>>());
- if self.last_spreadsheet_node_name == current_name && self.last_spreadsheet_node_params == current_params {
- cache_hit = true;
- }
- } else {
- if self.last_spreadsheet_node_name.is_none() {
- cache_hit = true;
- }
- }
-
- if !cache_hit {
- let mut headers = Vec::new();
- let mut rows = Vec::new();
-
- if let Some(node) = selected_node {
- if node.node_type.eq_ignore_ascii_case("sphere") {
- if let Some(idx) = find_sphere_index(&self.fs_root, node) {
- let center = Vec3::new((idx % 4) as f32 * 1.25 - 1.875, 0.55, -((idx / 4) as f32) * 1.25);
- let radius = node_param_f32(node, "Radius", 0.5).max(0.05);
- let geom = sphere_vertices(center, radius);
- let (h, r) = Self::geometry_to_spreadsheet_data(&geom);
- headers = h;
- rows = r;
- }
- } else if node.node_type.eq_ignore_ascii_case("line") {
- if let Some(idx) = find_sphere_index(&self.fs_root, node) {
- let start = Vec3::new((idx % 4) as f32 * 1.25 - 1.875, 0.55, -((idx / 4) as f32) * 1.25);
- let length = node_param_f32(node, "Length", 1.0);
- let thickness = node_param_f32(node, "Thickness", 0.02);
- let end = start + Vec3::new(0.0, length, 0.0);
- let geom = line_vertices(start, end, thickness);
- let (h, r) = Self::geometry_to_spreadsheet_data(&geom);
- headers = h;
- rows = r;
- }
- } else if node.node_type.eq_ignore_ascii_case("add") {
- if let Some(idx) = find_sphere_index(&self.fs_root, node) {
- let center = Vec3::new((idx % 4) as f32 * 1.25 - 1.875, 0.55, -((idx / 4) as f32) * 1.25);
- let num_points = node_param_f32(node, "Points", 100.0) as i32;
- let mut geom = Geometry::new();
- for i in 0..num_points {
- let t = i as f32 / num_points.max(1) as f32;
- let angle = t * std::f32::consts::TAU * 3.0;
- let r = 0.4 * t;
- let px = center.x + r * angle.cos();
- let py = center.y + t * 0.5 - 0.25;
- let pz = center.z + r * angle.sin();
- let pt_center = Vec3::new(px, py, pz);
- geom.merge(sphere_vertices(pt_center, 0.02));
- }
- let (h, r) = Self::geometry_to_spreadsheet_data(&geom);
- headers = h;
- rows = r;
- }
- } else if node.node_type.eq_ignore_ascii_case("transform") {
- let mut visited = Vec::new();
- if let Some(geom) = resolve_transform_geometry(&self.fs_root, node, &mut visited) {
- let (h, r) = Self::geometry_to_spreadsheet_data(&geom);
- headers = h;
- rows = r;
- }
- }
- }
-
- self.widgets[SPREADSHEET_IDX].set_spreadsheet_data(headers, rows);
- self.last_spreadsheet_node_name = current_name;
- self.last_spreadsheet_node_params = current_params;
- }
- }
-
- fn save_to_file(&mut self, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
- if path.file_name().map_or(false, |n| n == "default_project.json") {
- let proj = Project {
- name: "Default Project".to_string(),
- root: self.fs_root.clone(),
- view_state: ProjectViewState {
- active_camera: self.active_camera.clone(),
- pan: (self.pan_x, self.pan_y),
- current_path: self.current_path.clone(),
- selected_node: self.widgets[CONTENT_IDX].selected_node(),
- },
- };
- let content = serde_json::to_string_pretty(&proj)?;
- fs::write(path, content)?;
- self.last_saved_root_json = serde_json::to_string(&self.fs_root).unwrap_or_default();
- return Ok(());
- }
-
- let project_dir = path;
- let project_name = project_dir.file_name()
- .and_then(|n| n.to_str())
- .unwrap_or("Default Project")
- .to_string();
-
- fs::create_dir_all(project_dir)?;
-
- let state_file_path = project_dir.join("state.json");
-
- let proj = Project {
- name: project_name,
- root: self.fs_root.clone(),
- view_state: ProjectViewState {
- active_camera: self.active_camera.clone(),
- pan: (self.pan_x, self.pan_y),
- current_path: self.current_path.clone(),
- selected_node: self.widgets[CONTENT_IDX].selected_node(),
- },
- };
- let content = serde_json::to_string_pretty(&proj)?;
- fs::write(&state_file_path, content)?;
- self.last_saved_root_json = serde_json::to_string(&self.fs_root).unwrap_or_default();
- Ok(())
- }
-
- fn load_from_file(&mut self, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
- if path.file_name().map_or(false, |n| n == "default_project.json") {
- let content = fs::read_to_string(path)?;
- let proj: Project = serde_json::from_str(&content)?;
- self.fs_root = proj.root;
- self.active_camera = proj.view_state.active_camera;
- self.pan_x = proj.view_state.pan.0;
- self.pan_y = proj.view_state.pan.1;
- self.pan_velocity_x = 0.0;
- self.pan_velocity_y = 0.0;
- self.last_frame_pan_x = self.pan_x;
- self.last_frame_pan_y = self.pan_y;
- self.is_scrolling_trackpad = false;
- self.scroll_accum_x = 0.0;
- self.scroll_accum_y = 0.0;
- self.current_path = proj.view_state.current_path;
-
- let sel = proj.view_state.selected_node;
- self.widgets[CONTENT_IDX].set_selected_node(sel);
- if sel.is_some() {
- self.focused_widget = Some(CONTENT_IDX);
- } else {
- self.focused_widget = None;
- }
- self.drag_widget = None;
- self.last_click = None;
-
- self.sync_grid_settings();
- self.sync_nodes();
-
- let params = if !self.is_detached_network {
- self.widgets[CONTENT_IDX].selected_node().and_then(|sel_idx| {
- let dir = self.current_dir();
- if sel_idx < dir.children.len() {
- Some(param_display(&dir.children[sel_idx].params))
- } else { None }
- }).unwrap_or_default()
- } else {
- vec![]
- };
- self.widgets[PARAM_IDX].set_display_params(¶ms);
-
- self.rebuild_scene_geometry();
- self.rebuild_positions();
- self.apply_layout();
- self.update_panel_bounds();
- self.upload_vertices();
- self.loaded_project_path = None;
- self.last_saved_root_json = serde_json::to_string(&self.fs_root).unwrap_or_default();
- self.update_window_title();
- return Ok(());
- }
-
- let (state_file_path, project_dir) = if path.is_dir() {
- (path.join("state.json"), path.to_path_buf())
- } else {
- if path.file_name().map_or(false, |name| name == "state.json") {
- (path.to_path_buf(), path.parent().unwrap_or(path).to_path_buf())
- } else {
- (path.to_path_buf(), path.parent().unwrap_or(path).to_path_buf())
- }
- };
-
- let content = fs::read_to_string(&state_file_path)?;
- let proj: Project = serde_json::from_str(&content)?;
- self.fs_root = proj.root;
- self.active_camera = proj.view_state.active_camera;
- self.pan_x = proj.view_state.pan.0;
- self.pan_y = proj.view_state.pan.1;
- self.pan_velocity_x = 0.0;
- self.pan_velocity_y = 0.0;
- self.last_frame_pan_x = self.pan_x;
- self.last_frame_pan_y = self.pan_y;
- self.is_scrolling_trackpad = false;
- self.scroll_accum_x = 0.0;
- self.scroll_accum_y = 0.0;
- self.current_path = proj.view_state.current_path;
-
- let sel = proj.view_state.selected_node;
- self.widgets[CONTENT_IDX].set_selected_node(sel);
- if sel.is_some() {
- self.focused_widget = Some(CONTENT_IDX);
- } else {
- self.focused_widget = None;
- }
- self.drag_widget = None;
- self.last_click = None;
-
- self.sync_grid_settings();
- self.sync_nodes();
-
- // Sync Parameters pane with selected node
- let params = if !self.is_detached_network {
- self.widgets[CONTENT_IDX].selected_node().and_then(|sel_idx| {
- let dir = self.current_dir();
- if sel_idx < dir.children.len() {
- Some(param_display(&dir.children[sel_idx].params))
- } else { None }
- }).unwrap_or_default()
- } else {
- vec![]
- };
- self.widgets[PARAM_IDX].set_display_params(¶ms);
-
- self.rebuild_scene_geometry();
- self.rebuild_positions();
- self.apply_layout();
- self.update_panel_bounds();
- self.upload_vertices();
- self.loaded_project_path = Some(project_dir);
- self.last_saved_root_json = serde_json::to_string(&self.fs_root).unwrap_or_default();
- self.update_window_title();
- Ok(())
- }
-
- fn new_project(&mut self) {
- self.fs_root = FsNode {
- name: "root".to_string(),
- node_type: "node".to_string(),
- children: vec![],
- params: vec![],
- geometry_visible: true,
- position: (0.0, 0.0),
- };
- self.active_camera = "Default Camera".to_string();
- self.pan_x = 0.0;
- self.pan_y = 0.0;
- self.pan_velocity_x = 0.0;
- self.pan_velocity_y = 0.0;
- self.last_frame_pan_x = 0.0;
- self.last_frame_pan_y = 0.0;
- self.is_scrolling_trackpad = false;
- self.scroll_accum_x = 0.0;
- self.scroll_accum_y = 0.0;
- self.current_path.clear();
- self.grid_cursor_col = 0;
- self.grid_cursor_row = 0;
-
- self.focused_widget = None;
- self.drag_widget = None;
- self.last_click = None;
-
- self.sync_grid_settings();
- self.sync_nodes();
- self.rebuild_scene_geometry();
- self.rebuild_positions();
- self.apply_layout();
- self.update_panel_bounds();
- self.upload_vertices();
- self.loaded_project_path = None;
- self.last_saved_root_json = serde_json::to_string(&self.fs_root).unwrap_or_default();
- self.update_window_title();
- }
-
- fn create_depth_texture(&self) -> (wgpu::Texture, wgpu::TextureView) {
- let tex = self.wgpu_adapter.device.create_texture(&wgpu::TextureDescriptor {
- label: Some("Depth Texture"),
- size: wgpu::Extent3d { width: self.physical_width.max(1), height: self.physical_height.max(1), depth_or_array_layers: 1 },
- mip_level_count: 1,
- sample_count: 1,
- dimension: wgpu::TextureDimension::D2,
- format: wgpu::TextureFormat::Depth32Float,
- usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
- view_formats: &[],
- });
- let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
- (tex, view)
- }
-
- fn create_backdrop_texture(&self) -> (wgpu::Texture, wgpu::TextureView) {
- let tex = self.wgpu_adapter.device.create_texture(&wgpu::TextureDescriptor {
- label: Some("Backdrop Texture"),
- size: wgpu::Extent3d { width: self.physical_width.max(1), height: self.physical_height.max(1), depth_or_array_layers: 1 },
- mip_level_count: 1,
- sample_count: 1,
- dimension: wgpu::TextureDimension::D2,
- format: self.wgpu_adapter.config.format,
- usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC,
- view_formats: &[],
- });
- let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
- (tex, view)
- }
-
- async fn new(
- conn: &Connection,
- qh: &QueueHandle<AppState>,
- compositor_state: &CompositorState,
- xdg_shell_state: &XdgShell,
- pw: u32,
- ph: u32,
- scale: f64,
- is_detached_network: bool,
- ) -> Self {
- clear_ui::scale::set_scale_factor(scale as f32);
- let settings = DesignSettings::load();
- let lw = pw as f32 / scale as f32;
- let lh = ph as f32 / scale as f32;
- let sw = lw;
-
- let wl_surface = compositor_state.create_surface(qh);
- wl_surface.set_buffer_scale(scale as i32);
- let window = xdg_shell_state.create_window(wl_surface.clone(), WindowDecorations::None, qh);
- if is_detached_network {
- window.set_title("Network Pane");
- window.set_app_id("circular-network-pane");
- window.set_min_size(Some((200, 200)));
- } else {
- window.set_title("Clear Design Interface");
- window.set_app_id("cce-design-interface");
- window.set_min_size(Some((480, 320)));
- }
- window.commit();
-
- let display_ptr = conn.backend().display_id().as_ptr() as *mut std::ffi::c_void;
- let surface_ptr = wl_surface.id().as_ptr() as *mut std::ffi::c_void;
- let wgpu_adapter = clear_ui::backend::WgpuAdapter::new(display_ptr, surface_ptr, pw, ph).await;
-
- let device = &wgpu_adapter.device;
- let queue = &wgpu_adapter.queue;
- let surface = &wgpu_adapter.surface;
- let config = &wgpu_adapter.config;
-
- let backdrop_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
- label: Some("Backdrop Bind Group Layout"),
- entries: &[
- wgpu::BindGroupLayoutEntry {
- binding: 0,
- visibility: wgpu::ShaderStages::FRAGMENT,
- ty: wgpu::BindingType::Texture {
- multisampled: false,
- view_dimension: wgpu::TextureViewDimension::D2,
- sample_type: wgpu::TextureSampleType::Float { filterable: true },
- },
- count: None,
- },
- wgpu::BindGroupLayoutEntry {
- binding: 1,
- visibility: wgpu::ShaderStages::FRAGMENT,
- ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
- count: None,
- },
- ],
- });
-
- let backdrop_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
- label: Some("Backdrop Sampler"),
- address_mode_u: wgpu::AddressMode::ClampToEdge,
- address_mode_v: wgpu::AddressMode::ClampToEdge,
- address_mode_w: wgpu::AddressMode::ClampToEdge,
- mag_filter: wgpu::FilterMode::Linear,
- min_filter: wgpu::FilterMode::Linear,
- mipmap_filter: wgpu::FilterMode::Nearest,
- ..Default::default()
- });
-
- let backdrop_texture = device.create_texture(&wgpu::TextureDescriptor {
- label: Some("Backdrop Texture"),
- size: wgpu::Extent3d { width: pw.max(1), height: ph.max(1), depth_or_array_layers: 1 },
- mip_level_count: 1,
- sample_count: 1,
- dimension: wgpu::TextureDimension::D2,
- format: config.format,
- usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC,
- view_formats: &[],
- });
- let backdrop_texture_view = backdrop_texture.create_view(&wgpu::TextureViewDescriptor::default());
-
- let backdrop_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some("Backdrop Bind Group"),
- layout: &backdrop_bind_group_layout,
- entries: &[
- wgpu::BindGroupEntry {
- binding: 0,
- resource: wgpu::BindingResource::TextureView(&backdrop_texture_view),
- },
- wgpu::BindGroupEntry {
- binding: 1,
- resource: wgpu::BindingResource::Sampler(&backdrop_sampler),
- },
- ],
- });
-
- let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
- label: Some("Shader"),
- source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
- });
-
- let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
- label: Some("Pipeline Layout"),
- bind_group_layouts: &[&backdrop_bind_group_layout],
- push_constant_ranges: &[],
- });
-
- let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
- label: Some("Render Pipeline"),
- layout: Some(&pipeline_layout),
- vertex: wgpu::VertexState {
- module: &shader,
- entry_point: Some("vs_main"),
- buffers: &[Vertex::desc()],
- compilation_options: Default::default(),
- },
- fragment: Some(wgpu::FragmentState {
- module: &shader,
- entry_point: Some("fs_main"),
- targets: &[Some(wgpu::ColorTargetState {
- format: config.format,
- blend: Some(wgpu::BlendState::ALPHA_BLENDING),
- write_mask: wgpu::ColorWrites::ALL,
- })],
- compilation_options: Default::default(),
- }),
- primitive: wgpu::PrimitiveState {
- topology: wgpu::PrimitiveTopology::TriangleList,
- strip_index_format: None,
- front_face: wgpu::FrontFace::Ccw,
- cull_mode: None,
- polygon_mode: wgpu::PolygonMode::Fill,
- unclipped_depth: false,
- conservative: false,
- },
- depth_stencil: None,
- multisample: wgpu::MultisampleState { count: 1, mask: !0, alpha_to_coverage_enabled: false },
- multiview: None,
- cache: None,
- });
-
- // 3D pipeline
- let shader_3d = device.create_shader_module(wgpu::ShaderModuleDescriptor {
- label: Some("Shader 3D"),
- source: wgpu::ShaderSource::Wgsl(include_str!("shader_3d.wgsl").into()),
- });
-
- let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("Uniform Buffer"),
- size: 64,
- usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
-
- let bind_group_layout_3d = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
- label: Some("3D Bind Group Layout"),
- entries: &[wgpu::BindGroupLayoutEntry {
- binding: 0,
- visibility: wgpu::ShaderStages::VERTEX,
- ty: wgpu::BindingType::Buffer {
- ty: wgpu::BufferBindingType::Uniform,
- has_dynamic_offset: false,
- min_binding_size: wgpu::BufferSize::new(64),
- },
- count: None,
- }],
- });
-
- let pipeline_layout_3d = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
- label: Some("3D Pipeline Layout"),
- bind_group_layouts: &[&bind_group_layout_3d],
- push_constant_ranges: &[],
- });
-
- let bind_group_3d = device.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some("3D Bind Group"),
- layout: &bind_group_layout_3d,
- entries: &[wgpu::BindGroupEntry {
- binding: 0,
- resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
- buffer: &uniform_buffer,
- offset: 0,
- size: wgpu::BufferSize::new(64),
- }),
- }],
- });
-
- let uniform_buffer_grid = device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("Grid Uniform Buffer"),
- size: 64,
- usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
-
- let bind_group_grid = device.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some("Grid 3D Bind Group"),
- layout: &bind_group_layout_3d,
- entries: &[wgpu::BindGroupEntry {
- binding: 0,
- resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
- buffer: &uniform_buffer_grid,
- offset: 0,
- size: wgpu::BufferSize::new(64),
- }),
- }],
- });
-
- let uniform_buffer_pivot = device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("Pivot Uniform Buffer"),
- size: 64,
- usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
-
- let bind_group_pivot = device.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some("Pivot Bind Group"),
- layout: &bind_group_layout_3d,
- entries: &[wgpu::BindGroupEntry {
- binding: 0,
- resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
- buffer: &uniform_buffer_pivot,
- offset: 0,
- size: wgpu::BufferSize::new(64),
- }),
- }],
- });
-
- let vertex_buffer_3d = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
- label: Some("Cube Vertex Buffer"),
- contents: bytemuck::cast_slice(&cube_vertices()),
- usage: wgpu::BufferUsages::VERTEX,
- });
-
- let vertex_buffer_spheres = device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("Sphere Node Vertex Buffer"),
- size: 1,
- usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
-
- let grid_verts = grid_vertices(settings.grid_thickness, settings.grid_color);
- let vertex_count_grid = grid_verts.len() as u32;
- let vertex_buffer_grid = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
- label: Some("Grid Vertex Buffer"),
- contents: bytemuck::cast_slice(&grid_verts),
- usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
- });
-
- let origin_verts = origin_vectors_vertices(settings.origin_size);
- let vertex_count_origin = origin_verts.len() as u32;
- let vertex_buffer_origin = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
- label: Some("Origin Vectors Vertex Buffer"),
- contents: bytemuck::cast_slice(&origin_verts),
- usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
- });
-
- let pivot_verts = camera_pivot_vertices(settings.camera_pivot_size);
- let vertex_count_pivot = pivot_verts.len() as u32;
- let vertex_buffer_pivot = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
- label: Some("Camera Pivot Vertex Buffer"),
- contents: bytemuck::cast_slice(&pivot_verts),
- usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
- });
-
- let pipeline_3d = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
- label: Some("3D Pipeline"),
- layout: Some(&pipeline_layout_3d),
- vertex: wgpu::VertexState {
- module: &shader_3d,
- entry_point: Some("vs_main"),
- buffers: &[Vertex3D::desc()],
- compilation_options: Default::default(),
- },
- fragment: Some(wgpu::FragmentState {
- module: &shader_3d,
- entry_point: Some("fs_main"),
- targets: &[Some(wgpu::ColorTargetState {
- format: config.format,
- blend: Some(wgpu::BlendState::ALPHA_BLENDING),
- write_mask: wgpu::ColorWrites::ALL,
- })],
- compilation_options: Default::default(),
- }),
- primitive: wgpu::PrimitiveState {
- topology: wgpu::PrimitiveTopology::TriangleList,
- strip_index_format: None,
- front_face: wgpu::FrontFace::Ccw,
- cull_mode: Some(wgpu::Face::Back),
- polygon_mode: wgpu::PolygonMode::Fill,
- unclipped_depth: false,
- conservative: false,
- },
- depth_stencil: Some(wgpu::DepthStencilState {
- format: wgpu::TextureFormat::Depth32Float,
- depth_write_enabled: true,
- depth_compare: wgpu::CompareFunction::Less,
- stencil: wgpu::StencilState::default(),
- bias: wgpu::DepthBiasState::default(),
- }),
- multisample: wgpu::MultisampleState { count: 1, mask: !0, alpha_to_coverage_enabled: false },
- multiview: None,
- cache: None,
- });
-
- let cache = Cache::new(device);
-
- let shader_textured = device.create_shader_module(wgpu::ShaderModuleDescriptor {
- label: Some("Textured Shader"),
- source: wgpu::ShaderSource::Wgsl(include_str!("shader_textured.wgsl").into()),
- });
-
- let textured_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
- label: Some("Textured Bind Group Layout"),
- entries: &[
- wgpu::BindGroupLayoutEntry {
- binding: 0,
- visibility: wgpu::ShaderStages::FRAGMENT,
- ty: wgpu::BindingType::Texture {
- multisampled: false,
- view_dimension: wgpu::TextureViewDimension::D2,
- sample_type: wgpu::TextureSampleType::Float { filterable: true },
- },
- count: None,
- },
- wgpu::BindGroupLayoutEntry {
- binding: 1,
- visibility: wgpu::ShaderStages::FRAGMENT,
- ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
- count: None,
- },
- ],
- });
-
- let textured_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
- label: Some("Textured Pipeline Layout"),
- bind_group_layouts: &[&textured_bind_group_layout],
- push_constant_ranges: &[],
- });
-
- let curved_text_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
- label: Some("Textured Render Pipeline"),
- layout: Some(&textured_pipeline_layout),
- vertex: wgpu::VertexState {
- module: &shader_textured,
- entry_point: Some("vs_main"),
- buffers: &[TexturedVertex::desc()],
- compilation_options: Default::default(),
- },
- fragment: Some(wgpu::FragmentState {
- module: &shader_textured,
- entry_point: Some("fs_main"),
- targets: &[Some(wgpu::ColorTargetState {
- format: config.format,
- blend: Some(wgpu::BlendState::ALPHA_BLENDING),
- write_mask: wgpu::ColorWrites::ALL,
- })],
- compilation_options: Default::default(),
- }),
- primitive: wgpu::PrimitiveState {
- topology: wgpu::PrimitiveTopology::TriangleList,
- strip_index_format: None,
- front_face: wgpu::FrontFace::Ccw,
- cull_mode: None,
- unclipped_depth: false,
- polygon_mode: wgpu::PolygonMode::Fill,
- conservative: false,
- },
- depth_stencil: None,
- multisample: wgpu::MultisampleState::default(),
- multiview: None,
- cache: None,
- });
-
- let curved_text_texture = device.create_texture(&wgpu::TextureDescriptor {
- label: Some("Curved Text Texture"),
- size: wgpu::Extent3d {
- width: 1024,
- height: 1024,
- depth_or_array_layers: 1,
- },
- mip_level_count: 1,
- sample_count: 1,
- dimension: wgpu::TextureDimension::D2,
- format: config.format,
- usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
- view_formats: &[],
- });
- let curved_text_texture_view = curved_text_texture.create_view(&wgpu::TextureViewDescriptor::default());
-
- let curved_text_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
- label: Some("Curved Text Sampler"),
- address_mode_u: wgpu::AddressMode::ClampToEdge,
- address_mode_v: wgpu::AddressMode::ClampToEdge,
- address_mode_w: wgpu::AddressMode::ClampToEdge,
- mag_filter: wgpu::FilterMode::Linear,
- min_filter: wgpu::FilterMode::Linear,
- mipmap_filter: wgpu::FilterMode::Nearest,
- ..Default::default()
- });
-
- let curved_text_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some("Curved Text Bind Group"),
- layout: &textured_bind_group_layout,
- entries: &[
- wgpu::BindGroupEntry {
- binding: 0,
- resource: wgpu::BindingResource::TextureView(&curved_text_texture_view),
- },
- wgpu::BindGroupEntry {
- binding: 1,
- resource: wgpu::BindingResource::Sampler(&curved_text_sampler),
- },
- ],
- });
-
- let mut curved_text_atlas = TextAtlas::new(&device, &queue, &cache, config.format);
- let curved_text_renderer = TextRenderer::new(&mut curved_text_atlas, &device, wgpu::MultisampleState::default(), None);
- let mut curved_text_viewport = Viewport::new(&device, &cache);
- curved_text_viewport.update(&queue, Resolution { width: 1024, height: 1024 });
-
- let textured_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("Textured Vertex Buffer"),
- size: 1,
- usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
-
-
- let splitter_layout = clear_ui::layout::SplitterLayout::new(sw, SPLITTER_W, MIN_COLUMN);
- let (depth_texture, depth_texture_view) = {
- let tex = device.create_texture(&wgpu::TextureDescriptor {
- label: Some("Depth Texture"),
- size: wgpu::Extent3d { width: pw.max(1), height: ph.max(1), depth_or_array_layers: 1 },
- mip_level_count: 1,
- sample_count: 1,
- dimension: wgpu::TextureDimension::D2,
- format: wgpu::TextureFormat::Depth32Float,
- usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
- view_formats: &[],
- });
- let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
- (tex, view)
- };
-
- let templates_root = load_fs_tree();
- let node_templates = flatten_node_templates(&templates_root);
-
- let default_proj_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("default_project.json");
- let mut loaded_project = None;
- if default_proj_path.exists() {
- if let Ok(content) = fs::read_to_string(&default_proj_path) {
- if let Ok(proj) = serde_json::from_str::<Project>(&content) {
- loaded_project = Some(proj);
- }
- }
- }
-
- let fs_root = if let Some(ref proj) = loaded_project {
- proj.root.clone()
- } else {
- FsNode {
- name: "root".to_string(),
- node_type: "node".to_string(),
- children: vec![],
- params: vec![],
- geometry_visible: true,
- position: (0.0, 0.0),
- }
- };
-
- let active_camera = if let Some(ref proj) = loaded_project {
- proj.view_state.active_camera.clone()
- } else {
- "Default Camera".to_string()
- };
-
- let (pan_x, pan_y) = if let Some(ref proj) = loaded_project {
- proj.view_state.pan
- } else {
- (0.0, 0.0)
- };
-
- let current_path = if let Some(ref proj) = loaded_project {
- proj.view_state.current_path.clone()
- } else {
- vec![]
- };
- let context_opts = vec![
- "0: Network".to_string(),
- "1: Viewport".to_string(),
- "2: Parameters".to_string(),
- "3: Spreadsheet".to_string(),
- "Main Menu".to_string(),
- ];
- let mut widgets: Vec<Box<dyn Element>> = vec![
- Box::new(MenuBar::new(0.0, 0.0, 0.0, HEADER_H).with_title("Clear Design Interface").with_label("Main Menu Bar").with_item("File", &["New Project", "Open", "Save", "Save As", "Exit"]).with_item("Edit", &["Undo", "Redo"]).with_item("View", &["Zoom In", "Zoom Out", "Reset Zoom", "Detach Circular Window", "Show Network Pane", "Show Viewport Pane", "Show Parameters Pane", "Show Spreadsheet Pane"]).with_item("Help", &["About"]).with_z_index(110).with_context_options(context_opts.clone(), 4)),
- Box::new(Graph::new()),
- Box::new(Splitter::new(SPLITTER_W)),
- Box::new(ViewportBg::new()),
- Box::new(Splitter::new(SPLITTER_W)),
- Box::new(Plate::new(0.0, 0.0, 0.0, 0.0).with_color(colors::PARAM_BG).with_blur(true)),
- Box::new(ParametersBg::new()),
- Box::new(Canvas::new()),
- Box::new(MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("0: Network").with_label("Network Menu Bar").with_item("File", &["New", "Open", "Save", "Save As"]).with_item("Edit", &["Undo", "Redo"]).with_item("View", &["Zoom In", "Zoom Out", "Circular Pane", "Detach Pane", "Close Pane"]).with_item("Settings", &[]).with_context_options(context_opts.clone(), 0)),
- Box::new(MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("1: Viewport").with_label("Viewport Menu Bar").with_item("Camera", &["Perspective", "Orthographic"]).with_item("Display", &["Square Aspect"]).with_item("Guides", &["Show Grid", "Cube", "Origin", "Camera Pivot"]).with_item("View", &["Close Pane"]).with_item("Settings", &[]).with_context_options(context_opts.clone(), 1)),
- Box::new(MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("2: Parameters").with_label("Parameters Menu Bar").with_item("Preset", &["Default", "Custom"]).with_item("Reset", &["All"]).with_item("View", &["Close Pane"]).with_context_options(context_opts.clone(), 2)),
- Box::new(StatusBar::new().with_text("Ready")),
- Box::new(Breadcrumb::new()),
- Box::new(NodePalette::new()),
- Box::new(Spreadsheet::new()),
- ];
-
- let mut spreadsheet_menubar = MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("3: Spreadsheet").with_label("Spreadsheet Menu Bar").with_item("View", &["Close Pane"]).with_context_options(context_opts.clone(), 3);
- spreadsheet_menubar.set_visible(false);
- widgets.push(Box::new(spreadsheet_menubar));
-
- let network_panel = Plate::new(0.0, 0.0, 0.0, 0.0).with_color([0.10, 0.10, 0.13, 0.95]);
- widgets.push(Box::new(network_panel));
-
- let paginator = Paginator::new(56.0, vec![])
- .with_sidebar_mode(true)
- .with_column_layout(true)
- .with_tabs_rotated(false)
- .with_context_options(context_opts.clone(), 0);
- widgets.push(Box::new(paginator));
-
- let mut positions = Vec::with_capacity(PAGINATOR_IDX + 1);
- positions.resize_with(PAGINATOR_IDX + 1, || (0.0, 0.0, 0.0, 0.0));
-
- let recent_files = Self::load_recent_files();
- let recent_files_list = ScrollingList::new(22.0, 2.0);
- let mut recent_files_buttons = Vec::new();
- for file in &recent_files {
- let label = file.file_name()
- .map(|n| n.to_string_lossy().to_string())
- .unwrap_or_else(|| file.to_string_lossy().to_string());
- let btn = Button::new_list_row(0.0, 0.0, 0.0, 0.0).with_label(&label);
- recent_files_buttons.push(btn);
- }
-
- let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("Vertex Buffer"),
- size: 1,
- usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
-
- let mut shortcut_manager = ShortcutManager::new();
- shortcut_manager.register("Ctrl+g", Action::ToggleGrid).unwrap();
- shortcut_manager.register("Ctrl+e", Action::ToggleCube).unwrap();
- shortcut_manager.register("Ctrl+a", Action::ToggleSquareViewport).unwrap();
- shortcut_manager.register("Ctrl+,", Action::ToggleConfigure).unwrap();
- shortcut_manager.register("`", Action::ToggleSpreadsheet).unwrap();
- shortcut_manager.register("Ctrl+d", Action::ToggleCircularPane).unwrap();
- shortcut_manager.register("Ctrl+s", Action::Save).unwrap();
-
- let mut state = Self {
- window,
- wl_surface,
- wgpu_adapter,
- render_pipeline,
- vertex_buffer,
- vertex_count: 0,
- vertex_data: Vec::with_capacity(4096),
- pipeline_3d,
- bind_group_3d,
- bind_group_layout_3d,
- uniform_buffer,
- bind_group_grid,
- uniform_buffer_grid,
- bind_group_pivot,
- uniform_buffer_pivot,
- vertex_buffer_3d,
- vertex_count_3d: cube_vertices().len() as u32,
- vertex_buffer_spheres,
- vertex_count_spheres: 0,
- vertex_buffer_grid,
- vertex_count_grid,
- depth_texture,
- depth_texture_view,
- backdrop_texture,
- backdrop_texture_view,
- backdrop_sampler,
- backdrop_bind_group_layout,
- backdrop_bind_group,
- rotation_y: 0.0,
- rotation_x: 0.0,
- is_rotating_viewport: false,
- scroll_lock: 0,
- last_rotate_time: Instant::now(),
- rotate_accum_yaw: 0.0,
- rotate_accum_pitch: 0.0,
- rotate_velocity_yaw: 0.0,
- rotate_velocity_pitch: 0.0,
- show_grid: settings.show_grid_enabled,
- show_cube: settings.show_cube_enabled,
- show_origin: settings.show_origin_enabled,
- show_camera_pivot: settings.show_camera_pivot_enabled,
- viewport_bg_color: settings.viewport_bg_color,
- node_color: settings.node_color,
- grid_color: settings.grid_color,
- cell_color: settings.cell_color,
- gap_color: settings.gap_color,
- vertex_buffer_origin,
- vertex_count_origin,
- vertex_buffer_pivot,
- vertex_count_pivot,
- origin_size: settings.origin_size,
- camera_pivot_size: settings.camera_pivot_size,
- fs_root: fs_root.clone(),
- node_templates,
- current_path,
- last_click: None,
- last_frame: Instant::now(),
- shortcut_manager,
- pending_action: None,
- exit_requested: false,
- widgets,
- positions,
- splitter_layout,
- node_palette_visible: false,
- node_palette_query: String::new(),
- node_palette_filtered: Vec::new(),
- node_palette_selected: 0,
- curved_text_texture,
- curved_text_texture_view,
- curved_text_sampler,
- curved_text_bind_group,
- curved_text_pipeline,
- curved_text_atlas,
- curved_text_renderer,
- curved_text_viewport,
- textured_vertex_buffer,
- textured_vertex_count: 0,
-
- drag_widget: None,
- focused_widget: None,
- cursor_x: 0.0,
- cursor_y: 0.0,
- grid_cursor_col: 0,
- grid_cursor_row: 0,
- modifiers: ModifiersState::default(),
- width: lw,
- height: lh,
- physical_width: pw,
- physical_height: ph,
- scale,
- square_viewport: settings.square_viewport,
- grid_snap_enabled: settings.grid_snap_enabled,
- network_grid_visible: settings.network_grid_enabled,
- grid_size_x: settings.grid_size_x,
- grid_size_y: settings.grid_size_y,
- skipped_row_h: settings.skipped_row_h,
- skipped_col_w: settings.skipped_col_w,
-
- pan_x,
- pan_y,
- pan_velocity_x: 0.0,
- pan_velocity_y: 0.0,
- last_frame_pan_x: pan_x,
- last_frame_pan_y: pan_y,
- is_panning: false,
- pan_start_cx: 0.0,
- pan_start_cy: 0.0,
- pan_start_x: 0.0,
- pan_start_y: 0.0,
- space_pressed: false,
- active_camera,
- show_network: true,
- show_viewport: true,
- show_parameters: true,
- show_spreadsheet: false,
- is_scrolling_trackpad: false,
- last_scroll_time: Instant::now(),
- scroll_accum_x: 0.0,
- scroll_accum_y: 0.0,
- last_spreadsheet_node_name: None,
- last_spreadsheet_node_params: None,
- viewport_zoom: 1.0,
- is_zooming_viewport: false,
- last_zoom_time: Instant::now(),
- zoom_accum: 0.0,
- zoom_velocity: 0.0,
- grid_thickness: settings.grid_thickness,
- focused_pane: LEFT_MENUBAR_IDX,
- inertial_scroll_enabled: true,
- inertial_scroll_friction: 0.90,
- scroll_speed: 1.0,
- last_config_read: Instant::now(),
- circular_network_pane: is_detached_network,
- circular_network_layout: clear_ui::layout::CircularPaneLayout::new(250.0, 300.0, 180.0),
- is_detached_network,
- detached_circular_network: false,
- last_project_mod_time: {
- let default_proj_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("default_project.json");
- std::fs::metadata(&default_proj_path).and_then(|m| m.modified()).ok()
- },
- last_project_check: std::time::Instant::now(),
- last_inspector_check: std::time::Instant::now(),
- last_inspector_update: std::time::Instant::now(),
- needs_autosave: false,
- last_autosave_time: std::time::Instant::now(),
- window_x: 0,
- window_y: 0,
- active_menu_cloud_pid: None,
- active_menu_cloud_idx: None,
- uniform_background: settings.uniform_background,
- network_opacity: settings.network_opacity,
- last_design_mod_time: {
- let design_path = DesignSettings::file_path();
- std::fs::metadata(&design_path).and_then(|m| m.modified()).ok()
- },
- last_config_mod_time: {
- let paths = [
- "/home/lsgalante/.config/cce/config.toml",
- "/home/lsgalante/.config/ccec/config.toml",
- ];
- let mut mod_time = None;
- for path in &paths {
- if let Ok(m) = std::fs::metadata(path) {
- if let Ok(t) = m.modified() {
- mod_time = Some(t);
- break;
- }
- }
- }
- mod_time
- },
- floating_network_layout: (18.0, 44.0, 400.0, 710.0),
- is_resizing_network: None,
- drag_start_rect: (0.0, 0.0, 0.0, 0.0),
- drag_start_mouse: (0.0, 0.0),
- floating_param_width: 300.0,
- is_resizing_param: false,
- drag_start_param_w: 0.0,
- floating_spreadsheet_height: 250.0,
- is_resizing_spreadsheet: false,
- drag_start_spreadsheet_h: 0.0,
- paginator_page_widgets: Vec::new(),
- last_paginator_menubar: None,
- loaded_project_path: None,
- last_saved_root_json: serde_json::to_string(&fs_root).unwrap_or_default(),
- recent_files,
- recent_files_list,
- recent_files_buttons,
- ui_context: clear_ui::context::UiContext::new(),
- };
-
- state.update_inertial_settings();
- state.update_window_title();
- colors::set_node_color([
- settings.node_color[0],
- settings.node_color[1],
- settings.node_color[2],
- 1.0,
- ]);
- state.sync_nodes();
- state.rebuild_scene_geometry();
- state.sync_grid_settings();
- state.widgets[RIGHT_MENUBAR_IDX].set_item_checked(2, 0, state.show_grid);
- state.widgets[RIGHT_MENUBAR_IDX].set_item_checked(2, 1, state.show_cube);
- state.widgets[RIGHT_MENUBAR_IDX].set_item_checked(2, 2, state.show_origin);
- state.widgets[RIGHT_MENUBAR_IDX].set_item_checked(2, 3, state.show_camera_pivot);
- state.widgets[LEFT_MENUBAR_IDX].set_item_checked(2, 2, state.circular_network_pane);
- state.widgets[LEFT_MENUBAR_IDX].set_item_checked(2, 3, state.detached_circular_network);
- state.widgets[HEADER_IDX].set_item_checked(2, 3, state.detached_circular_network);
- state.widgets[HEADER_IDX].set_item_checked(2, 4, state.show_network);
- state.widgets[HEADER_IDX].set_item_checked(2, 5, state.show_viewport);
- state.widgets[HEADER_IDX].set_item_checked(2, 6, state.show_parameters);
- state.widgets[HEADER_IDX].set_item_checked(2, 7, state.show_spreadsheet);
-
- state.rebuild_positions();
- state.apply_layout();
- state.update_panel_bounds();
- state.sync_pane_focus();
- state.upload_vertices();
- state
- }
-
- fn sync_grid_settings(&mut self) {
- let active_node_area_y = self.positions[CONTENT_IDX].1;
- let active_node_area_x = self.positions[CONTENT_IDX].0;
-
- self.widgets[CONTENT_IDX].set_show_network_grid(self.network_grid_visible);
- self.widgets[CONTENT_IDX].set_grid_sizes(self.grid_size_x, self.grid_size_y);
- self.widgets[CONTENT_IDX].set_skipped_sizes(self.skipped_row_h, self.skipped_col_w);
- self.widgets[CONTENT_IDX].set_grid_origin(active_node_area_x + self.pan_x, active_node_area_y + self.pan_y);
- self.widgets[CONTENT_IDX].set_grid_snap_enabled(self.grid_snap_enabled);
- if let Some(graph) = self.widgets[CONTENT_IDX].as_any_mut().downcast_mut::<clear_ui::widget::Graph>() {
- graph.set_uniform_background(self.uniform_background);
- graph.set_network_opacity(self.network_opacity);
- graph.set_cell_color(self.cell_color);
- graph.set_gap_color(self.gap_color);
- }
- if let Some(menubar) = self.widgets[LEFT_MENUBAR_IDX].as_any_mut().downcast_mut::<clear_ui::widget::MenuBar>() {
- menubar.set_network_opacity(self.network_opacity);
- }
- if let Some(breadcrumb) = self.widgets[BREADCRUMB_IDX].as_any_mut().downcast_mut::<clear_ui::widget::Breadcrumb>() {
- breadcrumb.set_network_opacity(self.network_opacity);
- }
- if let Some(plate) = self.widgets[NETWORK_PANEL_IDX].as_any_mut().downcast_mut::<clear_ui::widget::Plate>() {
- plate.set_network_opacity(self.network_opacity);
- }
- }
-
- fn update_inertial_settings(&mut self) {
- self.last_config_read = Instant::now();
- let config_path = "/home/lsgalante/.config/cce/config.toml";
-
- let mut enabled = true;
- let mut friction = 0.90;
- let mut speed = 1.0;
-
- if let Ok(content) = std::fs::read_to_string(config_path) {
- #[derive(serde::Deserialize)]
- struct InertialSection {
- inertial_scroll: Option<bool>,
- scroll_friction: Option<u16>,
- scroll_speed: Option<f32>,
- }
- #[derive(serde::Deserialize)]
- struct Config {
- inertial: Option<InertialSection>,
- }
- if let Ok(cfg) = toml::from_str::<Config>(&content) {
- if let Some(inertial) = cfg.inertial {
- if let Some(val) = inertial.inertial_scroll {
- enabled = val;
- }
- if let Some(friction_val) = inertial.scroll_friction {
- friction = (friction_val as f32 / 1000.0).clamp(0.1, 0.999);
- }
- if let Some(speed_val) = inertial.scroll_speed {
- speed = speed_val.clamp(0.01, 10.0);
- }
- }
- }
- }
-
- self.inertial_scroll_enabled = enabled;
- self.inertial_scroll_friction = friction;
- self.scroll_speed = speed;
- }
-
-
- fn zoom(&mut self, factor: f32, center: Option<(f32, f32)>) {
- self.pan_velocity_x = 0.0;
- self.pan_velocity_y = 0.0;
- self.is_scrolling_trackpad = false;
- self.scroll_accum_x = 0.0;
- self.scroll_accum_y = 0.0;
- let old_gx = self.grid_size_x;
- let old_gy = self.grid_size_y;
-
- let new_gx = (old_gx * factor).clamp(30.0, 500.0);
- let new_gy = (old_gy * factor).clamp(15.0, 250.0);
-
- if (new_gx - old_gx).abs() < 0.01 {
- return;
- }
-
- let old_row_h = self.skipped_row_h;
- let old_col_w = self.skipped_col_w;
-
- let node_area_y = self.positions[CONTENT_IDX].1;
- let (cx, cy) = match center {
- Some(pt) => pt,
- None => {
- let col = self.grid_cursor_col as f32;
- let row = self.grid_cursor_row as f32;
- (
- col * (old_gx + old_col_w) + 0.5 * old_gx + self.pan_x,
- row * (old_gy + old_row_h) + 0.5 * old_gy + self.pan_y + node_area_y,
- )
- }
- };
-
- let col_f = (cx - self.pan_x) / old_gx;
- let row_f = (cy - self.pan_y - node_area_y) / old_gy;
-
- self.pan_x = cx - col_f * new_gx;
- self.pan_y = cy - row_f * new_gy - node_area_y;
-
- self.grid_size_x = new_gx;
- self.grid_size_y = new_gy;
- self.skipped_row_h = old_row_h * (new_gy / old_gy);
- self.skipped_col_w = old_col_w * (new_gx / old_gx);
-
- self.sync_grid_settings();
- }
-
-
- fn keep_cursor_in_view(&mut self) {
- let (px, py, pw, ph) = self.positions[CONTENT_IDX];
-
- let cx = px + self.grid_cursor_col as f32 * (self.grid_size_x + self.skipped_col_w) + self.pan_x;
- let cy = py + self.grid_cursor_row as f32 * (self.grid_size_y + self.skipped_row_h) + self.pan_y;
- let cw = self.grid_size_x;
- let ch = self.grid_size_y;
-
- if cx < px {
- self.pan_x -= cx - px;
- } else if cx + cw > px + pw {
- self.pan_x -= cx + cw - (px + pw);
- }
-
- if cy < py {
- self.pan_y -= cy - py;
- } else if cy + ch > py + ph {
- self.pan_y -= cy + ch - (py + ph);
- }
- self.sync_grid_settings();
- }
-
- fn rebuild_positions(&mut self) {
- self.clamp_splitters();
- self.widgets[LEFT_MENUBAR_IDX].set_center_items(self.circular_network_pane);
-
- let body_h = self.body_h();
-
- if self.is_detached_network {
- let cx = self.width / 2.0;
- let cy = self.height / 2.0;
- let r = (self.width.min(self.height) / 2.0 - 10.0).max(50.0);
-
- self.circular_network_layout.x = cx;
- self.circular_network_layout.y = cy;
- self.circular_network_layout.r = r;
-
- self.positions[0] = (0.0, 0.0, 0.0, 0.0);
- self.positions[LEFT_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
- if let Some(menubar) = self.widgets[LEFT_MENUBAR_IDX].as_any_mut().downcast_mut::<clear_ui::widget::MenuBar>() {
- menubar.set_curved_circle(None);
- }
- self.positions[BREADCRUMB_IDX] = (cx - r, cy - r + 45.0, 2.0 * r, BREADCRUMB_H);
- self.positions[CONTENT_IDX] = (cx - r, cy - r + 45.0 + BREADCRUMB_H, 2.0 * r, 2.0 * r - (45.0 + BREADCRUMB_H));
- self.positions[NETWORK_PANEL_IDX] = (cx - r, cy - r, 2.0 * r, 2.0 * r);
- self.widgets[NETWORK_PANEL_IDX].set_rect(cx - r, cy - r, 2.0 * r, 2.0 * r);
- if let Some(plate) = self.widgets[NETWORK_PANEL_IDX].as_any_mut().downcast_mut::<clear_ui::widget::Plate>() {
- plate.set_curved_circle(Some((cx, cy, r)));
- }
- self.positions[SPLITTER1_IDX] = (0.0, 0.0, 0.0, 0.0);
- self.positions[SPLITTER2_IDX] = (0.0, 0.0, 0.0, 0.0);
- self.positions[PARAM_IDX] = (0.0, 0.0, 0.0, 0.0);
- self.positions[VIEWPORT_IDX] = (0.0, 0.0, 0.0, 0.0);
- self.positions[RIGHT_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
- self.positions[SPREADSHEET_IDX] = (0.0, 0.0, 0.0, 0.0);
- self.positions[SPREADSHEET_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
- self.positions[CANVAS_IDX] = (0.0, 0.0, 0.0, 0.0);
- self.positions[PARAM_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
- self.positions[STATUS_IDX] = (0.0, 0.0, 0.0, 0.0);
- self.positions[NODE_PALETTE_IDX] = (0.0, 0.0, self.width, self.height);
-
- self.widgets[0].set_visible(false);
- self.widgets[STATUS_IDX].set_visible(false);
- self.widgets[CONTENT_IDX].set_visible(true);
- self.widgets[NETWORK_PANEL_IDX].set_visible(true);
- self.widgets[LEFT_MENUBAR_IDX].set_visible(false);
- self.widgets[BREADCRUMB_IDX].set_visible(true);
- self.widgets[SPLITTER1_IDX].set_visible(false);
- self.widgets[SPLITTER2_IDX].set_visible(false);
- self.widgets[VIEWPORT_IDX].set_visible(false);
- self.widgets[RIGHT_MENUBAR_IDX].set_visible(false);
- self.widgets[PARAM_IDX].set_visible(false);
- self.widgets[PARAM_MENUBAR_IDX].set_visible(false);
- self.widgets[SPREADSHEET_IDX].set_visible(false);
- self.widgets[SPREADSHEET_MENUBAR_IDX].set_visible(false);
- } else if self.detached_circular_network {
- // Parent process: Network pane is detached (hidden from main window)
- let left_visible = false;
- let viewport_visible = self.show_viewport;
- let spreadsheet_visible = self.show_spreadsheet;
- let center_visible = viewport_visible || spreadsheet_visible;
- let right_visible = self.show_parameters;
-
- let (_col_l_x, _col_l_w, _s1_x, _s1_w, col_c_x, col_c_w, s2_x, s2_w, col_r_x, col_r_w) =
- match (left_visible, center_visible, right_visible) {
- (false, true, true) => {
- let c_w = self.splitter_layout.splitter2_x;
- let s2 = c_w;
- let r_x = s2 + SPLITTER_W;
- let r_w = (self.width - r_x).max(0.0);
- (0.0, 0.0, 0.0, 0.0, 0.0, c_w, s2, SPLITTER_W, r_x, r_w)
- }
- (false, true, false) => {
- (0.0, 0.0, 0.0, 0.0, 0.0, self.width, 0.0, 0.0, 0.0, 0.0)
- }
- (false, false, true) => {
- (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, self.width)
- }
- _ => {
- (0.0, 0.0, 0.0, 0.0, 0.0, self.width, 0.0, 0.0, 0.0, 0.0)
- }
- };
-
- let mut vp_y = 0.0;
- let mut vp_h = 0.0;
- let mut sp_menub_y = 0.0;
- let mut sp_menub_h = 0.0;
- let mut sp_y = 0.0;
- let mut sp_h = 0.0;
-
- if center_visible {
- if viewport_visible && spreadsheet_visible {
- let viewport_h = body_h * 2.0 / 3.0;
- let spreadsheet_h = body_h - viewport_h;
- vp_y = HEADER_H;
- vp_h = viewport_h;
- sp_menub_y = 0.0;
- sp_menub_h = 0.0;
- sp_y = HEADER_H + viewport_h;
- sp_h = spreadsheet_h;
- } else if viewport_visible {
- vp_y = HEADER_H;
- vp_h = body_h;
- } else if spreadsheet_visible {
- sp_menub_y = 0.0;
- sp_menub_h = 0.0;
- sp_y = HEADER_H;
- sp_h = body_h;
- }
- }
-
- self.positions[0] = (0.0, 0.0, self.width, HEADER_H);
- self.positions[LEFT_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
- self.positions[BREADCRUMB_IDX] = (0.0, 0.0, 0.0, 0.0);
- self.positions[CONTENT_IDX] = (0.0, 0.0, 0.0, 0.0);
- self.positions[NETWORK_PANEL_IDX] = (0.0, 0.0, 0.0, 0.0);
- self.widgets[NETWORK_PANEL_IDX].set_rect(0.0, 0.0, 0.0, 0.0);
- self.positions[SPLITTER1_IDX] = (0.0, 0.0, 0.0, 0.0);
- self.positions[SPLITTER2_IDX] = (s2_x, HEADER_H, s2_w, body_h);
- self.positions[PARAM_IDX] = (col_r_x, HEADER_H, col_r_w, body_h);
-
- self.positions[VIEWPORT_IDX] = (col_c_x, vp_y, col_c_w, vp_h);
- self.positions[RIGHT_MENUBAR_IDX] = (col_c_x, vp_y, col_c_w, if viewport_visible { MENUBAR_H } else { 0.0 });
-
- self.positions[SPREADSHEET_MENUBAR_IDX] = (col_c_x, sp_menub_y, col_c_w, sp_menub_h);
- self.positions[SPREADSHEET_IDX] = (col_c_x, sp_y, col_c_w, sp_h);
-
- self.positions[CANVAS_IDX] = (0.0, HEADER_H, self.width, body_h);
- self.positions[PARAM_MENUBAR_IDX] = (col_r_x, HEADER_H, col_r_w, if right_visible { MENUBAR_H } else { 0.0 });
- self.positions[STATUS_IDX] = (0.0, self.height - STATUS_H, self.width, STATUS_H);
- self.positions[NODE_PALETTE_IDX] = (0.0, 0.0, self.width, self.height);
-
- self.widgets[0].set_visible(true);
- self.widgets[STATUS_IDX].set_visible(false);
- self.widgets[CONTENT_IDX].set_visible(false);
- self.widgets[NETWORK_PANEL_IDX].set_visible(false);
- self.widgets[LEFT_MENUBAR_IDX].set_visible(false);
- self.widgets[BREADCRUMB_IDX].set_visible(false);
- self.widgets[SPLITTER1_IDX].set_visible(false);
- self.widgets[SPLITTER2_IDX].set_visible(s2_w > 0.0);
- self.widgets[VIEWPORT_IDX].set_visible(viewport_visible);
- self.widgets[RIGHT_MENUBAR_IDX].set_visible(viewport_visible);
- self.widgets[PARAM_IDX].set_visible(right_visible);
- self.widgets[PARAM_MENUBAR_IDX].set_visible(right_visible);
- self.widgets[SPREADSHEET_IDX].set_visible(spreadsheet_visible);
- self.widgets[SPREADSHEET_MENUBAR_IDX].set_visible(spreadsheet_visible);
- } else {
- let paginator_w = self.widgets[PAGINATOR_IDX].sidebar_w();
- let body_h = self.height - STATUS_H;
- self.positions[0] = (0.0, 0.0, 0.0, 0.0);
- if self.circular_network_pane {
- let left_visible = false;
- let viewport_visible = self.show_viewport;
- let spreadsheet_visible = self.show_spreadsheet;
- let center_visible = viewport_visible || spreadsheet_visible;
- let right_visible = self.show_parameters;
-
- let (_col_l_x, _col_l_w, _s1_x, _s1_w, col_c_x, col_c_w, s2_x, s2_w, col_r_x, col_r_w) =
- match (left_visible, center_visible, right_visible) {
- (false, true, true) => {
- let s2 = self.splitter_layout.splitter2_x.max(paginator_w + MIN_COLUMN);
- let viewport_w = s2 - paginator_w;
- let r_x = s2 + SPLITTER_W;
- let r_w = (self.width - r_x).max(0.0);
- (0.0, 0.0, 0.0, 0.0, paginator_w, viewport_w, s2, SPLITTER_W, r_x, r_w)
- }
- (false, true, false) => {
- (0.0, 0.0, 0.0, 0.0, paginator_w, self.width - paginator_w, 0.0, 0.0, 0.0, 0.0)
- }
- (false, false, true) => {
- (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, paginator_w, self.width - paginator_w)
- }
- _ => {
- (0.0, 0.0, 0.0, 0.0, paginator_w, self.width - paginator_w, 0.0, 0.0, 0.0, 0.0)
- }
- };
-
- let mut vp_y = 0.0;
- let mut vp_h = 0.0;
- let mut sp_menub_y = 0.0;
- let mut sp_menub_h = 0.0;
- let mut sp_y = 0.0;
- let mut sp_h = 0.0;
-
- if center_visible {
- if viewport_visible && spreadsheet_visible {
- let viewport_h = body_h * 2.0 / 3.0;
- let spreadsheet_h = body_h - viewport_h;
- vp_y = 0.0;
- vp_h = viewport_h;
- sp_menub_y = 0.0;
- sp_menub_h = 0.0;
- sp_y = viewport_h;
- sp_h = spreadsheet_h;
- } else if viewport_visible {
- vp_y = 0.0;
- vp_h = body_h;
- } else if spreadsheet_visible {
- sp_menub_y = 0.0;
- sp_menub_h = 0.0;
- sp_y = 0.0;
- sp_h = body_h;
- }
- }
-
- if self.widgets[NETWORK_PANEL_IDX].is_dragging() {
- let (px, py, _pw, _ph) = self.widgets[NETWORK_PANEL_IDX].rect();
- let r = self.circular_network_layout.r;
- self.circular_network_layout.x = px + r;
- self.circular_network_layout.y = py + r;
- }
-
- let gap = 18.0;
- let max_r = ((self.width - paginator_w - 2.0 * gap).min(self.height - STATUS_H - 2.0 * gap) / 2.0).max(50.0);
- self.circular_network_layout.r = self.circular_network_layout.r.clamp(50.0, max_r);
-
- let r = self.circular_network_layout.r;
- let min_x = paginator_w + gap + r;
- let max_x = (self.width - gap - r).max(min_x);
- self.circular_network_layout.x = self.circular_network_layout.x.clamp(min_x, max_x);
-
- let min_y = gap + r;
- let max_y = (self.height - STATUS_H - gap - r).max(min_y);
- self.circular_network_layout.y = self.circular_network_layout.y.clamp(min_y, max_y);
-
- let cx = self.circular_network_layout.x;
- let cy = self.circular_network_layout.y;
-
- self.positions[LEFT_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
- if let Some(menubar) = self.widgets[LEFT_MENUBAR_IDX].as_any_mut().downcast_mut::<clear_ui::widget::MenuBar>() {
- menubar.set_curved_circle(None);
- }
- self.positions[BREADCRUMB_IDX] = (cx - r, cy - r + 45.0, 2.0 * r, BREADCRUMB_H);
- self.positions[CONTENT_IDX] = (cx - r, cy - r + 45.0 + BREADCRUMB_H, 2.0 * r, 2.0 * r - (45.0 + BREADCRUMB_H));
- self.positions[NETWORK_PANEL_IDX] = (cx - r, cy - r, 2.0 * r, 2.0 * r);
- self.widgets[NETWORK_PANEL_IDX].set_rect(cx - r, cy - r, 2.0 * r, 2.0 * r);
- if let Some(plate) = self.widgets[NETWORK_PANEL_IDX].as_any_mut().downcast_mut::<clear_ui::widget::Plate>() {
- plate.set_curved_circle(Some((cx, cy, r)));
- }
- self.widgets[NETWORK_PANEL_IDX].set_drag_bounds(0.0, 0.0, self.width, self.height);
- self.positions[SPLITTER1_IDX] = (0.0, 0.0, 0.0, 0.0);
- self.positions[SPLITTER2_IDX] = (s2_x, 0.0, s2_w, body_h);
- self.positions[PARAM_IDX] = (col_r_x, 0.0, col_r_w, body_h);
-
- self.positions[VIEWPORT_IDX] = (col_c_x, vp_y, col_c_w, vp_h);
- self.positions[RIGHT_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
-
- self.positions[SPREADSHEET_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
- self.positions[SPREADSHEET_IDX] = (col_c_x, sp_y, col_c_w, sp_h);
-
- self.positions[CANVAS_IDX] = (0.0, 0.0, self.width, body_h);
- self.positions[PARAM_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
- self.positions[STATUS_IDX] = (0.0, self.height - STATUS_H, self.width, STATUS_H);
- self.positions[NODE_PALETTE_IDX] = (0.0, 0.0, self.width, self.height);
-
- self.positions[PAGINATOR_IDX] = (0.0, 0.0, paginator_w, self.height - STATUS_H);
- self.widgets[PAGINATOR_IDX].set_rect(0.0, 0.0, paginator_w, self.height - STATUS_H);
-
- self.widgets[0].set_visible(false);
- self.widgets[STATUS_IDX].set_visible(false);
- self.widgets[CONTENT_IDX].set_visible(self.show_network);
- self.widgets[NETWORK_PANEL_IDX].set_visible(self.show_network);
- self.widgets[LEFT_MENUBAR_IDX].set_visible(false);
- self.widgets[BREADCRUMB_IDX].set_visible(self.show_network);
- self.widgets[SPLITTER1_IDX].set_visible(false);
- self.widgets[SPLITTER2_IDX].set_visible(s2_w > 0.0);
- self.widgets[VIEWPORT_IDX].set_visible(viewport_visible);
- self.widgets[RIGHT_MENUBAR_IDX].set_visible(false);
- self.widgets[PARAM_IDX].set_visible(right_visible);
- self.widgets[PARAM_MENUBAR_IDX].set_visible(false);
- self.widgets[SPREADSHEET_IDX].set_visible(spreadsheet_visible);
- self.widgets[SPREADSHEET_MENUBAR_IDX].set_visible(false);
- self.widgets[PAGINATOR_IDX].set_visible(true);
- } else {
- if !self.circular_network_pane && self.widgets[NETWORK_PANEL_IDX].is_dragging() {
- let (px, py, _pw, _ph) = self.widgets[NETWORK_PANEL_IDX].rect();
- self.floating_network_layout.0 = px;
- self.floating_network_layout.1 = py;
- }
-
- let gap = 18.0_f32;
- let (mut _fx, mut _fy, mut fw, mut _fh) = self.floating_network_layout;
- fw = fw.clamp(150.0, (self.width - paginator_w - 2.0 * gap).max(150.0));
- let fx = paginator_w + gap;
- let fy = gap;
- let fh = (self.height - STATUS_H - 2.0 * gap).max(100.0);
- self.floating_network_layout = (fx, fy, fw, fh);
-
- let param_w = self.floating_param_width.clamp(150.0, (self.width - paginator_w - 2.0 * gap).max(150.0));
- self.floating_param_width = param_w;
- let param_x = self.width - gap - param_w;
- let param_y = gap;
- let param_h = (self.height - STATUS_H - 2.0 * gap).max(100.0);
-
- let ss_x = if self.show_network { fx + fw + gap } else { paginator_w + gap };
- let ss_w_end = if self.show_parameters { param_x - gap } else { self.width - gap };
- let ss_w = (ss_w_end - ss_x).max(150.0);
- let ss_y_end = self.height - STATUS_H - gap;
- let ss_h = self.floating_spreadsheet_height.clamp(100.0, (ss_y_end - gap).max(100.0));
- let ss_y = ss_y_end - ss_h;
- self.floating_spreadsheet_height = ss_h;
-
- let viewport_visible = self.show_viewport;
- let spreadsheet_visible = self.show_spreadsheet;
- let right_visible = self.show_parameters;
-
- let col_c_x = paginator_w;
- let col_c_w = self.width - paginator_w;
- let vp_y = 0.0;
- let vp_h = if viewport_visible { body_h } else { 0.0 };
-
- let (px, py, pw, ph) = if self.show_network {
- (fx, fy, fw, fh)
- } else {
- (0.0, 0.0, 0.0, 0.0)
- };
-
- if let Some(menubar) = self.widgets[LEFT_MENUBAR_IDX].as_any_mut().downcast_mut::<clear_ui::widget::MenuBar>() {
- menubar.set_curved_circle(None);
- }
-
- let mb_h = 0.0;
- let bc_h = if self.show_network { BREADCRUMB_H } else { 0.0 };
- let content_h = (ph - mb_h - bc_h).max(0.0);
-
- self.positions[CONTENT_IDX] = (px, py + mb_h + bc_h, pw, content_h);
- self.positions[NETWORK_PANEL_IDX] = (px, py, pw, ph);
- self.widgets[NETWORK_PANEL_IDX].set_rect(px, py, pw, ph);
- if let Some(plate) = self.widgets[NETWORK_PANEL_IDX].as_any_mut().downcast_mut::<clear_ui::widget::Plate>() {
- plate.set_curved_circle(None);
- }
- self.positions[SPLITTER1_IDX] = (0.0, 0.0, 0.0, 0.0);
- self.positions[SPLITTER2_IDX] = (0.0, 0.0, 0.0, 0.0);
- let p_rect = if self.show_parameters { (param_x, param_y, param_w, param_h) } else { (0.0, 0.0, 0.0, 0.0) };
- self.positions[PARAM_IDX] = p_rect;
- self.widgets[PARAM_IDX].set_rect(p_rect.0, p_rect.1, p_rect.2, p_rect.3);
-
- self.positions[VIEWPORT_IDX] = (col_c_x, vp_y, col_c_w, vp_h);
- self.positions[RIGHT_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
- self.positions[BREADCRUMB_IDX] = (px, py + mb_h, pw, bc_h);
- self.positions[LEFT_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
-
- self.positions[SPREADSHEET_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
- self.positions[SPREADSHEET_IDX] = if spreadsheet_visible { (ss_x, ss_y, ss_w, ss_h) } else { (0.0, 0.0, 0.0, 0.0) };
- self.widgets[SPREADSHEET_IDX].set_rect(
- self.positions[SPREADSHEET_IDX].0,
- self.positions[SPREADSHEET_IDX].1,
- self.positions[SPREADSHEET_IDX].2,
- self.positions[SPREADSHEET_IDX].3,
- );
-
- self.positions[CANVAS_IDX] = (0.0, 0.0, self.width, body_h);
- self.positions[PARAM_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
- self.positions[STATUS_IDX] = (0.0, self.height - STATUS_H, self.width, STATUS_H);
- self.positions[NODE_PALETTE_IDX] = (0.0, 0.0, self.width, self.height);
-
- self.positions[PAGINATOR_IDX] = (0.0, 0.0, paginator_w, self.height - STATUS_H);
- self.widgets[PAGINATOR_IDX].set_rect(0.0, 0.0, paginator_w, self.height - STATUS_H);
-
- self.widgets[0].set_visible(false);
- self.widgets[STATUS_IDX].set_visible(false);
- self.widgets[CONTENT_IDX].set_visible(self.show_network);
- self.widgets[NETWORK_PANEL_IDX].set_visible(self.show_network);
- self.widgets[LEFT_MENUBAR_IDX].set_visible(false);
- self.widgets[BREADCRUMB_IDX].set_visible(self.show_network);
- self.widgets[SPLITTER1_IDX].set_visible(false);
- self.widgets[SPLITTER2_IDX].set_visible(false);
- self.widgets[VIEWPORT_IDX].set_visible(viewport_visible);
- self.widgets[RIGHT_MENUBAR_IDX].set_visible(false);
- self.widgets[PARAM_IDX].set_visible(right_visible);
- self.widgets[PARAM_MENUBAR_IDX].set_visible(false);
- self.widgets[SPREADSHEET_IDX].set_visible(spreadsheet_visible);
- self.widgets[SPREADSHEET_MENUBAR_IDX].set_visible(false);
- self.widgets[PAGINATOR_IDX].set_visible(true);
- }
- }
-
- if !self.is_detached_network {
- let mut active_menubar = self.focused_pane;
- if active_menubar == LEFT_MENUBAR_IDX && !self.show_network {
- active_menubar = HEADER_IDX;
- }
- if active_menubar == RIGHT_MENUBAR_IDX && !self.show_viewport {
- active_menubar = HEADER_IDX;
- }
- if active_menubar == PARAM_MENUBAR_IDX && !self.show_parameters {
- active_menubar = HEADER_IDX;
- }
- if active_menubar == SPREADSHEET_MENUBAR_IDX && !self.show_spreadsheet {
- active_menubar = HEADER_IDX;
- }
-
- if active_menubar != self.focused_pane {
- self.focused_pane = active_menubar;
- }
-
- let menubars = [
- HEADER_IDX,
- LEFT_MENUBAR_IDX,
- RIGHT_MENUBAR_IDX,
- PARAM_MENUBAR_IDX,
- SPREADSHEET_MENUBAR_IDX,
- ];
- for &idx in &menubars {
- self.positions[idx] = (0.0, 0.0, 0.0, 0.0);
- self.widgets[idx].set_visible(false);
- }
- }
- self.positions[PARAM_PLATE_IDX] = self.positions[PARAM_IDX];
- let p_visible = self.widgets[PARAM_IDX].visible();
- self.widgets[PARAM_PLATE_IDX].set_visible(p_visible);
- }
-
-
- fn apply_layout(&mut self) {
- for (i, pos) in self.positions.iter().enumerate() {
- if let Some(widget) = self.widgets.get_mut(i) {
- if widget.is_dragging() { continue; }
- let (x, y, w, h) = *pos;
- widget.set_rect(x, y, w, h);
- }
- }
- self.update_recent_files_layout();
- }
-
- fn update_panel_bounds(&mut self) {
- // Unbounded 2D canvas - no clamping
- }
-
- fn update_paginator(&mut self) {
- let active_menubar = self.focused_pane;
- let title = self.widgets[active_menubar].label().unwrap_or_default();
- let mut label_name = if let Some(colon_idx) = title.find(':') {
- title.split_at(colon_idx + 1).1.trim().to_uppercase()
- } else {
- title.to_uppercase()
- };
- if label_name.ends_with(" MENU BAR") {
- label_name = label_name.replace(" MENU BAR", "");
- }
- self.widgets[PAGINATOR_IDX].set_sidebar_label(Some(label_name));
-
- let menu_names = self.widgets[active_menubar].menu_names();
- let menu_items = self.widgets[active_menubar].menu_items_list();
- let menu_checked = self.widgets[active_menubar].menu_checked_list();
-
- let num_pages = menu_names.len();
- self.widgets[PAGINATOR_IDX].set_pages_with_items(menu_names.clone(), menu_items.clone());
-
- // Check if we need to rebuild paginator page widgets.
- let need_rebuild = self.last_paginator_menubar != Some(active_menubar)
- || self.paginator_page_widgets.len() != num_pages;
-
- if need_rebuild {
- self.last_paginator_menubar = Some(active_menubar);
- self.paginator_page_widgets.clear();
- self.paginator_page_widgets.resize_with(num_pages, Vec::new);
-
- for (page_idx, page_name) in menu_names.iter().enumerate() {
- if page_name == "Settings" {
- if active_menubar == LEFT_MENUBAR_IDX {
- // 0: Snap to Grid (Checkbox)
- let mut cb = Checkbox::new().with_label("Snap to Grid");
- cb.set_checked(self.grid_snap_enabled);
- cb.set_rect(0.0, 0.0, 0.0, 42.0);
- self.paginator_page_widgets[page_idx].push(Box::new(cb));
-
- // 1: Grid Visible (Checkbox)
- let mut cb = Checkbox::new().with_label("Grid Visible");
- cb.set_checked(self.network_grid_visible);
- cb.set_rect(0.0, 0.0, 0.0, 42.0);
- self.paginator_page_widgets[page_idx].push(Box::new(cb));
-
- // 2: Grid X (Spinbox, range 10-200)
- let mut sb = Spinbox::new(self.grid_size_x as i32, 10, 200, 1).with_label("Grid X");
- sb.set_rect(0.0, 0.0, 0.0, 42.0);
- self.paginator_page_widgets[page_idx].push(Box::new(sb));
-
- // 3: Grid Y (Spinbox, range 5-100)
- let mut sb = Spinbox::new(self.grid_size_y as i32, 5, 100, 1).with_label("Grid Y");
- sb.set_rect(0.0, 0.0, 0.0, 42.0);
- self.paginator_page_widgets[page_idx].push(Box::new(sb));
-
- // 4: Skipped Row H (Spinbox, range 0-150)
- let mut sb = Spinbox::new(self.skipped_row_h as i32, 0, 150, 1).with_label("Skipped Row H");
- sb.set_rect(0.0, 0.0, 0.0, 42.0);
- self.paginator_page_widgets[page_idx].push(Box::new(sb));
-
- // 5: Skipped Col W (Spinbox, range 0-150)
- let mut sb = Spinbox::new(self.skipped_col_w as i32, 0, 150, 1).with_label("Skipped Col W");
- sb.set_rect(0.0, 0.0, 0.0, 42.0);
- self.paginator_page_widgets[page_idx].push(Box::new(sb));
-
- // 6: Uniform Background (Checkbox)
- let mut cb = Checkbox::new().with_label("Uniform Background");
- cb.set_checked(self.uniform_background);
- cb.set_rect(0.0, 0.0, 0.0, 42.0);
- self.paginator_page_widgets[page_idx].push(Box::new(cb));
-
- // 7: Opacity (Slider, range 0.0-1.0)
- let mut sl = Slider::new().with_range(0.0, 1.0).with_value(self.network_opacity).with_label("Opacity").with_readout(true).with_scroll(true);
- sl.set_rect(0.0, 0.0, 0.0, 42.0);
- self.paginator_page_widgets[page_idx].push(Box::new(sl));
-
- // 8: Node Color (ColorSelector)
- let mut cs = ColorSelector::new([
- (self.node_color[0] * 255.0) as u8,
- (self.node_color[1] * 255.0) as u8,
- (self.node_color[2] * 255.0) as u8,
- ]).with_label("Node Color");
- cs.set_rect(0.0, 0.0, 0.0, 42.0);
- self.paginator_page_widgets[page_idx].push(Box::new(cs));
-
- // 9: Cell Color (ColorSelector)
- let mut cs = ColorSelector::new([
- (self.cell_color[0] * 255.0) as u8,
- (self.cell_color[1] * 255.0) as u8,
- (self.cell_color[2] * 255.0) as u8,
- ]).with_label("Cell Color");
- cs.set_rect(0.0, 0.0, 0.0, 42.0);
- self.paginator_page_widgets[page_idx].push(Box::new(cs));
-
- // 10: Gap Color (ColorSelector)
- let mut cs = ColorSelector::new([
- (self.gap_color[0] * 255.0) as u8,
- (self.gap_color[1] * 255.0) as u8,
- (self.gap_color[2] * 255.0) as u8,
- ]).with_label("Gap Color");
- cs.set_rect(0.0, 0.0, 0.0, 42.0);
- self.paginator_page_widgets[page_idx].push(Box::new(cs));
- } else if active_menubar == RIGHT_MENUBAR_IDX {
- // 0: Show Grid Guide (Checkbox)
- let mut cb = Checkbox::new().with_label("Show Grid Guide");
- cb.set_checked(self.show_grid);
- cb.set_rect(0.0, 0.0, 0.0, 42.0);
- self.paginator_page_widgets[page_idx].push(Box::new(cb));
-
- // 1: Show Reference Cube (Checkbox)
- let mut cb = Checkbox::new().with_label("Show Reference Cube");
- cb.set_checked(self.show_cube);
- cb.set_rect(0.0, 0.0, 0.0, 42.0);
- self.paginator_page_widgets[page_idx].push(Box::new(cb));
-
- // 2: Show Origin Axes (Checkbox)
- let mut cb = Checkbox::new().with_label("Show Origin Axes");
- cb.set_checked(self.show_origin);
- cb.set_rect(0.0, 0.0, 0.0, 42.0);
- self.paginator_page_widgets[page_idx].push(Box::new(cb));
-
- // 3: Show Camera Pivot (Checkbox)
- let mut cb = Checkbox::new().with_label("Show Camera Pivot");
- cb.set_checked(self.show_camera_pivot);
- cb.set_rect(0.0, 0.0, 0.0, 42.0);
- self.paginator_page_widgets[page_idx].push(Box::new(cb));
-
- // 4: Grid Thickness (Spinbox, range 2-200, decimals 3)
- let mut sb = Spinbox::new((self.grid_thickness * 1000.0) as i32, 2, 200, 1).with_label("Grid Thickness").with_decimals(3);
- sb.set_rect(0.0, 0.0, 0.0, 42.0);
- self.paginator_page_widgets[page_idx].push(Box::new(sb));
-
- // 5: Origin Guide Size (Spinbox, range 1-50, decimals 1)
- let mut sb = Spinbox::new((self.origin_size * 10.0) as i32, 1, 50, 1).with_label("Origin Guide Size").with_decimals(1);
- sb.set_rect(0.0, 0.0, 0.0, 42.0);
- self.paginator_page_widgets[page_idx].push(Box::new(sb));
-
- // 6: Camera Pivot Size (Spinbox, range 1-50, decimals 1)
- let mut sb = Spinbox::new((self.camera_pivot_size * 10.0) as i32, 1, 50, 1).with_label("Camera Pivot Size").with_decimals(1);
- sb.set_rect(0.0, 0.0, 0.0, 42.0);
- self.paginator_page_widgets[page_idx].push(Box::new(sb));
-
- // 7, 8, 9: BG Color R, G, B (Spinbox, range 0-255)
- let mut sb = Spinbox::new((self.viewport_bg_color[0] * 255.0) as i32, 0, 255, 1).with_label("BG Color R");
- sb.set_rect(0.0, 0.0, 0.0, 42.0);
- self.paginator_page_widgets[page_idx].push(Box::new(sb));
- let mut sb = Spinbox::new((self.viewport_bg_color[1] * 255.0) as i32, 0, 255, 1).with_label("BG Color G");
- sb.set_rect(0.0, 0.0, 0.0, 42.0);
- self.paginator_page_widgets[page_idx].push(Box::new(sb));
- let mut sb = Spinbox::new((self.viewport_bg_color[2] * 255.0) as i32, 0, 255, 1).with_label("BG Color B");
- sb.set_rect(0.0, 0.0, 0.0, 42.0);
- self.paginator_page_widgets[page_idx].push(Box::new(sb));
-
- // 10, 11, 12: Grid Color R, G, B (Spinbox, range 0-255)
- let mut sb = Spinbox::new((self.grid_color[0] * 255.0) as i32, 0, 255, 1).with_label("Grid Color R");
- sb.set_rect(0.0, 0.0, 0.0, 42.0);
- self.paginator_page_widgets[page_idx].push(Box::new(sb));
- let mut sb = Spinbox::new((self.grid_color[1] * 255.0) as i32, 0, 255, 1).with_label("Grid Color G");
- sb.set_rect(0.0, 0.0, 0.0, 42.0);
- self.paginator_page_widgets[page_idx].push(Box::new(sb));
- let mut sb = Spinbox::new((self.grid_color[2] * 255.0) as i32, 0, 255, 1).with_label("Grid Color B");
- sb.set_rect(0.0, 0.0, 0.0, 42.0);
- self.paginator_page_widgets[page_idx].push(Box::new(sb));
- }
- } else {
- let items = &menu_items[page_idx];
- let checked_list = menu_checked.get(page_idx);
- for (item_idx, item_name) in items.iter().enumerate() {
- let is_checked = checked_list.and_then(|l| l.get(item_idx).copied().flatten());
- if let Some(checked_val) = is_checked {
- let mut cb = Checkbox::new().with_label(item_name);
- cb.set_checked(checked_val);
- self.paginator_page_widgets[page_idx].push(Box::new(cb));
- } else {
- let btn = Button::new(0.0, 0.0, 150.0, 24.0).with_label(item_name);
- self.paginator_page_widgets[page_idx].push(Box::new(btn));
- }
- }
-
- if menu_names[page_idx] == "File" {
- let mut recent_lbl = Label::new("Recent Files").with_font_size(11.0).with_color([0xd4, 0xd4, 0xd4]);
- recent_lbl.set_rect(0.0, 0.0, 150.0, 16.0);
- self.paginator_page_widgets[page_idx].push(Box::new(recent_lbl));
-
- let mut recent_list = ScrollingList::new(22.0, 2.0);
- recent_list.set_rect(0.0, 0.0, 150.0, 100.0);
- self.paginator_page_widgets[page_idx].push(Box::new(recent_list));
- }
- }
- }
-
- // Page widgets rebuild completed.
- }
-
- let sel_page = self.widgets[PAGINATOR_IDX].selected_page();
- self.widgets[PARAM_IDX].clear_children(&mut self.ui_context);
- if !self.widgets[PAGINATOR_IDX].is_page_hidden() {
- if sel_page < self.paginator_page_widgets.len() {
- for widget in &self.paginator_page_widgets[sel_page] {
- let ptr = &**widget as *const (dyn Element + 'static) as *mut (dyn Element + 'static);
- self.widgets[PARAM_IDX].add_child(ptr, &mut self.ui_context);
- }
- if menu_names.get(sel_page).map(|s| s.as_str()) == Some("File") {
- for btn in &mut self.recent_files_buttons {
- let ptr = btn as *mut Button as *mut (dyn Element + 'static);
- self.widgets[PARAM_IDX].add_child(ptr, &mut self.ui_context);
- }
- }
- }
- }
-
- let (px, py, pw, ph) = self.positions[PAGINATOR_IDX];
- self.widgets[PAGINATOR_IDX].set_rect(px, py, pw, ph);
-
- // Layout the parameters plate with the new children immediately.
- let (ppx, ppy, ppw, pph) = self.positions[PARAM_IDX];
- self.widgets[PARAM_IDX].set_rect(ppx, ppy, ppw, pph);
-
- self.update_recent_files_layout();
- }
-
- fn sync_context_dropdowns(&mut self) {
- let selected_idx = match self.focused_pane {
- LEFT_MENUBAR_IDX => 0,
- RIGHT_MENUBAR_IDX => 1,
- PARAM_MENUBAR_IDX => 2,
- SPREADSHEET_MENUBAR_IDX => 3,
- HEADER_IDX => 4,
- _ => return,
- };
- for &widget_idx in &[HEADER_IDX, LEFT_MENUBAR_IDX, RIGHT_MENUBAR_IDX, PARAM_MENUBAR_IDX, SPREADSHEET_MENUBAR_IDX, PAGINATOR_IDX] {
- self.widgets[widget_idx].set_context_selected(selected_idx);
- }
- }
-
- fn sync_pane_focus(&mut self) {
- for &menubar_idx in &[HEADER_IDX, LEFT_MENUBAR_IDX, RIGHT_MENUBAR_IDX, PARAM_MENUBAR_IDX, SPREADSHEET_MENUBAR_IDX] {
- self.widgets[menubar_idx].set_selected(menubar_idx == self.focused_pane);
- }
- if self.focused_pane != PARAM_MENUBAR_IDX {
- self.widgets[PARAM_IDX].unfocus();
- self.sync_parameters_to_project();
- }
- self.sync_context_dropdowns();
- self.update_paginator();
- }
-
- fn sync_layout(&mut self) {
- let left_visible = self.show_network && !self.circular_network_pane && !self.is_detached_network && !self.detached_circular_network;
- let center_visible = self.show_viewport || self.show_spreadsheet;
- let right_visible = self.show_parameters;
-
- if left_visible && center_visible && self.widgets[SPLITTER1_IDX].visible() && self.widgets[SPLITTER1_IDX].rect().2 > 0.0 {
- self.splitter_layout.splitter1_x = self.widgets[SPLITTER1_IDX].rect().0;
- }
- if right_visible && (center_visible || left_visible) && self.widgets[SPLITTER2_IDX].visible() && self.widgets[SPLITTER2_IDX].rect().2 > 0.0 {
- self.splitter_layout.splitter2_x = self.widgets[SPLITTER2_IDX].rect().0;
- }
- self.rebuild_positions();
- self.apply_layout();
- self.update_panel_bounds();
- self.sync_pane_focus();
- }
-
- fn execute_action(&mut self, action: Action) {
- let mut settings_changed = false;
- match action {
- Action::ToggleGrid => {
- self.show_grid = !self.show_grid;
- self.widgets[RIGHT_MENUBAR_IDX].set_item_checked(2, 0, self.show_grid);
- settings_changed = true;
- }
- Action::ToggleCube => {
- self.show_cube = !self.show_cube;
- self.widgets[RIGHT_MENUBAR_IDX].set_item_checked(2, 1, self.show_cube);
- settings_changed = true;
- }
- Action::ToggleOrigin => {
- self.show_origin = !self.show_origin;
- self.widgets[RIGHT_MENUBAR_IDX].set_item_checked(2, 2, self.show_origin);
- settings_changed = true;
- }
- Action::ToggleCameraPivot => {
- self.show_camera_pivot = !self.show_camera_pivot;
- self.widgets[RIGHT_MENUBAR_IDX].set_item_checked(2, 3, self.show_camera_pivot);
- settings_changed = true;
- }
- Action::ToggleSquareViewport => {
- self.square_viewport = !self.square_viewport;
- settings_changed = true;
- }
- Action::ToggleConfigure => {
- let target_pane = if self.show_network {
- LEFT_MENUBAR_IDX
- } else if self.show_viewport {
- RIGHT_MENUBAR_IDX
- } else {
- LEFT_MENUBAR_IDX
- };
- self.focused_pane = target_pane;
- self.widgets[PAGINATOR_IDX].set_page_hidden(false);
- let page_idx = if target_pane == LEFT_MENUBAR_IDX { 3 } else { 4 };
- self.widgets[PAGINATOR_IDX].set_selected_page(page_idx);
- self.sync_pane_focus();
- self.rebuild_positions();
- self.apply_layout();
- self.upload_vertices();
- return;
- }
- Action::ToggleSpreadsheet => {
- self.show_spreadsheet = !self.show_spreadsheet;
- self.widgets[SPREADSHEET_IDX].set_visible(self.show_spreadsheet);
- self.widgets[SPREADSHEET_MENUBAR_IDX].set_visible(self.show_spreadsheet);
- self.widgets[HEADER_IDX].set_item_checked(2, 7, self.show_spreadsheet);
- if !self.show_spreadsheet && self.focused_pane == SPREADSHEET_MENUBAR_IDX {
- self.focused_pane = get_next_visible_pane(
- self.focused_pane,
- self.show_network,
- self.show_viewport,
- self.show_parameters,
- self.show_spreadsheet,
- false,
- );
- }
- self.rebuild_positions();
- self.apply_layout();
- self.sync_pane_focus();
- self.sync_nodes();
- }
- Action::ToggleCircularPane => {
- self.circular_network_pane = !self.circular_network_pane;
- self.widgets[LEFT_MENUBAR_IDX].set_item_checked(2, 2, self.circular_network_pane);
- self.rebuild_positions();
- self.apply_layout();
- self.sync_grid_settings();
- }
- Action::DetachCircularWindow => {
- let default_proj_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("default_project.json");
- if let Err(e) = self.save_to_file(&default_proj_path) {
- eprintln!("Failed to save default project before detaching: {:?}", e);
- }
-
- self.detached_circular_network = !self.detached_circular_network;
- self.widgets[LEFT_MENUBAR_IDX].set_item_checked(2, 3, self.detached_circular_network);
- self.widgets[HEADER_IDX].set_item_checked(2, 3, self.detached_circular_network);
-
- if self.detached_circular_network {
- let _ = std::process::Command::new(std::env::current_exe().unwrap())
- .arg("--detached-network")
- .spawn();
- }
-
- self.rebuild_positions();
- self.apply_layout();
- self.sync_grid_settings();
- }
- Action::Save => {
- let path_opt = self.loaded_project_path.clone();
- if let Some(path) = path_opt {
- if let Err(e) = self.save_to_file(&path) {
- eprintln!("Failed to save project: {:?}", e);
- self.update_status_text(&format!("Failed to save: {:?}", e));
- } else {
- self.update_status_text(&format!("Project saved to {}", path.display()));
- self.add_recent_file(path);
- }
- } else {
- self.save_file_chooser();
- }
- }
- }
- if settings_changed {
- self.save_settings();
- }
- }
-
- fn read_panel_offsets(&mut self) {
- let updated_nodes = self.widgets[CONTENT_IDX].get_nodes();
- let dir = self.current_dir_mut();
- for (i, node) in updated_nodes.iter().enumerate() {
- if let Some(child) = dir.children.get_mut(i) {
- child.position = node.position;
- }
- }
- if let Some(sel_idx) = self.widgets[CONTENT_IDX].selected_node() {
- if let Some(node) = updated_nodes.get(sel_idx) {
- self.grid_cursor_col = node.position.0 as i32;
- self.grid_cursor_row = node.position.1 as i32;
- }
- }
- }
-
- fn sync_cursor_and_selection(&mut self) {
- if self.focused_widget != Some(CONTENT_IDX) {
- return;
- }
- let dir = self.current_dir();
- let mut node_at_cursor_idx = None;
- for (i, node) in dir.children.iter().enumerate() {
- if node.position.0 as i32 == self.grid_cursor_col && node.position.1 as i32 == self.grid_cursor_row {
- node_at_cursor_idx = Some(i);
- break;
- }
- }
-
- if let Some(idx) = node_at_cursor_idx {
- self.widgets[CONTENT_IDX].set_selected_node(Some(idx));
- if self.focused_widget != Some(CONTENT_IDX) {
- self.focused_widget = Some(CONTENT_IDX);
- }
- } else {
- self.widgets[CONTENT_IDX].set_selected_node(None);
- if self.focused_widget == Some(CONTENT_IDX) {
- self.focused_widget = None;
- }
- }
- }
-
-
- fn collect_vertices(&self, verts: &mut Vec<Vertex>) {
- verts.clear();
- let sw = self.width;
- let sh = self.height;
-
- let node_area_y = self.positions[CONTENT_IDX].1;
- let dialog_open = self.node_palette_visible;
- let show_cursor = self.drag_widget.is_none()
- && !dialog_open;
-
- let clip = if self.circular_network_pane {
- (
- self.circular_network_layout.x - self.circular_network_layout.r,
- self.circular_network_layout.y - self.circular_network_layout.r,
- self.circular_network_layout.x + self.circular_network_layout.r,
- self.circular_network_layout.y + self.circular_network_layout.r,
- )
- } else {
- let (cx, cy, cw, ch) = self.positions[CONTENT_IDX];
- (cx, cy, cx + cw, cy + ch)
- };
-
- let clip_circle_val = if self.circular_network_pane {
- [self.circular_network_layout.x * self.scale as f32, self.circular_network_layout.y * self.scale as f32, self.circular_network_layout.r * self.scale as f32]
- } else {
- [0.0, 0.0, 0.0]
- };
-
- let mut draw_order: Vec<usize> = (0..self.widgets.len()).collect();
- draw_order.sort_by_key(|&i| {
- if i == NETWORK_PANEL_IDX || i == PARAM_PLATE_IDX {
- -5
- } else if i == VIEWPORT_IDX || i == PARAM_IDX {
- -4
- } else {
- self.widgets[i].z_index()
- }
- });
- // println!("DEBUG_DRAW_ORDER: {:?}", draw_order.iter().map(|&i| (i, self.widgets[i].visible(), self.positions[i])).collect::<Vec<_>>());
-
- for &i in &draw_order {
- let w = &self.widgets[i];
- if !w.visible() {
- continue;
- }
- let is_network_part = i == CONTENT_IDX || i == LEFT_MENUBAR_IDX || i == BREADCRUMB_IDX || i == NETWORK_PANEL_IDX;
- let active_clip_circle = if is_network_part { clip_circle_val } else { [0.0, 0.0, 0.0] };
-
- if i == NETWORK_PANEL_IDX {
- if self.circular_network_pane {
- push_circle_vertices(
- self.circular_network_layout.x,
- self.circular_network_layout.y,
- self.circular_network_layout.r,
- sw,
- sh,
- w.color(),
- 64,
- active_clip_circle,
- verts,
- );
- push_circle_border_vertices(
- self.circular_network_layout.x,
- self.circular_network_layout.y,
- self.circular_network_layout.r,
- 3.0,
- sw,
- sh,
- [0.35, 0.65, 0.95, 0.80 * self.network_opacity],
- 64,
- active_clip_circle,
- verts,
- );
- } else {
- push_widget_vertices(w.as_ref(), sw, sh, active_clip_circle, verts);
- }
- } else if i == CONTENT_IDX {
- if !self.circular_network_pane {
- push_widget_vertices(w.as_ref(), sw, sh, active_clip_circle, verts);
- }
-
- for (qx, qy, qw, qh, qc) in w.extra_quads() {
- push_extra_quad_vertices_clipped(w.as_ref(), qx, qy, qw, qh, sw, sh, qc, clip, active_clip_circle, verts);
- }
- } else if i == LEFT_MENUBAR_IDX && self.circular_network_pane {
- let cx = self.circular_network_layout.x;
- let cy = self.circular_network_layout.y;
- let r = self.circular_network_layout.r;
-
- let bg_color = w.color();
- push_arc_background_vertices(
- cx, cy, r,
- MENUBAR_H,
- std::f32::consts::PI,
- 2.0 * std::f32::consts::PI,
- sw, sh,
- bg_color,
- 64,
- active_clip_circle,
- verts,
- );
-
- let border_color = [0.22, 0.22, 0.28, 0.90 * self.network_opacity];
- push_arc_background_vertices(
- cx, cy, r - MENUBAR_H,
- 1.5,
- std::f32::consts::PI,
- 2.0 * std::f32::consts::PI,
- sw, sh,
- border_color,
- 64,
- active_clip_circle,
- verts,
- );
-
- for (qx, qy, qw, qh, qc) in w.extra_quads() {
- push_extra_quad_vertices(w.as_ref(), qx, qy, qw, qh, sw, sh, qc, active_clip_circle, verts);
- }
- for (acx, acy, ar, ath, a_start, a_end, acolor) in w.extra_arcs() {
- push_arc_background_vertices(
- acx, acy, ar,
- ath,
- a_start, a_end,
- sw, sh,
- acolor,
- 64,
- active_clip_circle,
- verts,
- );
- }
- } else {
- push_widget_vertices(w.as_ref(), sw, sh, active_clip_circle, verts);
- if i == PAGINATOR_IDX {
- let eq = w.extra_quads();
- // println!("DEBUG_COLLECT: Paginator extra_quads len = {}", eq.len());
- for (qx, qy, qw, qh, qc) in eq {
- push_extra_quad_vertices(w.as_ref(), qx, qy, qw, qh, sw, sh, qc, active_clip_circle, verts);
- }
- } else {
- for (qx, qy, qw, qh, qc) in w.extra_quads() {
- push_extra_quad_vertices(w.as_ref(), qx, qy, qw, qh, sw, sh, qc, active_clip_circle, verts);
- }
- }
- for (acx, acy, ar, ath, a_start, a_end, acolor) in w.extra_arcs() {
- push_arc_background_vertices(
- acx, acy, ar,
- ath,
- a_start, a_end,
- sw, sh,
- acolor,
- 64,
- active_clip_circle,
- verts,
- );
- }
- }
-
-
- if i == CONTENT_IDX && show_cursor {
- let _cx = active_clip_circle[0] / self.scale as f32 - self.circular_network_layout.r + self.pan_x; // Wait, let's keep the exact cursor coordinates!
- let active_node_area_y = if self.circular_network_pane {
- self.circular_network_layout.y - self.circular_network_layout.r + 45.0 + BREADCRUMB_H
- } else {
- node_area_y
- };
- let active_node_area_x = self.positions[CONTENT_IDX].0;
- let cx = active_node_area_x + self.grid_cursor_col as f32 * (self.grid_size_x + self.skipped_col_w) + self.pan_x;
- let cy = active_node_area_y + self.grid_cursor_row as f32 * (self.grid_size_y + self.skipped_row_h) + self.pan_y;
- let cw = self.grid_size_x;
- let ch = self.grid_size_y;
-
- let bg_color = [0.15, 0.25, 0.45, 0.15];
- let border_color = [0.35, 0.65, 0.95, 0.60];
- let thickness = 2.0_f32;
-
- // Filled cursor background
- push_quad_vertices_clipped(
- cx + thickness,
- cy + thickness,
- cw - 2.0 * thickness,
- ch - 2.0 * thickness,
- sw,
- sh,
- bg_color,
- clip,
- active_clip_circle,
- verts,
- );
-
- // 4 border edges
- push_quad_vertices_clipped(cx, cy, cw, thickness, sw, sh, border_color, clip, active_clip_circle, verts);
- push_quad_vertices_clipped(cx, cy + ch - thickness, cw, thickness, sw, sh, border_color, clip, active_clip_circle, verts);
- push_quad_vertices_clipped(cx, cy + thickness, thickness, ch - 2.0 * thickness, sw, sh, border_color, clip, active_clip_circle, verts);
- push_quad_vertices_clipped(cx + cw - thickness, cy + thickness, thickness, ch - 2.0 * thickness, sw, sh, border_color, clip, active_clip_circle, verts);
- }
- }
- }
-
- fn upload_vertices(&mut self) {
- let mut verts = std::mem::take(&mut self.vertex_data);
- self.collect_vertices(&mut verts);
- self.vertex_count = verts.len() as u32;
- let data = bytemuck::cast_slice(&verts);
- let needed = data.len() as wgpu::BufferAddress;
- if needed > self.vertex_buffer.size() {
- self.vertex_buffer = self.wgpu_adapter.device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("Vertex Buffer"),
- size: needed,
- usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
- }
- self.wgpu_adapter.queue.write_buffer(&self.vertex_buffer, 0, data);
- self.vertex_data = verts;
- }
-
- fn rebuild_scene_geometry(&mut self) {
- let mut geom = network_sphere_vertices(&self.fs_root);
-
- fn collect_opencl_codes<'a>(node: &'a FsNode, codes: &mut Vec<&'a str>) {
- if node.node_type.eq_ignore_ascii_case("opencl") && node.geometry_visible {
- let code = node.params.iter()
- .find(|p| p.name.eq_ignore_ascii_case("code"))
- .map(|p| p.default.as_str())
- .unwrap_or("");
- codes.push(code);
- }
- for child in &node.children {
- collect_opencl_codes(child, codes);
- }
- }
- let mut opencl_codes = Vec::new();
- collect_opencl_codes(&self.fs_root, &mut opencl_codes);
-
- let mut ocl_error = None;
- for code in &opencl_codes {
- if !code.is_empty() {
- if let Err(e) = run_opencl_kernel(code, &mut geom) {
- ocl_error = Some(e);
- break;
- }
- }
- }
-
- if let Some(err) = ocl_error {
- self.update_status_text(&err);
- } else if !opencl_codes.is_empty() {
- self.update_status_text("OpenCL kernel executed successfully.");
- }
-
- let verts = geom.to_vertex3d_vec();
- self.vertex_count_spheres = verts.len() as u32;
- if !verts.is_empty() {
- let data = bytemuck::cast_slice(&verts);
- let needed = data.len() as wgpu::BufferAddress;
- if needed > self.vertex_buffer_spheres.size() {
- self.vertex_buffer_spheres = self.wgpu_adapter.device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("Sphere Node Vertex Buffer"),
- size: needed,
- usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
- }
- self.wgpu_adapter.queue.write_buffer(&self.vertex_buffer_spheres, 0, data);
- }
- }
-
- fn update_status_text(&mut self, text: &str) {
- self.widgets[STATUS_IDX].set_text(text);
- }
-
- fn prepare_text(&mut self) {
- // 1. Prepare text on all widgets using self.wgpu_adapter.font_system
- for w in &mut self.widgets {
- w.prepare_text(&mut self.wgpu_adapter.font_system);
- }
-
- // 2. Destructure self
- let splitter1_x = self.splitter_layout.splitter1_x;
- let height = self.height;
- let sw = self.width;
- let sh = self.height;
- let circular_network_pane = self.circular_network_pane;
- let network_circle_x = self.circular_network_layout.x;
- let network_circle_y = self.circular_network_layout.y;
- let network_circle_radius = self.circular_network_layout.r;
-
- let Self {
- ref mut wgpu_adapter,
- physical_width, physical_height, scale,
- ref widgets,
- ref curved_text_texture,
- ref mut curved_text_atlas,
- ref mut curved_text_renderer,
- ref mut curved_text_viewport,
- ref mut textured_vertex_buffer,
- ref mut textured_vertex_count,
- ..
- } = self;
-
- let clear_ui::backend::WgpuAdapter {
- ref device,
- ref queue,
- ref mut font_system,
- ref mut text_atlas,
- ref mut text_viewport,
- ref mut text_renderer,
- ref mut swash_cache,
- ..
- } = wgpu_adapter;
-
- let viewport = Resolution { width: *physical_width, height: *physical_height };
- text_viewport.update(queue, viewport);
- let s = *scale as f32;
-
- let mut areas: Vec<TextArea> = Vec::new();
-
- // Temporary storage for legacy buffers generated during this frame
- let mut legacy_buffers: Vec<Buffer> = Vec::new();
- let mut legacy_labels: Vec<TextLabel> = Vec::new();
- let mut legacy_bounds: Vec<TextBounds> = Vec::new();
-
- let mut curved_labels = Vec::new();
-
- for (i, w) in widgets.iter().enumerate() {
- if !w.visible() {
- continue;
- }
- let is_node = i == CONTENT_IDX;
- let is_network_part = i == CONTENT_IDX || i == LEFT_MENUBAR_IDX || i == BREADCRUMB_IDX || i == NETWORK_PANEL_IDX;
-
- let bounds = if is_node {
- let node_area_y = self.positions[CONTENT_IDX].1;
- if circular_network_pane {
- TextBounds {
- left: ((network_circle_x - network_circle_radius) * s) as i32,
- top: ((network_circle_y - network_circle_radius) * s) as i32,
- right: ((network_circle_x + network_circle_radius) * s) as i32,
- bottom: (((network_circle_y + network_circle_radius) * s) as i32).max(0),
- }
- } else {
- TextBounds {
- left: 0,
- top: (node_area_y * s) as i32,
- right: (splitter1_x * s) as i32,
- bottom: (((height - STATUS_H) * s) as i32).max(0),
- }
- }
- } else {
- TextBounds {
- left: 0,
- top: 0,
- right: *physical_width as i32,
- bottom: *physical_height as i32,
- }
- };
-
- let cached_items = w.get_text_items();
- let has_cached_items = !cached_items.is_empty();
- // if i == LEFT_MENUBAR_IDX {
- // eprintln!("DEBUG_PREPARE: i={} cached_items.len={} circular_network_pane={}", i, cached_items.len(), circular_network_pane);
- // }
- if has_cached_items && !(circular_network_pane && i == LEFT_MENUBAR_IDX) {
- for (buf, x, y, color) in cached_items {
- if circular_network_pane && is_network_part && i != LEFT_MENUBAR_IDX {
- let dx = x - network_circle_x;
- let dy = y - network_circle_y;
- let dist_sq = dx * dx + dy * dy;
- if dist_sq > network_circle_radius * network_circle_radius {
- continue;
- }
- }
- areas.push(TextArea {
- buffer: buf,
- left: (x * s).round(),
- top: (y * s).round(),
- scale: s,
- bounds,
- default_color: color,
- custom_glyphs: &[],
- });
- }
- }
- if !has_cached_items || (circular_network_pane && i == LEFT_MENUBAR_IDX) {
- let labels = w.text_labels_with_font_and_bounds(&self.ui_context);
- // if i == LEFT_MENUBAR_IDX {
- // eprintln!("DEBUG_PREPARE_ELSE: i={} labels.len={}", i, labels.len());
- // }
- for (label, font_opt, label_bounds) in labels {
- let mut is_curved = false;
- if circular_network_pane && i == LEFT_MENUBAR_IDX && label.text.chars().count() == 1 {
- let dx = label.x - network_circle_x;
- let dy = label.y - network_circle_y;
- let dist = (dx * dx + dy * dy).sqrt();
- // eprintln!("DEBUG_CURVED: label='{}' count={} dist={} req_min={} req_max={}", label.text, label.text.chars().count(), dist, network_circle_radius - 35.0, network_circle_radius + 5.0);
- if dist >= network_circle_radius - 35.0 && dist <= network_circle_radius + 5.0 {
- is_curved = true;
- }
- }
-
- if is_curved {
- curved_labels.push(label);
- } else {
- if circular_network_pane && is_network_part && i != LEFT_MENUBAR_IDX {
- let dx = label.x - network_circle_x;
- let dy = label.y - network_circle_y;
- let dist_sq = dx * dx + dy * dy;
- if dist_sq > network_circle_radius * network_circle_radius {
- continue;
- }
- }
- let mut item_bounds = bounds;
- if let Some([l, t, r, b]) = label_bounds {
- let pl = (l * s).round() as i32;
- let pt = (t * s).round() as i32;
- let pr = (r * s).round() as i32;
- let pb = (b * s).round() as i32;
- item_bounds = TextBounds {
- left: item_bounds.left.max(pl),
- top: item_bounds.top.max(pt),
- right: item_bounds.right.min(pr),
- bottom: item_bounds.bottom.min(pb),
- };
- }
- // if label.text.contains("Grid") || label.text.contains("Background") || label.text.contains("Snap") || label.text.contains("Opacity") {
- // eprintln!("DEBUG_PREPARE_LABEL: text='{}' x={} y={} font_size={} s={} left={} top={} bounds={:?}", label.text, label.x, label.y, label.font_size, s, label.x * s, label.y * s, item_bounds);
- // }
- legacy_buffers.push(make_text_buffer_with_font(font_system, &label.text, label.font_size, font_opt.as_deref()));
- legacy_labels.push(label);
- legacy_bounds.push(item_bounds);
- }
- }
- }
- }
-
- // Add the legacy buffered items (references are safe now that legacy_buffers is not reallocated)
- for ((buf, label), bounds) in legacy_buffers.iter().zip(legacy_labels.iter()).zip(legacy_bounds.iter()) {
- areas.push(TextArea {
- buffer: buf,
- left: (label.x * s).round(),
- top: (label.y * s).round(),
- scale: s,
- bounds: *bounds,
- default_color: glyphon::Color::rgb(label.color[0], label.color[1], label.color[2]),
- custom_glyphs: &[],
- });
- }
-
- // for label in &legacy_labels {
- // eprintln!("DEBUG_LEGACY_LABEL: text='{}' x={} y={}", label.text, label.x, label.y);
- // }
-
- text_renderer.prepare(device, queue, font_system, text_atlas, text_viewport, areas, swash_cache).unwrap();
-
- // Process curved labels
- let mut textured_verts = Vec::new();
-
- if !curved_labels.is_empty() {
- struct CurvedDrawInfo {
- label: TextLabel,
- tx: f32,
- ty: f32,
- tw: f32,
- th: f32,
- buffer: Buffer,
- }
-
- let mut curved_draws = Vec::new();
- let mut current_x = 4.0;
- let mut current_y = 4.0;
- let font_size = 12.0;
- let row_height = (font_size + 8.0) * s;
-
- for label in curved_labels {
- let char_w = TextLabel::estimate_width(&label.text, font_size);
- let physical_w = char_w * s;
- if current_x + physical_w + 4.0 > 1024.0 {
- current_x = 4.0;
- current_y += row_height;
- }
- let buf = make_text_buffer(font_system, &label.text, font_size);
- curved_draws.push(CurvedDrawInfo {
- label: label.clone(),
- tx: current_x,
- ty: current_y,
- tw: char_w,
- th: font_size,
- buffer: buf,
- });
- current_x += physical_w + 8.0 * s;
- }
-
- let mut curved_areas = Vec::new();
- for draw in &curved_draws {
- curved_areas.push(TextArea {
- buffer: &draw.buffer,
- left: draw.tx.round(),
- top: draw.ty.round(),
- scale: s,
- bounds: TextBounds {
- left: 0,
- top: 0,
- right: 1024,
- bottom: 1024,
- },
- default_color: glyphon::Color::rgb(255, 255, 255),
- custom_glyphs: &[],
- });
- }
-
- curved_text_renderer.prepare(
- device,
- queue,
- font_system,
- curved_text_atlas,
- curved_text_viewport,
- curved_areas,
- swash_cache,
- ).unwrap();
-
- let mut texture_encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
- label: Some("Curved Text Texture Encoder"),
- });
- {
- let view_for_pass = curved_text_texture.create_view(&wgpu::TextureViewDescriptor::default());
- let mut pass = texture_encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
- label: Some("Curved Text Render Pass"),
- color_attachments: &[Some(wgpu::RenderPassColorAttachment {
- view: &view_for_pass,
- resolve_target: None,
- ops: wgpu::Operations {
- load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }),
- store: wgpu::StoreOp::Store,
- },
- })],
- depth_stencil_attachment: None,
- timestamp_writes: None,
- occlusion_query_set: None,
- });
- curved_text_renderer.render(curved_text_atlas, curved_text_viewport, &mut pass).unwrap();
- }
- queue.submit(std::iter::once(texture_encoder.finish()));
-
- let clip_circle_val = if circular_network_pane {
- [network_circle_x * s, network_circle_y * s, network_circle_radius * s]
- } else {
- [0.0, 0.0, 0.0]
- };
-
- for draw in curved_draws {
- let dx = (draw.label.x + draw.tw / 2.0) - network_circle_x;
- let dy = (draw.label.y + draw.th / 2.0) - network_circle_y;
- let theta = dy.atan2(dx);
- let angle = theta + std::f32::consts::FRAC_PI_2;
-
- let cx = draw.label.x + draw.tw / 2.0;
- let cy = draw.label.y + draw.th / 2.0;
- let w_half = draw.tw / 2.0;
- let h_half = draw.th / 2.0;
-
- let cos_a = angle.cos();
- let sin_a = angle.sin();
-
- let local_pts = [
- [-w_half, -h_half],
- [w_half, -h_half],
- [-w_half, h_half],
- [w_half, h_half],
- ];
-
- let mut screen_pts = [[0.0; 2]; 4];
- for (k, pt) in local_pts.iter().enumerate() {
- let rx = pt[0] * cos_a - pt[1] * sin_a;
- let ry = pt[0] * sin_a + pt[1] * cos_a;
- screen_pts[k] = [cx + rx, cy + ry];
- }
-
- let ndc_pts = screen_pts.map(|pt| [
- (pt[0] / sw) * 2.0 - 1.0,
- 1.0 - (pt[1] / sh) * 2.0,
- ]);
-
- // eprintln!("DEBUG_NDCPTS: char='{}' ndc0={:?} ndc1={:?} ndc2={:?} ndc3={:?}",
- // draw.label.text, ndc_pts[0], ndc_pts[1], ndc_pts[2], ndc_pts[3]);
-
- let u0 = draw.tx / 1024.0;
- let v0 = draw.ty / 1024.0;
- let u1 = (draw.tx + draw.tw * s) / 1024.0;
- let v1 = (draw.ty + draw.th * s) / 1024.0;
-
- let c = [
- draw.label.color[0] as f32 / 255.0,
- draw.label.color[1] as f32 / 255.0,
- draw.label.color[2] as f32 / 255.0,
- 1.0,
- ];
-
- let v_tl = TexturedVertex { position: ndc_pts[0], tex_coords: [u0, v0], color: c, clip_circle: clip_circle_val };
- let v_tr = TexturedVertex { position: ndc_pts[1], tex_coords: [u1, v0], color: c, clip_circle: clip_circle_val };
- let v_bl = TexturedVertex { position: ndc_pts[2], tex_coords: [u0, v1], color: c, clip_circle: clip_circle_val };
- let v_br = TexturedVertex { position: ndc_pts[3], tex_coords: [u1, v1], color: c, clip_circle: clip_circle_val };
-
- textured_verts.push(v_tl);
- textured_verts.push(v_tr);
- textured_verts.push(v_bl);
-
- textured_verts.push(v_tr);
- textured_verts.push(v_br);
- textured_verts.push(v_bl);
- }
- }
-
- *textured_vertex_count = textured_verts.len() as u32;
- if *textured_vertex_count > 0 {
- let data = bytemuck::cast_slice(&textured_verts);
- let needed = data.len() as wgpu::BufferAddress;
- if needed > textured_vertex_buffer.size() {
- *textured_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("Textured Vertex Buffer"),
- size: needed,
- usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
- }
- queue.write_buffer(textured_vertex_buffer, 0, data);
- }
- }
-
-
- fn resize(&mut self, width: u32, height: u32) {
- if width > 0 && height > 0 {
- let old_width = self.width;
- self.physical_width = width;
- self.physical_height = height;
- self.width = width as f32 / self.scale as f32;
- self.height = height as f32 / self.scale as f32;
- self.wgpu_adapter.resize(width, height);
-
- let (tex, view) = self.create_depth_texture();
- self.depth_texture = tex;
- self.depth_texture_view = view;
-
- let (b_tex, b_view) = self.create_backdrop_texture();
- self.backdrop_texture = b_tex;
- self.backdrop_texture_view = b_view;
-
- self.backdrop_bind_group = self.wgpu_adapter.device.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some("Backdrop Bind Group"),
- layout: &self.backdrop_bind_group_layout,
- entries: &[
- wgpu::BindGroupEntry {
- binding: 0,
- resource: wgpu::BindingResource::TextureView(&self.backdrop_texture_view),
- },
- wgpu::BindGroupEntry {
- binding: 1,
- resource: wgpu::BindingResource::Sampler(&self.backdrop_sampler),
- },
- ],
- });
-
- if old_width > 0.0 {
- let r = self.width / old_width;
- self.splitter_layout.scale(r);
- let body_h = self.body_h();
- self.widgets[SPLITTER1_IDX].set_rect(self.splitter_layout.splitter1_x, HEADER_H, SPLITTER_W, body_h);
- self.widgets[SPLITTER2_IDX].set_rect(self.splitter_layout.splitter2_x, HEADER_H, SPLITTER_W, body_h);
- }
-
- self.sync_layout();
- self.read_panel_offsets();
- self.keep_cursor_in_view();
- self.upload_vertices();
- }
- }
-
- fn handle_event(&mut self, event: &WindowEvent) -> bool {
- match event {
- WindowEvent::MouseWheel { delta, phase, .. } => {
- let dialog_open = self.node_palette_visible;
- let in_network_pane = self.in_network_pane();
- // eprintln!("DEBUG MOUSEWHEEL: delta={:?}, phase={:?}, cursor=({}, {}), in_network_pane={}", delta, phase, self.cursor_x, self.cursor_y, in_network_pane);
- let node_area_y = self.positions[CONTENT_IDX].1;
-
- let in_viewport = self.cursor_x >= self.content_right_x()
- && self.cursor_x < self.splitter_layout.splitter2_x
- && self.cursor_y >= node_area_y
- && self.cursor_y < self.height - STATUS_H;
-
- let mut focus_changed = false;
- let mut new_pane = None;
- if !dialog_open {
- if in_network_pane {
- new_pane = Some(LEFT_MENUBAR_IDX);
- } else if in_viewport {
- if self.show_spreadsheet && self.cursor_y >= self.positions[SPREADSHEET_IDX].1 {
- new_pane = Some(SPREADSHEET_MENUBAR_IDX);
- } else {
- new_pane = Some(RIGHT_MENUBAR_IDX);
- }
- } else if self.cursor_x > self.splitter_layout.splitter2_x + SPLITTER_W && self.cursor_y >= node_area_y && self.cursor_y < self.height - STATUS_H {
- new_pane = Some(PARAM_MENUBAR_IDX);
- }
- }
- if let Some(pane_idx) = new_pane {
- if self.focused_pane != pane_idx {
- self.focused_pane = pane_idx;
- self.sync_pane_focus();
- focus_changed = true;
- }
- }
-
- let mut handled = false;
- let mut needs_sync_grid = false;
- if !dialog_open && !self.modifiers.control_key() {
- let ctx = &mut self.ui_context;
- for (i, w) in self.widgets.iter_mut().enumerate() {
- if w.mouse_wheel(delta, self.cursor_x, self.cursor_y, ctx) {
- handled = true;
- if i == CONTENT_IDX {
- let active_node_area_y = self.positions[CONTENT_IDX].1;
- let active_node_area_x = self.positions[CONTENT_IDX].0;
- let (gx, gy) = w.grid_origin();
- let prev_pan_x = self.pan_x;
- let prev_pan_y = self.pan_y;
- self.pan_x = gx - active_node_area_x;
- self.pan_y = gy - active_node_area_y;
-
- let dx = prev_pan_x - self.pan_x;
- let dy = prev_pan_y - self.pan_y;
-
- let dt_scroll = Instant::now().duration_since(self.last_frame).as_secs_f32().min(0.1);
- let vel_x = if dt_scroll > 1e-4 { -dx / dt_scroll } else { -dx * 60.0 };
- let vel_y = if dt_scroll > 1e-4 { -dy / dt_scroll } else { -dy * 60.0 };
- self.pan_velocity_x = self.pan_velocity_x * 0.4 + vel_x * 0.6;
- self.pan_velocity_y = self.pan_velocity_y * 0.4 + vel_y * 0.6;
-
- self.is_scrolling_trackpad = match delta {
- MouseScrollDelta::LineDelta(_, _) => false,
- MouseScrollDelta::PixelDelta(_) => match phase {
- TouchPhase::Started | TouchPhase::Moved => true,
- TouchPhase::Ended | TouchPhase::Cancelled => false,
- }
- };
- if self.is_scrolling_trackpad {
- self.last_scroll_time = Instant::now();
- self.scroll_accum_x += dx;
- self.scroll_accum_y += dy;
- }
- needs_sync_grid = true;
- }
- }
- }
- }
- if needs_sync_grid {
- self.sync_grid_settings();
- }
-
- let result = if handled {
- self.sync_parameters_to_project();
-
- // Sync Parameters pane with selected node
- let params = if !self.is_detached_network {
- if let Some(slot_idx) = self.widgets[CONTENT_IDX].selected_node() {
- let dir = self.current_dir();
- if slot_idx < dir.children.len() {
- param_display(&dir.children[slot_idx].params)
- } else {
- vec![]
- }
- } else {
- vec![]
- }
- } else {
- vec![]
- };
- self.widgets[PARAM_IDX].set_display_params(¶ms);
-
-
- self.sync_layout();
- self.read_panel_offsets();
- self.upload_vertices();
- true
- } else if !dialog_open && in_network_pane {
- if self.modifiers.control_key() {
- let factor = match delta {
- MouseScrollDelta::LineDelta(_x, y) => {
- if *y > 0.0 { 1.15 } else if *y < 0.0 { 1.0 / 1.15 } else { 1.0 }
- }
- MouseScrollDelta::PixelDelta(pos) => {
- let dy = pos.y as f32;
- (1.0 + dy / 100.0).clamp(0.8, 1.25)
- }
- };
- if factor != 1.0 {
- self.zoom(factor, Some((self.cursor_x, self.cursor_y)));
- true
- } else {
- false
- }
- } else {
- match delta {
- MouseScrollDelta::LineDelta(x, y) => {
- let dx = *x * 30.0 * self.scroll_speed;
- let dy = *y * 30.0 * self.scroll_speed;
- self.pan_x -= dx;
- self.pan_y -= dy;
- self.is_scrolling_trackpad = false;
- let dt_scroll = Instant::now().duration_since(self.last_frame).as_secs_f32().min(0.1);
- let vel_x = if dt_scroll > 1e-4 { -dx / dt_scroll } else { -dx * 60.0 };
- let vel_y = if dt_scroll > 1e-4 { -dy / dt_scroll } else { -dy * 60.0 };
- self.pan_velocity_x = self.pan_velocity_x * 0.4 + vel_x * 0.6;
- self.pan_velocity_y = self.pan_velocity_y * 0.4 + vel_y * 0.6;
- self.sync_grid_settings();
- true
- }
- MouseScrollDelta::PixelDelta(pos) => {
- let dx = (pos.x as f32 / self.scale as f32) * self.scroll_speed;
- let dy = (pos.y as f32 / self.scale as f32) * self.scroll_speed;
- self.pan_x -= dx;
- self.pan_y -= dy;
- self.is_scrolling_trackpad = match phase {
- TouchPhase::Started | TouchPhase::Moved => true,
- TouchPhase::Ended | TouchPhase::Cancelled => false,
- };
- self.last_scroll_time = Instant::now();
- self.scroll_accum_x += dx;
- self.scroll_accum_y += dy;
- self.sync_grid_settings();
- true
- }
- }
- }
- } else if !dialog_open && in_viewport {
- if self.modifiers.control_key() {
- match delta {
- MouseScrollDelta::LineDelta(_x, y) => {
- let dy = *y * 0.15;
- self.viewport_zoom *= (-dy).exp();
- self.viewport_zoom = self.viewport_zoom.clamp(0.05, 20.0);
-
- self.is_zooming_viewport = false;
- let dt_scroll = Instant::now().duration_since(self.last_frame).as_secs_f32().min(0.1);
- let vel_zoom = if dt_scroll > 1e-4 { dy / dt_scroll } else { dy * 60.0 };
- self.zoom_velocity = self.zoom_velocity * 0.4 + vel_zoom * 0.6;
-
- true
- }
- MouseScrollDelta::PixelDelta(pos) => {
- let dy = (pos.y as f32 / self.scale as f32) * 0.005;
- self.viewport_zoom *= (-dy).exp();
- self.viewport_zoom = self.viewport_zoom.clamp(0.05, 20.0);
-
- self.is_zooming_viewport = match phase {
- TouchPhase::Started | TouchPhase::Moved => true,
- TouchPhase::Ended | TouchPhase::Cancelled => false,
- };
- self.last_zoom_time = Instant::now();
- self.zoom_accum += dy;
-
- true
- }
- }
- } else {
- match delta {
- MouseScrollDelta::LineDelta(x, y) => {
- self.scroll_lock = 0;
- let dx = *x * 0.05;
- let dy = *y * 0.05;
- if self.active_camera != "Default Camera" {
- self.update_active_camera_rotation(dx, -dy);
- } else {
- self.rotation_y += dx;
- self.rotation_x -= dy;
- }
-
- self.is_rotating_viewport = false;
- let dt_scroll = Instant::now().duration_since(self.last_frame).as_secs_f32().min(0.1);
- let vel_yaw = if dt_scroll > 1e-4 { dx / dt_scroll } else { dx * 60.0 };
- let vel_pitch = if dt_scroll > 1e-4 { -dy / dt_scroll } else { -dy * 60.0 };
- self.rotate_velocity_yaw = self.rotate_velocity_yaw * 0.4 + vel_yaw * 0.6;
- self.rotate_velocity_pitch = self.rotate_velocity_pitch * 0.4 + vel_pitch * 0.6;
-
- true
- }
- MouseScrollDelta::PixelDelta(pos) => {
- let mut dx = (pos.x as f32 / self.scale as f32) * 0.005;
- let mut dy = (pos.y as f32 / self.scale as f32) * 0.005;
-
- if *phase == TouchPhase::Started {
- self.scroll_lock = 0;
- self.rotate_accum_yaw = 0.0;
- self.rotate_accum_pitch = 0.0;
- }
-
- if self.scroll_lock == 0 {
- self.rotate_accum_yaw += dx;
- self.rotate_accum_pitch -= dy;
- if self.rotate_accum_yaw.abs() > 0.002 || self.rotate_accum_pitch.abs() > 0.002 {
- if self.rotate_accum_pitch.abs() > 1.2 * self.rotate_accum_yaw.abs() {
- self.scroll_lock = 2; // lock vertical
- } else if self.rotate_accum_yaw.abs() > 1.2 * self.rotate_accum_pitch.abs() {
- self.scroll_lock = 1; // lock horizontal
- }
- }
- } else {
- if self.scroll_lock == 1 {
- dy = 0.0;
- self.rotate_accum_yaw += dx;
- } else {
- dx = 0.0;
- self.rotate_accum_pitch -= dy;
- }
- }
-
- if self.active_camera != "Default Camera" {
- self.update_active_camera_rotation(dx, -dy);
- } else {
- self.rotation_y += dx;
- self.rotation_x -= dy;
- }
-
- self.is_rotating_viewport = match phase {
- TouchPhase::Started | TouchPhase::Moved => true,
- TouchPhase::Ended | TouchPhase::Cancelled => {
- self.scroll_lock = 0;
- false
- }
- };
- self.last_rotate_time = Instant::now();
-
- true
- }
- }
- }
- } else {
- false
- };
-
- if focus_changed {
- self.sync_layout();
- self.read_panel_offsets();
- self.upload_vertices();
- true
- } else {
- result
- }
- }
- WindowEvent::PinchGesture { delta, .. } => {
- let dialog_open = self.node_palette_visible;
- let in_network_pane = self.in_network_pane();
-
- if !dialog_open && in_network_pane {
- if delta.is_finite() && *delta != 0.0 {
- let factor = (1.0 + *delta as f32).clamp(0.8, 1.25);
- self.zoom(factor, Some((self.cursor_x, self.cursor_y)));
- true
- } else {
- false
- }
- } else {
- false
- }
- }
- WindowEvent::CursorMoved { position, .. } => {
- self.cursor_x = position.x as f32;
- self.cursor_y = position.y as f32;
- let mut changed = false;
-
- if self.is_panning {
- let dx = self.cursor_x - self.pan_start_cx;
- let dy = self.cursor_y - self.pan_start_cy;
- self.pan_x = self.pan_start_x + dx;
- self.pan_y = self.pan_start_y + dy;
- self.sync_grid_settings();
- self.rebuild_positions();
- self.apply_layout();
- self.update_panel_bounds();
- changed = true;
- } else {
- if let Some(idx) = self.drag_widget {
- if idx == NETWORK_PANEL_IDX && self.is_resizing_network.is_some() {
- let dir = self.is_resizing_network.unwrap();
- let dx = self.cursor_x - self.drag_start_mouse.0;
- let dy = self.cursor_y - self.drag_start_mouse.1;
- let (sx, sy, sw, sh) = self.drag_start_rect;
-
- let mut fx = sx;
- let mut fy = sy;
- let mut fw = sw;
- let mut fh = sh;
-
- if dir.left {
- let new_w = (sw - dx).max(150.0);
- fx = sx + sw - new_w;
- fw = new_w;
- } else if dir.right {
- fw = (sw + dx).max(150.0);
- }
-
- if dir.top {
- let new_h = (sh - dy).max(100.0);
- fy = sy + sh - new_h;
- fh = new_h;
- } else if dir.bottom {
- fh = (sh + dy).max(100.0);
- }
-
- self.floating_network_layout = (fx, fy, fw, fh);
- self.rebuild_positions();
- self.apply_layout();
- self.sync_grid_settings();
- changed = true;
- } else if idx == PARAM_IDX && self.is_resizing_param {
- let dx = self.cursor_x - self.drag_start_mouse.0;
- let sw = self.drag_start_param_w;
- let new_w = (sw - dx).max(150.0);
- self.floating_param_width = new_w;
- self.rebuild_positions();
- self.apply_layout();
- changed = true;
- } else if idx == SPREADSHEET_IDX && self.is_resizing_spreadsheet {
- let dy = self.cursor_y - self.drag_start_mouse.1;
- let sh = self.drag_start_spreadsheet_h;
- let new_h = (sh - dy).max(100.0);
- self.floating_spreadsheet_height = new_h;
- self.rebuild_positions();
- self.apply_layout();
- changed = true;
- } else {
- self.widgets[idx].set_modifiers(self.modifiers.control_key(), self.modifiers.shift_key(), self.modifiers.alt_key());
- if self.widgets[idx].drag_update(self.cursor_x, self.cursor_y) {
- changed = true;
- if idx == NETWORK_PANEL_IDX {
- self.sync_grid_settings();
- }
- }
- }
- }
-
- if self.drag_widget.is_none() {
- for i in 0..self.widgets.len() {
- let (cx, cy) = (self.cursor_x, self.cursor_y);
- let is_network_part = i == CONTENT_IDX || i == LEFT_MENUBAR_IDX || i == BREADCRUMB_IDX || i == NETWORK_PANEL_IDX;
- let inside = if self.circular_network_pane && is_network_part {
- if i == CONTENT_IDX {
- self.circular_network_layout.hit_test_content(cx, cy, 0.0, BREADCRUMB_H)
- } else if i == LEFT_MENUBAR_IDX {
- false
- } else if i == BREADCRUMB_IDX {
- self.circular_network_layout.hit_test_breadcrumb(cx, cy, 0.0, BREADCRUMB_H)
- } else if i == NETWORK_PANEL_IDX {
- self.widgets[NETWORK_PANEL_IDX].hit_test(cx, cy, &self.ui_context)
- } else {
- false
- }
- } else {
- self.widgets[i].hit_test(cx, cy, &self.ui_context)
- };
- let (tx, ty) = if inside { (cx, cy) } else { (-9999.0, -9999.0) };
- if self.widgets[i].cursor_moved(tx, ty, &mut self.ui_context) {
- changed = true;
- }
- }
- }
- }
- changed
- }
- WindowEvent::MouseInput { state: btn_state, button, .. } => {
- println!("DEBUG: MouseInput state={:?} button={:?} cursor=({}, {})", btn_state, button, self.cursor_x, self.cursor_y);
- if *btn_state == ElementState::Pressed {
- self.pan_velocity_x = 0.0;
- self.pan_velocity_y = 0.0;
- self.is_scrolling_trackpad = false;
- self.scroll_accum_x = 0.0;
- self.scroll_accum_y = 0.0;
-
- self.rotate_velocity_yaw = 0.0;
- self.rotate_velocity_pitch = 0.0;
- self.is_rotating_viewport = false;
- self.scroll_lock = 0;
- self.rotate_accum_yaw = 0.0;
- self.rotate_accum_pitch = 0.0;
-
- self.zoom_velocity = 0.0;
- self.is_zooming_viewport = false;
- self.zoom_accum = 0.0;
- }
- let dialog_open = self.node_palette_visible;
- let in_network_pane = self.in_network_pane();
- let node_area_x = self.positions[CONTENT_IDX].0;
- let node_area_y = self.positions[CONTENT_IDX].1;
-
- let is_pan_trigger = !dialog_open
- && in_network_pane
- && (*button == MouseButton::Middle
- || (*button == MouseButton::Left && self.space_pressed));
-
- if is_pan_trigger {
- match btn_state {
- ElementState::Pressed => {
- self.is_panning = true;
- self.pan_start_cx = self.cursor_x;
- self.pan_start_cy = self.cursor_y;
- self.pan_start_x = self.pan_x;
- self.pan_start_y = self.pan_y;
- self.pan_velocity_x = 0.0;
- self.pan_velocity_y
diff truncated