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

commitd958916ecc645de8eb32f9022ecc3982ee64792a
parent68aca57c95
authorLucas Galante <[email protected]>
date2026-05-24 17:03
Update components and system settings

 nodes/line.json |    8 +
 src/main.rs     | 2333 +++++++++++++++++++++++++++++++++++++++++++++----------
 2 files changed, 1914 insertions(+), 427 deletions(-)

diff --git a/nodes/line.json b/nodes/line.json
new file mode 100644
index 0000000..bed2c6c
--- /dev/null
+++ b/nodes/line.json
@@ -0,0 +1,8 @@
+{
+  "name": "Line 1",
+  "type": "line",
+  "params": [
+    { "name": "Length", "default": "1.0", "type": "slider" },
+    { "name": "Thickness", "default": "0.02", "type": "slider" }
+  ]
+}
diff --git a/src/main.rs b/src/main.rs
index 6526cf6..13b246a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,6 +2,8 @@ use std::sync::Arc;
 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};
 
@@ -15,15 +17,16 @@ use opencl3::memory::{Buffer as ClBuffer, CL_MEM_READ_WRITE};
 use opencl3::types::{cl_float, cl_int, CL_TRUE};
 
 use winit::application::ApplicationHandler;
-use winit::event::{ElementState, KeyEvent, MouseButton, WindowEvent};
+use winit::event::{ElementState, KeyEvent, MouseButton, WindowEvent, MouseScrollDelta};
 use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
 use winit::keyboard::{Key, ModifiersState, NamedKey};
 use winit::window::{Window, WindowAttributes};
 
 use wgpu::util::DeviceExt;
 
-use clear_ui::widget::{Breadcrumb, Canvas, ColorSelector, ContentBg, MenuBar, Node, ParametersBg, Splitter, Spreadsheet, StatusBar, TextLabel, ViewportBg, Widget};
+use clear_ui::widget::{Breadcrumb, Canvas, ColorSelector, ContentBg, MenuBar, Node, ParametersBg, Spinbox, Splitter, Spreadsheet, StatusBar, TextLabel, Toggle, ViewportBg, Widget};
 use clear_ui::colors;
+use clear_ui::layout::{RenderTarget, Section};
 
 use glyphon::{
     Attrs, Buffer, Cache, FontSystem, Metrics, Resolution, SwashCache, TextArea, TextAtlas,
@@ -122,6 +125,30 @@ struct Project {
     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 },
+}
+
+#[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,
@@ -197,6 +224,138 @@ fn load_fs_tree() -> FsNode {
     }
 }
 
+fn default_grid_thickness() -> f32 { 0.03 }
+fn default_show_camera_pivot() -> bool { false }
+fn default_camera_pivot_size() -> f32 { 1.0 }
+
+#[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,
+}
+
+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,
+        }
+    }
+}
+
+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("clearwm");
+        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);
+        }
+    }
+}
+
+enum ConfigControl {
+    Header(&'static str),
+    Text(&'static str),
+    Toggle {
+        #[allow(dead_code)]
+        label: &'static str,
+        #[allow(dead_code)]
+        id: usize,
+        #[allow(dead_code)]
+        enabled: bool,
+    },
+    ColorSelector,
+    Shortcut {
+        key: &'static str,
+        desc: &'static str,
+    },
+}
+
+struct PageLayout {
+    labels: Vec<TextLabel>,
+}
+
+struct SectionCollector {
+    quads: Vec<(f32, f32, f32, f32, [f32; 4])>,
+    labels: Vec<TextLabel>,
+    active: bool,
+}
+
+impl RenderTarget for SectionCollector {
+    fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32) {
+        if self.active {
+            self.quads.push((x, y, w, h, color));
+        }
+    }
+
+    fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
+        if self.active {
+            let u8_color = [
+                (color[0] * 255.0).round().clamp(0.0, 255.0) as u8,
+                (color[1] * 255.0).round().clamp(0.0, 255.0) as u8,
+                (color[2] * 255.0).round().clamp(0.0, 255.0) as u8,
+            ];
+            self.labels.push(TextLabel {
+                text: content.to_string(),
+                x,
+                y,
+                font_size: size,
+                color: u8_color,
+            });
+        }
+    }
+}
+
 struct ConfigDialog {
     x: f32, y: f32, w: f32, h: f32,
     hovered: bool,
@@ -206,70 +365,98 @@ struct ConfigDialog {
     panel_h: f32,
     active_page: usize,
     hovered_tab: Option<usize>,
-    grid_snap_enabled: bool,
-    grid_snap_pending: Option<bool>,
-    grid_snap_hovered: bool,
-    network_grid_enabled: bool,
-    network_grid_pending: Option<bool>,
-    network_grid_hovered: bool,
+    toggle_grid_snap: Toggle,
+    toggle_network_grid: Toggle,
     grid_size_x: f32,
     grid_size_y: f32,
     skipped_row_h: f32,
     skipped_col_w: f32,
-    grid_size_pending: Option<(usize, f32)>,
-    hovered_spin_btn: Option<usize>,
-    show_grid_enabled: bool,
-    show_grid_pending: Option<bool>,
-    show_grid_hovered: bool,
-    show_cube_enabled: bool,
-    show_cube_pending: Option<bool>,
-    show_cube_hovered: bool,
-    show_origin_enabled: bool,
-    show_origin_pending: Option<bool>,
-    show_origin_hovered: bool,
+    toggle_show_grid: Toggle,
+    toggle_show_cube: Toggle,
+    toggle_show_origin: Toggle,
+    toggle_show_camera_pivot: Toggle,
     viewport_r: f32,
     viewport_g: f32,
     viewport_b: f32,
     color_selector: ColorSelector,
     last_sent_color: [u8; 3],
+    origin_size: f32,
+    camera_pivot_size: f32,
+    grid_thickness: f32,
+    spin_grid_x: Spinbox,
+    spin_grid_y: Spinbox,
+    spin_skipped_row_h: Spinbox,
+    spin_skipped_col_w: Spinbox,
+    spin_origin_size: Spinbox,
+    spin_grid_thickness: Spinbox,
+    spin_camera_pivot_size: Spinbox,
+    scroll_y: f32,
+    scroll_velocity: f32,
+    section_quads: Vec<(f32, f32, f32, f32, [f32; 4])>,
+    section_labels: Vec<TextLabel>,
 }
 
 impl ConfigDialog {
-    fn new() -> Self {
-        Self {
+    fn new(settings: &DesignSettings) -> Self {
+        let vr = settings.viewport_bg_color[0];
+        let vg = settings.viewport_bg_color[1];
+        let vb = settings.viewport_bg_color[2];
+        let r = (vr * 255.0).round().clamp(0.0, 255.0) as u8;
+        let g = (vg * 255.0).round().clamp(0.0, 255.0) as u8;
+        let b = (vb * 255.0).round().clamp(0.0, 255.0) as u8;
+
+        let mut toggle_grid_snap = Toggle::new().with_label("Snap to Grid");
+        toggle_grid_snap.set_toggled(settings.grid_snap_enabled);
+        let mut toggle_network_grid = Toggle::new().with_label("Show Grid");
+        toggle_network_grid.set_toggled(settings.network_grid_enabled);
+        let mut toggle_show_grid = Toggle::new().with_label("Show Grid Guide");
+        toggle_show_grid.set_toggled(settings.show_grid_enabled);
+        let mut toggle_show_cube = Toggle::new().with_label("Show Reference Cube");
+        toggle_show_cube.set_toggled(settings.show_cube_enabled);
+        let mut toggle_show_origin = Toggle::new().with_label("Show Origin Axes");
+        toggle_show_origin.set_toggled(settings.show_origin_enabled);
+        let mut toggle_show_camera_pivot = Toggle::new().with_label("Show Camera Pivot");
+        toggle_show_camera_pivot.set_toggled(settings.show_camera_pivot_enabled);
+
+        let mut dialog = Self {
             x: 0.0, y: 0.0, w: 0.0, h: 0.0,
             hovered: false, visible: false,
             close_hovered: false,
             panel_w: 800.0, panel_h: 600.0,
             active_page: 0,
             hovered_tab: None,
-            grid_snap_enabled: true,
-            grid_snap_pending: None,
-            grid_snap_hovered: false,
-            network_grid_enabled: true,
-            network_grid_pending: None,
-            network_grid_hovered: false,
-            grid_size_x: 80.0,
-            grid_size_y: 40.0,
-            skipped_row_h: 20.0,
-            skipped_col_w: 20.0,
-            grid_size_pending: None,
-            hovered_spin_btn: None,
-            show_grid_enabled: true,
-            show_grid_pending: None,
-            show_grid_hovered: false,
-            show_cube_enabled: false,
-            show_cube_pending: None,
-            show_cube_hovered: false,
-            show_origin_enabled: true,
-            show_origin_pending: None,
-            show_origin_hovered: false,
-            viewport_r: 0.05,
-            viewport_g: 0.05,
-            viewport_b: 0.10,
-            color_selector: ColorSelector::new([13, 13, 26]).with_label("Viewport Background"),
-            last_sent_color: [13, 13, 26],
-        }
+            toggle_grid_snap,
+            toggle_network_grid,
+            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,
+            toggle_show_grid,
+            toggle_show_cube,
+            toggle_show_origin,
+            toggle_show_camera_pivot,
+            viewport_r: vr,
+            viewport_g: vg,
+            viewport_b: vb,
+            color_selector: ColorSelector::new([r, g, b]).with_label("Viewport Background"),
+            last_sent_color: [r, g, b],
+            origin_size: settings.origin_size,
+            camera_pivot_size: settings.camera_pivot_size,
+            grid_thickness: settings.grid_thickness,
+            spin_grid_x: Spinbox::new(settings.grid_size_x as i32, 10, 200, 5).with_label("Grid X:"),
+            spin_grid_y: Spinbox::new(settings.grid_size_y as i32, 5, 100, 5).with_label("Grid Y:"),
+            spin_skipped_row_h: Spinbox::new(settings.skipped_row_h as i32, 0, 150, 5).with_label("Skipped Row H:"),
+            spin_skipped_col_w: Spinbox::new(settings.skipped_col_w as i32, 0, 150, 5).with_label("Skipped Col W:"),
+            spin_origin_size: Spinbox::new((settings.origin_size * 10.0).round() as i32, 1, 50, 1).with_label("Origin Guide Size:").with_decimals(1),
+            spin_grid_thickness: Spinbox::new((settings.grid_thickness * 1000.0).round() as i32, 2, 200, 5).with_label("Grid Thickness:").with_decimals(3),
+            spin_camera_pivot_size: Spinbox::new((settings.camera_pivot_size * 10.0).round() as i32, 1, 50, 1).with_label("Camera Pivot Size:").with_decimals(1),
+            scroll_y: 0.0,
+            scroll_velocity: 0.0,
+            section_quads: Vec::new(),
+            section_labels: Vec::new(),
+        };
+        dialog.update_child_layouts();
+        dialog
     }
 
     fn panel_rect(&self) -> (f32, f32, f32, f32) {
@@ -282,24 +469,6 @@ impl ConfigDialog {
         (px + pw - 28.0, py + 8.0, 20.0, 20.0)
     }
 
-    fn spin_btns(&self, px: f32, py: f32) -> Vec<(usize, f32, f32)> {
-        let row0_y = py + 156.0; // Grid X
-        let row1_y = py + 178.0; // Grid Y
-        let row2_y = py + 200.0; // Skipped Row H
-        let row3_y = py + 222.0; // Skipped Col W
-
-        vec![
-            (0, px + 160.0, row0_y - 2.0),
-            (1, px + 210.0, row0_y - 2.0),
-            (2, px + 160.0, row1_y - 2.0),
-            (3, px + 210.0, row1_y - 2.0),
-            (4, px + 160.0, row2_y - 2.0),
-            (5, px + 210.0, row2_y - 2.0),
-            (6, px + 160.0, row3_y - 2.0),
-            (7, px + 210.0, row3_y - 2.0),
-        ]
-    }
-
     fn tab_rects(&self, px: f32, py: f32) -> [(f32, f32, f32, f32); 4] {
         let tab_y = py + 30.0;
         let tab_h = 22.0;
@@ -315,44 +484,287 @@ impl ConfigDialog {
             (px + 16.0 + general_w + gap + network_w + gap + viewport_w + gap, tab_y, bindings_w, tab_h),
         ]
     }
+
+    fn get_page_controls(&self, page: usize) -> Vec<ConfigControl> {
+        match page {
+            0 => vec![
+                ConfigControl::Text("General settings for Clear Design Interface"),
+            ],
+            1 => vec![],
+            2 => vec![],
+            3 => vec![
+                ConfigControl::Header("Keyboard Shortcuts"),
+                ConfigControl::Shortcut { key: "Ctrl+G", desc: "Toggle Grid (viewport)" },
+                ConfigControl::Shortcut { key: "Ctrl+E", desc: "Toggle Cube" },
+                ConfigControl::Shortcut { key: "Ctrl+A", desc: "Square Viewport Aspect" },
+                ConfigControl::Shortcut { key: "Ctrl+,", desc: "Configure Dialog" },
+            ],
+            _ => vec![],
+        }
+    }
+
+    fn compute_layout(&self, px: f32, py: f32) -> PageLayout {
+        let mut labels = Vec::new();
+
+        let content_y = py + 60.0;
+        let mut y = content_y;
+
+        let controls = self.get_page_controls(self.active_page);
+        for control in controls {
+            match control {
+                ConfigControl::Header(title) => {
+                    labels.push(TextLabel {
+                        text: title.to_string(),
+                        x: px + 16.0,
+                        y,
+                        font_size: 13.0,
+                        color: [0xcc, 0xcc, 0xd4],
+                    });
+                    y += 24.0;
+                }
+                ConfigControl::Text(msg) => {
+                    labels.push(TextLabel {
+                        text: msg.to_string(),
+                        x: px + 16.0,
+                        y,
+                        font_size: 12.0,
+                        color: [0x88, 0x88, 0x99],
+                    });
+                    y += 20.0;
+                }
+                ConfigControl::Toggle { .. } => {
+                    // Handled as standard widget in update_child_layouts()
+                }
+                ConfigControl::ColorSelector => {
+                    // Handled as standard widget in update_child_layouts()
+                }
+                ConfigControl::Shortcut { key, desc } => {
+                    labels.push(TextLabel {
+                        text: key.to_string(),
+                        x: px + 32.0,
+                        y,
+                        font_size: 12.0,
+                        color: [0xdd, 0xdd, 0x88],
+                    });
+                    labels.push(TextLabel {
+                        text: desc.to_string(),
+                        x: px + 130.0,
+                        y,
+                        font_size: 12.0,
+                        color: [0xaa, 0xaa, 0xbb],
+                    });
+                    y += 22.0;
+                }
+            }
+        }
+
+        PageLayout {
+            labels,
+        }
+    }
+
+    fn update_child_layouts(&mut self) {
+        let (px, py, _, _) = self.panel_rect();
+
+        self.section_quads.clear();
+        self.section_labels.clear();
+        let mut collector = SectionCollector {
+            quads: std::mem::take(&mut self.section_quads),
+            labels: std::mem::take(&mut self.section_labels),
+            active: true,
+        };
+
+        let mut max_scroll_y = 0.0;
+
+        // --- Page 1 (Network) Layout ---
+        if self.active_page == 1 {
+            let y_start = py + 65.0;
+
+            // Section 1: Network Configuration Section (aligned vertically)
+            collector.active = true;
+            let mut sec1 = Section::new(&mut collector, px + 20.0, y_start, 760.0, "Network Configuration");
+            collector.active = false;
+            sec1.widget(&mut collector, &mut self.toggle_grid_snap, 14.0, 44.0, 22.0);
+            sec1.spacing(8.0);
+            sec1.widget(&mut collector, &mut self.toggle_network_grid, 14.0, 44.0, 22.0);
+            sec1.spacing(8.0);
+            collector.active = true;
+            let y1 = sec1.finish(&mut collector);
+
+            // Section 2: Grid Spacing Section (aligned vertically under sec1)
+            collector.active = true;
+            let mut sec2 = Section::new(&mut collector, px + 20.0, y1 + 12.0, 760.0, "Grid Spacing");
+            collector.active = false;
+            sec2.widget(&mut collector, &mut self.spin_grid_x, 14.0, 240.0, 22.0);
+            sec2.spacing(8.0);
+            sec2.widget(&mut collector, &mut self.spin_grid_y, 14.0, 240.0, 22.0);
+            sec2.spacing(8.0);
+            sec2.widget(&mut collector, &mut self.spin_skipped_row_h, 14.0, 240.0, 22.0);
+            sec2.spacing(8.0);
+            sec2.widget(&mut collector, &mut self.spin_skipped_col_w, 14.0, 240.0, 22.0);
+            sec2.spacing(8.0);
+            collector.active = true;
+            let y2 = sec2.finish(&mut collector);
+
+            let total_h = y2 - (py + 65.0);
+            let visible_h = 510.0;
+            max_scroll_y = (total_h - visible_h).max(0.0);
+        }
+
+        // --- Page 2 (Viewport) Layout ---
+        if self.active_page == 2 {
+            let y_start = py + 65.0;
+
+            // Section 1: Guides & Display Section (aligned vertically)
+            collector.active = true;
+            let mut sec1 = Section::new(&mut collector, px + 20.0, y_start, 760.0, "Guides & Display");
+            collector.active = false;
+            sec1.widget(&mut collector, &mut self.toggle_show_grid, 14.0, 44.0, 22.0);
+            sec1.spacing(8.0);
+            sec1.widget(&mut collector, &mut self.toggle_show_cube, 14.0, 44.0, 22.0);
+            sec1.spacing(8.0);
+            sec1.widget(&mut collector, &mut self.toggle_show_origin, 14.0, 44.0, 22.0);
+            sec1.spacing(8.0);
+            sec1.widget(&mut collector, &mut self.toggle_show_camera_pivot, 14.0, 44.0, 22.0);
+            sec1.spacing(8.0);
+            collector.active = true;
+            let y1 = sec1.finish(&mut collector);
+
+            // Section 2: Viewport Settings Section (aligned vertically under sec1)
+            collector.active = true;
+            let mut sec2 = Section::new(&mut collector, px + 20.0, y1 + 12.0, 760.0, "Viewport Settings");
+            collector.active = false;
+            sec2.widget(&mut collector, &mut self.spin_grid_thickness, 14.0, 240.0, 22.0);
+            sec2.spacing(8.0);
+            sec2.widget(&mut collector, &mut self.spin_origin_size, 14.0, 240.0, 22.0);
+            sec2.spacing(8.0);
+            sec2.widget(&mut collector, &mut self.spin_camera_pivot_size, 14.0, 240.0, 22.0);
+            sec2.spacing(12.0); // Slightly more space before color selector
+            sec2.widget(&mut collector, &mut self.color_selector, 14.0, 240.0, 24.0);
+            sec2.spacing(8.0);
+            collector.active = true;
+            let y2 = sec2.finish(&mut collector);
+
+            let total_h = y2 - (py + 65.0);
+            let visible_h = 510.0;
+            max_scroll_y = (total_h - visible_h).max(0.0);
+        }
+
+        self.scroll_y = self.scroll_y.clamp(0.0, max_scroll_y);
+
+        // Apply scroll offset shifting to all layout elements
+        if self.scroll_y > 0.0 {
+            for quad in &mut collector.quads {
+                quad.1 -= self.scroll_y;
+            }
+            for label in &mut collector.labels {
+                label.y -= self.scroll_y;
+            }
+
+            let shift = self.scroll_y;
+            let shift_widget = |w: &mut dyn Widget| {
+                let (wx, wy, ww, wh) = w.rect();
+                w.set_rect(wx, wy - shift, ww, wh);
+            };
+
+            if self.active_page == 1 {
+                shift_widget(&mut self.toggle_grid_snap);
+                shift_widget(&mut self.toggle_network_grid);
+                shift_widget(&mut self.spin_grid_x);
+                shift_widget(&mut self.spin_grid_y);
+                shift_widget(&mut self.spin_skipped_row_h);
+                shift_widget(&mut self.spin_skipped_col_w);
+            } else if self.active_page == 2 {
+                shift_widget(&mut self.toggle_show_grid);
+                shift_widget(&mut self.toggle_show_cube);
+                shift_widget(&mut self.toggle_show_origin);
+                shift_widget(&mut self.toggle_show_camera_pivot);
+                shift_widget(&mut self.spin_grid_thickness);
+                shift_widget(&mut self.spin_origin_size);
+                shift_widget(&mut self.spin_camera_pivot_size);
+                shift_widget(&mut self.color_selector);
+            }
+        }
+
+        self.section_quads = collector.quads;
+        self.section_labels = collector.labels;
+    }
 }
 
 impl Widget for ConfigDialog {
     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;
-        let px = x + (w - self.panel_w) / 2.0;
-        let py = y + (h - self.panel_h) / 2.0;
-        self.color_selector.set_rect(px + 20.0, py + 180.0, 240.0, 24.0);
+        self.update_child_layouts();
     }
     fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
     fn set_hovered(&mut self, v: bool) { self.hovered = v; }
     fn hovered(&self) -> bool { self.hovered }
 
-    fn set_visible(&mut self, v: bool) { self.visible = v; }
+    fn set_visible(&mut self, v: bool) {
+        self.visible = v;
+        if v {
+            self.scroll_y = 0.0;
+            self.scroll_velocity = 0.0;
+            self.update_child_layouts();
+        }
+    }
     fn visible(&self) -> bool { self.visible }
 
+    fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32) -> bool {
+        if !self.visible { return false; }
+        if self.hit_test(px, py) {
+            let scroll_amount = match delta {
+                MouseScrollDelta::LineDelta(_x, y) => *y * 24.0,
+                MouseScrollDelta::PixelDelta(pos) => pos.y as f32,
+            };
+            self.scroll_velocity -= scroll_amount * 12.0;
+            return true;
+        }
+        false
+    }
+
+    fn tick(&mut self, dt: f32) -> bool {
+        if !self.visible { return false; }
+        if self.scroll_velocity.abs() > 0.01 {
+            let old_scroll_y = self.scroll_y;
+            self.scroll_y += self.scroll_velocity * dt;
+            let friction = 8.0;
+            self.scroll_velocity *= (-friction * dt).exp();
+            if self.scroll_velocity.abs() < 5.0 {
+                self.scroll_velocity = 0.0;
+            }
+            self.update_child_layouts();
+            (self.scroll_y - old_scroll_y).abs() > 0.01
+        } else {
+            false
+        }
+    }
+
     fn set_config_toggle(&mut self, id: usize, val: bool) {
         match id {
-            0 => self.grid_snap_enabled = val,
-            1 => self.network_grid_enabled = val,
-            2 => self.show_grid_enabled = val,
-            3 => self.show_cube_enabled = val,
-            4 => self.show_origin_enabled = val,
+            0 => self.toggle_grid_snap.set_toggled(val),
+            1 => self.toggle_network_grid.set_toggled(val),
+            2 => self.toggle_show_grid.set_toggled(val),
+            3 => self.toggle_show_cube.set_toggled(val),
+            4 => self.toggle_show_origin.set_toggled(val),
+            5 => self.toggle_show_camera_pivot.set_toggled(val),
             _ => {}
         }
     }
     fn take_config_toggle(&mut self) -> Option<(usize, bool)> {
-        if let Some(v) = self.grid_snap_pending.take() {
-            Some((0, v))
-        } else if let Some(v) = self.network_grid_pending.take() {
-            Some((1, v))
-        } else if let Some(v) = self.show_grid_pending.take() {
-            Some((2, v))
-        } else if let Some(v) = self.show_cube_pending.take() {
-            Some((3, v))
-        } else if let Some(v) = self.show_origin_pending.take() {
-            Some((4, v))
+        if self.toggle_grid_snap.take_click() {
+            Some((0, self.toggle_grid_snap.toggled()))
+        } else if self.toggle_network_grid.take_click() {
+            Some((1, self.toggle_network_grid.toggled()))
+        } else if self.toggle_show_grid.take_click() {
+            Some((2, self.toggle_show_grid.toggled()))
+        } else if self.toggle_show_cube.take_click() {
+            Some((3, self.toggle_show_cube.toggled()))
+        } else if self.toggle_show_origin.take_click() {
+            Some((4, self.toggle_show_origin.toggled()))
+        } else if self.toggle_show_camera_pivot.take_click() {
+            Some((5, self.toggle_show_camera_pivot.toggled()))
         } else {
             None
         }
@@ -360,10 +772,22 @@ impl Widget for ConfigDialog {
 
     fn set_config_spin(&mut self, id: usize, val: f32) {
         match id {
-            0 => self.grid_size_x = val,
-            1 => self.grid_size_y = val,
-            2 => self.skipped_row_h = val,
-            3 => self.skipped_col_w = val,
+            0 => {
+                self.grid_size_x = val;
+                self.spin_grid_x.value = val as i32;
+            }
+            1 => {
+                self.grid_size_y = val;
+                self.spin_grid_y.value = val as i32;
+            }
+            2 => {
+                self.skipped_row_h = val;
+                self.spin_skipped_row_h.value = val as i32;
+            }
+            3 => {
+                self.skipped_col_w = val;
+                self.spin_skipped_col_w.value = val as i32;
+            }
             4 => {
                 self.viewport_r = val;
                 self.color_selector.color[0] = (val * 255.0).round().clamp(0.0, 255.0) as u8;
@@ -379,12 +803,49 @@ impl Widget for ConfigDialog {
                 self.color_selector.color[2] = (val * 255.0).round().clamp(0.0, 255.0) as u8;
                 self.last_sent_color[2] = self.color_selector.color[2];
             }
+            7 => {
+                self.origin_size = val;
+                self.spin_origin_size.value = (val * 10.0).round() as i32;
+            }
+            8 => {
+                self.grid_thickness = val;
+                self.spin_grid_thickness.value = (val * 1000.0).round() as i32;
+            }
+            9 => {
+                self.camera_pivot_size = val;
+                self.spin_camera_pivot_size.value = (val * 10.0).round() as i32;
+            }
             _ => {}
         }
     }
     fn take_config_spin(&mut self) -> Option<(usize, f32)> {
-        if let Some(v) = self.grid_size_pending.take() {
-            return Some(v);
+        if self.spin_grid_x.value as f32 != self.grid_size_x {
+            self.grid_size_x = self.spin_grid_x.value as f32;
+            return Some((0, self.grid_size_x));
+        }
+        if self.spin_grid_y.value as f32 != self.grid_size_y {
+            self.grid_size_y = self.spin_grid_y.value as f32;
+            return Some((1, self.grid_size_y));
+        }
+        if self.spin_skipped_row_h.value as f32 != self.skipped_row_h {
+            self.skipped_row_h = self.spin_skipped_row_h.value as f32;
+            return Some((2, self.skipped_row_h));
+        }
+        if self.spin_skipped_col_w.value as f32 != self.skipped_col_w {
+            self.skipped_col_w = self.spin_skipped_col_w.value as f32;
+            return Some((3, self.skipped_col_w));
+        }
+        if self.spin_origin_size.value as f32 / 10.0 != self.origin_size {
+            self.origin_size = self.spin_origin_size.value as f32 / 10.0;
+            return Some((7, self.origin_size));
+        }
+        if self.spin_grid_thickness.value as f32 / 1000.0 != self.grid_thickness {
+            self.grid_thickness = self.spin_grid_thickness.value as f32 / 1000.0;
+            return Some((8, self.grid_thickness));
+        }
+        if self.spin_camera_pivot_size.value as f32 / 10.0 != self.camera_pivot_size {
+            self.camera_pivot_size = self.spin_camera_pivot_size.value as f32 / 10.0;
+            return Some((9, self.camera_pivot_size));
         }
         for i in 0..3 {
             if self.color_selector.color[i] != self.last_sent_color[i] {
@@ -426,20 +887,6 @@ impl Widget for ConfigDialog {
             }
         }
 
-        let old_tg = self.grid_snap_hovered;
-        let old_ng = self.network_grid_hovered;
-        let old_sb = self.hovered_spin_btn;
-        let old_sg = self.show_grid_hovered;
-        let old_sc = self.show_cube_hovered;
-        let old_so = self.show_origin_hovered;
-
-        self.grid_snap_hovered = false;
-        self.network_grid_hovered = false;
-        self.hovered_spin_btn = None;
-        self.show_grid_hovered = false;
-        self.show_cube_hovered = false;
-        self.show_origin_hovered = false;
-
         let cs_changed = if self.visible && self.active_page == 2 {
             self.color_selector.cursor_moved(px, py)
         } else {
@@ -448,139 +895,286 @@ impl Widget for ConfigDialog {
             was
         };
 
-        if self.visible && self.active_page == 1 {
-            let row1_y = ppy + 84.0;
-            self.grid_snap_hovered = px >= ppx + 16.0 && px < ppx + 200.0
-                && py >= row1_y - 2.0 && py < row1_y + 18.0;
-            let row2_y = ppy + 108.0;
-            self.network_grid_hovered = px >= ppx + 16.0 && px < ppx + 200.0
-                && py >= row2_y - 2.0 && py < row2_y + 18.0;
-            for &(btn_id, bx, by) in &self.spin_btns(ppx, ppy) {
-                if btn_id < 8 && px >= bx && px < bx + 22.0 && py >= by && py < by + 20.0 {
-                    self.hovered_spin_btn = Some(btn_id);
-                    break;
-                }
+        let mut widgets_changed = false;
+        if self.visible {
+            if self.active_page == 1 {
+                widgets_changed |= self.toggle_grid_snap.cursor_moved(px, py);
+                widgets_changed |= self.toggle_network_grid.cursor_moved(px, py);
+                self.toggle_show_grid.set_hovered(false);
+                self.toggle_show_cube.set_hovered(false);
+                self.toggle_show_origin.set_hovered(false);
+                self.toggle_show_camera_pivot.set_hovered(false);
+
+                widgets_changed |= self.spin_grid_x.cursor_moved(px, py);
+                widgets_changed |= self.spin_grid_y.cursor_moved(px, py);
+                widgets_changed |= self.spin_skipped_row_h.cursor_moved(px, py);
+                widgets_changed |= self.spin_skipped_col_w.cursor_moved(px, py);
+                self.spin_grid_thickness.set_hovered(false);
+                self.spin_origin_size.set_hovered(false);
+                self.spin_camera_pivot_size.set_hovered(false);
+            } else if self.active_page == 2 {
+                self.toggle_grid_snap.set_hovered(false);
+                self.toggle_network_grid.set_hovered(false);
+                widgets_changed |= self.toggle_show_grid.cursor_moved(px, py);
+                widgets_changed |= self.toggle_show_cube.cursor_moved(px, py);
+                widgets_changed |= self.toggle_show_origin.cursor_moved(px, py);
+                widgets_changed |= self.toggle_show_camera_pivot.cursor_moved(px, py);
+
+                widgets_changed |= self.spin_grid_thickness.cursor_moved(px, py);
+                widgets_changed |= self.spin_origin_size.cursor_moved(px, py);
+                widgets_changed |= self.spin_camera_pivot_size.cursor_moved(px, py);
+                self.spin_grid_x.set_hovered(false);
+                self.spin_grid_y.set_hovered(false);
+                self.spin_skipped_row_h.set_hovered(false);
+                self.spin_skipped_col_w.set_hovered(false);
+            } else {
+                self.toggle_grid_snap.set_hovered(false);
+                self.toggle_network_grid.set_hovered(false);
+                self.toggle_show_grid.set_hovered(false);
+                self.toggle_show_cube.set_hovered(false);
+                self.toggle_show_origin.set_hovered(false);
+                self.toggle_show_camera_pivot.set_hovered(false);
+
+                self.spin_grid_x.set_hovered(false);
+                self.spin_grid_y.set_hovered(false);
+                self.spin_skipped_row_h.set_hovered(false);
+                self.spin_skipped_col_w.set_hovered(false);
+                self.spin_grid_thickness.set_hovered(false);
+                self.spin_origin_size.set_hovered(false);
+                self.spin_camera_pivot_size.set_hovered(false);
             }
-        } else if self.visible && self.active_page == 2 {
-            let row1_y = ppy + 84.0;
-            self.show_grid_hovered = px >= ppx + 16.0 && px < ppx + 200.0
-                && py >= row1_y - 2.0 && py < row1_y + 18.0;
-            let row2_y = ppy + 108.0;
-            self.show_cube_hovered = px >= ppx + 16.0 && px < ppx + 200.0
-                && py >= row2_y - 2.0 && py < row2_y + 18.0;
-            let row3_y = ppy + 132.0;
-            self.show_origin_hovered = px >= ppx + 16.0 && px < ppx + 200.0
-                && py >= row3_y - 2.0 && py < row3_y + 18.0;
         }
 
         was != self.hovered || old_close != self.close_hovered || old_tab != self.hovered_tab
-            || old_tg != self.grid_snap_hovered || old_ng != self.network_grid_hovered
-            || old_sb != self.hovered_spin_btn
-            || old_sg != self.show_grid_hovered || old_sc != self.show_cube_hovered || old_so != self.show_origin_hovered
-            || cs_changed
+            || cs_changed || widgets_changed
     }
 
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
-        if !self.visible || button != MouseButton::Left || state != ElementState::Pressed { return false; }
+        if !self.visible || button != MouseButton::Left { return false; }
         let (ppx, ppy, pw, ph) = self.panel_rect();
         let in_panel = px >= ppx && px <= ppx + pw && py >= ppy && py <= ppy + ph;
         let (cx, cy, cw, ch) = self.close_rect(ppx, pw, ppy);
         let on_close = px >= cx && px < cx + cw && py >= cy && py < cy + ch;
-        if !in_panel || on_close {
-            self.color_selector.unfocus();
-            self.visible = false;
-            return true;
-        }
-        for (i, (tx, ty, tw, th)) in self.tab_rects(ppx, ppy).iter().enumerate() {
-            if px >= *tx && px < *tx + *tw && py >= *ty && py < *ty + *th {
+
+        if state == ElementState::Pressed {
+            if !in_panel || on_close {
                 self.color_selector.unfocus();
-                self.active_page = i;
-                return true;
-            }
-        }
-        if self.active_page == 1 {
-            let row1_y = ppy + 84.0;
-            if px >= ppx + 16.0 && px < ppx + 200.0
-                && py >= row1_y - 2.0 && py < row1_y + 18.0
-            {
-                self.grid_snap_enabled = !self.grid_snap_enabled;
-                self.grid_snap_pending = Some(self.grid_snap_enabled);
-                return true;
-            }
-            let row2_y = ppy + 108.0;
-            if px >= ppx + 16.0 && px < ppx + 200.0
-                && py >= row2_y - 2.0 && py < row2_y + 18.0
-            {
-                self.network_grid_enabled = !self.network_grid_enabled;
-                self.network_grid_pending = Some(self.network_grid_enabled);
+                self.spin_grid_x.unfocus();
+                self.spin_grid_y.unfocus();
+                self.spin_skipped_row_h.unfocus();
+                self.spin_skipped_col_w.unfocus();
+                self.spin_grid_thickness.unfocus();
+                self.spin_origin_size.unfocus();
+                self.spin_camera_pivot_size.unfocus();
+                self.toggle_grid_snap.unfocus();
+                self.toggle_network_grid.unfocus();
+                self.toggle_show_grid.unfocus();
+                self.toggle_show_cube.unfocus();
+                self.toggle_show_origin.unfocus();
+                self.toggle_show_camera_pivot.unfocus();
+                self.visible = false;
                 return true;
             }
-            for &(btn_id, bx, by) in &self.spin_btns(ppx, ppy) {
-                if px >= bx && px < bx + 22.0 && py >= by && py < by + 20.0 {
-                    let (val, delta, min_val, max_val, pending_id) = match btn_id {
-                        0 => (&mut self.grid_size_x, -5.0, 10.0, 200.0, 0),
-                        1 => (&mut self.grid_size_x, 5.0, 10.0, 200.0, 0),
-                        2 => (&mut self.grid_size_y, -5.0, 5.0, 100.0, 1),
-                        3 => (&mut self.grid_size_y, 5.0, 5.0, 100.0, 1),
-                        4 => (&mut self.skipped_row_h, -5.0, 0.0, 150.0, 2),
-                        5 => (&mut self.skipped_row_h, 5.0, 0.0, 150.0, 2),
-                        6 => (&mut self.skipped_col_w, -5.0, 0.0, 150.0, 3),
-                        7 => (&mut self.skipped_col_w, 5.0, 0.0, 150.0, 3),
-                        _ => unreachable!(),
-                    };
-                    let new = (*val + delta).clamp(min_val, max_val);
-                    if (new - *val).abs() > 0.01 {
-                        *val = new;
-                        self.grid_size_pending = Some((pending_id, *val));
-                    }
+            for (i, (tx, ty, tw, th)) in self.tab_rects(ppx, ppy).iter().enumerate() {
+                if px >= *tx && px < *tx + *tw && py >= *ty && py < *ty + *th {
+                    self.color_selector.unfocus();
+                    self.spin_grid_x.unfocus();
+                    self.spin_grid_y.unfocus();
+                    self.spin_skipped_row_h.unfocus();
+                    self.spin_skipped_col_w.unfocus();
+                    self.spin_grid_thickness.unfocus();
+                    self.spin_origin_size.unfocus();
+                    self.spin_camera_pivot_size.unfocus();
+                    self.toggle_grid_snap.unfocus();
+                    self.toggle_network_grid.unfocus();
+                    self.toggle_show_grid.unfocus();
+                    self.toggle_show_cube.unfocus();
+                    self.toggle_show_origin.unfocus();
+                    self.toggle_show_camera_pivot.unfocus();
+                    self.active_page = i;
+                    self.scroll_y = 0.0;
+                    self.scroll_velocity = 0.0;
+                    self.update_child_layouts();
                     return true;
                 }
             }
+        }
+
+        // Now dispatch to child widgets
+        let mut handled = false;
+        if self.active_page == 1 {
+            if self.toggle_grid_snap.mouse_input(button, state, px, py) {
+                self.toggle_network_grid.unfocus();
+                self.spin_grid_x.unfocus();
+                self.spin_grid_y.unfocus();
+                self.spin_skipped_row_h.unfocus();
+                self.spin_skipped_col_w.unfocus();
+                handled = true;
+            } else if self.toggle_network_grid.mouse_input(button, state, px, py) {
+                self.toggle_grid_snap.unfocus();
+                self.spin_grid_x.unfocus();
+                self.spin_grid_y.unfocus();
+                self.spin_skipped_row_h.unfocus();
+                self.spin_skipped_col_w.unfocus();
+                handled = true;
+            } else if self.spin_grid_x.mouse_input(button, state, px, py) {
+                self.toggle_grid_snap.unfocus();
+                self.toggle_network_grid.unfocus();
+                self.spin_grid_y.unfocus();
+                self.spin_skipped_row_h.unfocus();
+                self.spin_skipped_col_w.unfocus();
+                handled = true;
+            } else if self.spin_grid_y.mouse_input(button, state, px, py) {
+                self.toggle_grid_snap.unfocus();
+                self.toggle_network_grid.unfocus();
+                self.spin_grid_x.unfocus();
+                self.spin_skipped_row_h.unfocus();
+                self.spin_skipped_col_w.unfocus();
+                handled = true;
+            } else if self.spin_skipped_row_h.mouse_input(button, state, px, py) {
+                self.toggle_grid_snap.unfocus();
+                self.toggle_network_grid.unfocus();
+                self.spin_grid_x.unfocus();
+                self.spin_grid_y.unfocus();
+                self.spin_skipped_col_w.unfocus();
+                handled = true;
+            } else if self.spin_skipped_col_w.mouse_input(button, state, px, py) {
+                self.toggle_grid_snap.unfocus();
+                self.toggle_network_grid.unfocus();
+                self.spin_grid_x.unfocus();
+                self.spin_grid_y.unfocus();
+                self.spin_skipped_row_h.unfocus();
+                handled = true;
+            }
         } else if self.active_page == 2 {
-            let row1_y = ppy + 84.0;
-            if px >= ppx + 16.0 && px < ppx + 200.0
-                && py >= row1_y - 2.0 && py < row1_y + 18.0
-            {
+            if self.toggle_show_grid.mouse_input(button, state, px, py) {
+                self.toggle_show_cube.unfocus();
+                self.toggle_show_origin.unfocus();
+                self.toggle_show_camera_pivot.unfocus();
+                self.spin_grid_thickness.unfocus();
+                self.spin_origin_size.unfocus();
+                self.spin_camera_pivot_size.unfocus();
                 self.color_selector.unfocus();
-                self.show_grid_enabled = !self.show_grid_enabled;
-                self.show_grid_pending = Some(self.show_grid_enabled);
-                return true;
-            }
-            let row2_y = ppy + 108.0;
-            if px >= ppx + 16.0 && px < ppx + 200.0
-                && py >= row2_y - 2.0 && py < row2_y + 18.0
-            {
+                handled = true;
+            } else if self.toggle_show_cube.mouse_input(button, state, px, py) {
+                self.toggle_show_grid.unfocus();
+                self.toggle_show_origin.unfocus();
+                self.toggle_show_camera_pivot.unfocus();
+                self.spin_grid_thickness.unfocus();
+                self.spin_origin_size.unfocus();
+                self.spin_camera_pivot_size.unfocus();
                 self.color_selector.unfocus();
-                self.show_cube_enabled = !self.show_cube_enabled;
-                self.show_cube_pending = Some(self.show_cube_enabled);
-                return true;
-            }
-            let row3_y = ppy + 132.0;
-            if px >= ppx + 16.0 && px < ppx + 200.0
-                && py >= row3_y - 2.0 && py < row3_y + 18.0
-            {
+                handled = true;
+            } else if self.toggle_show_origin.mouse_input(button, state, px, py) {
+                self.toggle_show_grid.unfocus();
+                self.toggle_show_cube.unfocus();
+                self.toggle_show_camera_pivot.unfocus();
+                self.spin_grid_thickness.unfocus();
+                self.spin_origin_size.unfocus();
+                self.spin_camera_pivot_size.unfocus();
                 self.color_selector.unfocus();
-                self.show_origin_enabled = !self.show_origin_enabled;
-                self.show_origin_pending = Some(self.show_origin_enabled);
-                return true;
-            }
-
-            if self.color_selector.mouse_input(button, state, px, py) {
-                return true;
-            } else {
+                handled = true;
+            } else if self.toggle_show_camera_pivot.mouse_input(button, state, px, py) {
+                self.toggle_show_grid.unfocus();
+                self.toggle_show_cube.unfocus();
+                self.toggle_show_origin.unfocus();
+                self.spin_grid_thickness.unfocus();
+                self.spin_origin_size.unfocus();
+                self.spin_camera_pivot_size.unfocus();
+                self.color_selector.unfocus();
+                handled = true;
+            } else if self.spin_grid_thickness.mouse_input(button, state, px, py) {
+                self.toggle_show_grid.unfocus();
+                self.toggle_show_cube.unfocus();
+                self.toggle_show_origin.unfocus();
+                self.toggle_show_camera_pivot.unfocus();
+                self.spin_origin_size.unfocus();
+                self.spin_camera_pivot_size.unfocus();
                 self.color_selector.unfocus();
+                handled = true;
+            } else if self.spin_origin_size.mouse_input(button, state, px, py) {
+                self.toggle_show_grid.unfocus();
+                self.toggle_show_cube.unfocus();
+                self.toggle_show_origin.unfocus();
+                self.toggle_show_camera_pivot.unfocus();
+                self.spin_grid_thickness.unfocus();
+                self.spin_camera_pivot_size.unfocus();
+                self.color_selector.unfocus();
+                handled = true;
+            } else if self.spin_camera_pivot_size.mouse_input(button, state, px, py) {
+                self.toggle_show_grid.unfocus();
+                self.toggle_show_cube.unfocus();
+                self.toggle_show_origin.unfocus();
+                self.toggle_show_camera_pivot.unfocus();
+                self.spin_grid_thickness.unfocus();
+                self.spin_origin_size.unfocus();
+                self.color_selector.unfocus();
+                handled = true;
+            } else if self.color_selector.mouse_input(button, state, px, py) {
+                self.toggle_show_grid.unfocus();
+                self.toggle_show_cube.unfocus();
+                self.toggle_show_origin.unfocus();
+                self.toggle_show_camera_pivot.unfocus();
+                self.spin_grid_thickness.unfocus();
+                self.spin_origin_size.unfocus();
+                self.spin_camera_pivot_size.unfocus();
+                handled = true;
             }
         }
+
+        if handled {
+            return true;
+        }
+
+        if state == ElementState::Pressed {
+            // Unfocus everything if clicked elsewhere in the panel
+            self.spin_grid_x.unfocus();
+            self.spin_grid_y.unfocus();
+            self.spin_skipped_row_h.unfocus();
+            self.spin_skipped_col_w.unfocus();
+            self.spin_grid_thickness.unfocus();
+            self.spin_origin_size.unfocus();
+            self.spin_camera_pivot_size.unfocus();
+            self.color_selector.unfocus();
+            self.toggle_grid_snap.unfocus();
+            self.toggle_network_grid.unfocus();
+            self.toggle_show_grid.unfocus();
+            self.toggle_show_cube.unfocus();
+            self.toggle_show_origin.unfocus();
+            self.toggle_show_camera_pivot.unfocus();
+        }
+
         false
     }
 
     fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
         if !self.visible { return false; }
-        if self.active_page == 2 && self.color_selector.keyboard_input(event) {
-            return true;
+        if self.active_page == 1 {
+            if self.spin_grid_x.keyboard_input(event) { return true; }
+            if self.spin_grid_y.keyboard_input(event) { return true; }
+            if self.spin_skipped_row_h.keyboard_input(event) { return true; }
+            if self.spin_skipped_col_w.keyboard_input(event) { return true; }
+        } else if self.active_page == 2 {
+            if self.spin_grid_thickness.keyboard_input(event) { return true; }
+            if self.spin_origin_size.keyboard_input(event) { return true; }
+            if self.spin_camera_pivot_size.keyboard_input(event) { return true; }
+            if self.color_selector.keyboard_input(event) { return true; }
         }
         if event.state == ElementState::Pressed && event.logical_key == Key::Named(NamedKey::Escape) {
             self.color_selector.unfocus();
+            self.spin_grid_x.unfocus();
+            self.spin_grid_y.unfocus();
+            self.spin_skipped_row_h.unfocus();
+            self.spin_skipped_col_w.unfocus();
+            self.spin_grid_thickness.unfocus();
+            self.spin_origin_size.unfocus();
+            self.spin_camera_pivot_size.unfocus();
+            self.toggle_grid_snap.unfocus();
+            self.toggle_network_grid.unfocus();
+            self.toggle_show_grid.unfocus();
+            self.toggle_show_cube.unfocus();
+            self.toggle_show_origin.unfocus();
+            self.toggle_show_camera_pivot.unfocus();
             self.visible = false;
             return true;
         }
@@ -618,38 +1212,48 @@ impl Widget for ConfigDialog {
             quads.push((tx, ty, tw, th, bg));
         }
 
-        // Toggle and spin hover highlights on Network page
-        if self.visible && self.active_page == 1 {
-            if self.grid_snap_hovered {
-                let row1_y = py + 84.0;
-                quads.push((px + 14.0, row1_y - 2.0, 186.0, 20.0, [0.25, 0.25, 0.35, 0.4]));
-            }
-            if self.network_grid_hovered {
-                let row2_y = py + 108.0;
-                quads.push((px + 14.0, row2_y - 2.0, 186.0, 20.0, [0.25, 0.25, 0.35, 0.4]));
-            }
-            if let Some(sb) = self.hovered_spin_btn {
-                for &(btn_id, bx, by) in &self.spin_btns(px, py) {
-                    if btn_id == sb {
-                        quads.push((bx, by, 22.0, 20.0, [0.35, 0.35, 0.45, 0.5]));
-                        break;
-                    }
+        let y_min = py + 56.0;
+        let y_max = py + 584.0;
+        let filter_quads = |q: Vec<(f32, f32, f32, f32, [f32; 4])>| {
+            q.into_iter().filter(|&(_, qy, _, qh, _)| qy >= y_min && qy + qh <= y_max).collect::<Vec<_>>()
+        };
+
+        if self.active_page == 1 || self.active_page == 2 {
+            quads.extend(filter_quads(self.section_quads.clone()));
+        }
+
+        if self.active_page == 1 {
+            quads.extend(filter_quads(self.toggle_grid_snap.extra_quads()));
+            quads.extend(filter_quads(self.toggle_network_grid.extra_quads()));
+
+            let draw_spin = |spin: &Spinbox, q: &mut Vec<(f32, f32, f32, f32, [f32; 4])>| {
+                let (sx, sy, sw, sh) = spin.rect();
+                if sy >= y_min && sy + sh <= y_max {
+                    q.push((sx, sy, sw, sh, colors::SPINBOX_BG));
+                    q.extend(spin.extra_quads().into_iter().filter(|&(_, qy, _, qh, _)| qy >= y_min && qy + qh <= y_max));
                 }
-            }
-        } else if self.visible && self.active_page == 2 {
-            if self.show_grid_hovered {
-                let row1_y = py + 84.0;
-                quads.push((px + 14.0, row1_y - 2.0, 186.0, 20.0, [0.25, 0.25, 0.35, 0.4]));
-            }
-            if self.show_cube_hovered {
-                let row2_y = py + 108.0;
-                quads.push((px + 14.0, row2_y - 2.0, 186.0, 20.0, [0.25, 0.25, 0.35, 0.4]));
-            }
-            if self.show_origin_hovered {
-                let row3_y = py + 132.0;
-                quads.push((px + 14.0, row3_y - 2.0, 186.0, 20.0, [0.25, 0.25, 0.35, 0.4]));
-            }
-            quads.extend(self.color_selector.extra_quads());
+            };
+            draw_spin(&self.spin_grid_x, &mut quads);
+            draw_spin(&self.spin_grid_y, &mut quads);
+            draw_spin(&self.spin_skipped_row_h, &mut quads);
+            draw_spin(&self.spin_skipped_col_w, &mut quads);
+        } else if self.active_page == 2 {
+            quads.extend(filter_quads(self.toggle_show_grid.extra_quads()));
+            quads.extend(filter_quads(self.toggle_show_cube.extra_quads()));
+            quads.extend(filter_quads(self.toggle_show_origin.extra_quads()));
+            quads.extend(filter_quads(self.toggle_show_camera_pivot.extra_quads()));
+
+            let draw_spin = |spin: &Spinbox, q: &mut Vec<(f32, f32, f32, f32, [f32; 4])>| {
+                let (sx, sy, sw, sh) = spin.rect();
+                if sy >= y_min && sy + sh <= y_max {
+                    q.push((sx, sy, sw, sh, colors::SPINBOX_BG));
+                    q.extend(spin.extra_quads().into_iter().filter(|&(_, qy, _, qh, _)| qy >= y_min && qy + qh <= y_max));
+                }
+            };
+            draw_spin(&self.spin_grid_thickness, &mut quads);
+            draw_spin(&self.spin_origin_size, &mut quads);
+            draw_spin(&self.spin_camera_pivot_size, &mut quads);
+            quads.extend(filter_quads(self.color_selector.extra_quads()));
         }
 
         quads
@@ -668,7 +1272,7 @@ impl Widget for ConfigDialog {
         let tabs = self.tab_rects(px, py);
         let tab_labels = ["General", "Network", "Viewport", "Bindings"];
         for (i, &(tx, ty, _, _)) in tabs.iter().enumerate() {
-            let color = if i == self.active_page { [0xcc, 0xcc, 0xd4] } else { [0x88, 0x88, 0x99] };
+            let color = if i == self.active_page { [0xcc, 0xcc, 0xd4] } else { [0xaa, 0xaa, 0xbb] };
             labels.push(TextLabel {
                 text: tab_labels[i].into(),
                 x: tx + 8.0,
@@ -678,62 +1282,43 @@ impl Widget for ConfigDialog {
             });
         }
 
-        // Page content
-        let content_y = py + 60.0;
-        if self.active_page == 0 {
-            labels.push(TextLabel { text: "General settings for Clear Design Interface".into(), x: px + 16.0, y: content_y, font_size: 12.0, color: [0x88, 0x88, 0x99] });
-        } else if self.active_page == 1 {
-            labels.push(TextLabel { text: "Network Configuration".into(), x: px + 16.0, y: content_y, font_size: 13.0, color: [0xcc, 0xcc, 0xd4] });
-            let snap_text = if self.grid_snap_enabled { "[\u{2713}] Snap to Grid" } else { "[ ] Snap to Grid" };
-            labels.push(TextLabel { text: snap_text.into(), x: px + 20.0, y: content_y + 26.0, font_size: 12.0, color: [0xaa, 0xaa, 0xbb] });
-            let grid_text = if self.network_grid_enabled { "[\u{2713}] Show Grid" } else { "[ ] Show Grid" };
-            labels.push(TextLabel { text: grid_text.into(), x: px + 20.0, y: content_y + 50.0, font_size: 12.0, color: [0xaa, 0xaa, 0xbb] });
-            labels.push(TextLabel { text: "Grid Spacing".into(), x: px + 16.0, y: content_y + 78.0, font_size: 12.0, color: [0x88, 0x88, 0x99] });
-            let gx = self.grid_size_x as i32;
-            labels.push(TextLabel { text: "Grid X:".into(), x: px + 16.0, y: content_y + 100.0, font_size: 12.0, color: [0xbb, 0xbb, 0xcc] });
-            labels.push(TextLabel { text: "\u{2212}".into(), x: px + 170.0, y: content_y + 100.0, font_size: 12.0, color: [0xcc, 0xcc, 0xd4] });
-            labels.push(TextLabel { text: format!("{}", gx), x: px + 188.0, y: content_y + 100.0, font_size: 12.0, color: [0xdd, 0xdd, 0x88] });
-            labels.push(TextLabel { text: "+".into(), x: px + 220.0, y: content_y + 100.0, font_size: 12.0, color: [0xcc, 0xcc, 0xd4] });
-            let gy = self.grid_size_y as i32;
-            labels.push(TextLabel { text: "Grid Y:".into(), x: px + 16.0, y: content_y + 122.0, font_size: 12.0, color: [0xbb, 0xbb, 0xcc] });
-            labels.push(TextLabel { text: "\u{2212}".into(), x: px + 170.0, y: content_y + 122.0, font_size: 12.0, color: [0xcc, 0xcc, 0xd4] });
-            labels.push(TextLabel { text: format!("{}", gy), x: px + 188.0, y: content_y + 122.0, font_size: 12.0, color: [0xdd, 0xdd, 0x88] });
-            labels.push(TextLabel { text: "+".into(), x: px + 220.0, y: content_y + 122.0, font_size: 12.0, color: [0xcc, 0xcc, 0xd4] });
-
-            let srh = self.skipped_row_h as i32;
-            labels.push(TextLabel { text: "Skipped Row H:".into(), x: px + 16.0, y: content_y + 144.0, font_size: 12.0, color: [0xbb, 0xbb, 0xcc] });
-            labels.push(TextLabel { text: "\u{2212}".into(), x: px + 170.0, y: content_y + 144.0, font_size: 12.0, color: [0xcc, 0xcc, 0xd4] });
-            labels.push(TextLabel { text: format!("{}", srh), x: px + 188.0, y: content_y + 144.0, font_size: 12.0, color: [0xdd, 0xdd, 0x88] });
-            labels.push(TextLabel { text: "+".into(), x: px + 220.0, y: content_y + 144.0, font_size: 12.0, color: [0xcc, 0xcc, 0xd4] });
-
-            let scw = self.skipped_col_w as i32;
-            labels.push(TextLabel { text: "Skipped Col W:".into(), x: px + 16.0, y: content_y + 166.0, font_size: 12.0, color: [0xbb, 0xbb, 0xcc] });
-            labels.push(TextLabel { text: "\u{2212}".into(), x: px + 170.0, y: content_y + 166.0, font_size: 12.0, color: [0xcc, 0xcc, 0xd4] });
-            labels.push(TextLabel { text: format!("{}", scw), x: px + 188.0, y: content_y + 166.0, font_size: 12.0, color: [0xdd, 0xdd, 0x88] });
-            labels.push(TextLabel { text: "+".into(), x: px + 220.0, y: content_y + 166.0, font_size: 12.0, color: [0xcc, 0xcc, 0xd4] });
+        let layout = self.compute_layout(px, py);
+        labels.extend(layout.labels);
+
+        let y_min = py + 56.0;
+        let y_max = py + 584.0;
+        let filter_labels = |l: Vec<TextLabel>| {
+            l.into_iter().filter(|lbl| lbl.y >= y_min && lbl.y <= y_max).collect::<Vec<_>>()
+        };
+
+        if self.active_page == 1 || self.active_page == 2 {
+            labels.extend(self.section_labels.iter().filter(|lbl| lbl.y >= y_min && lbl.y <= y_max).map(|l| TextLabel {
+                text: l.text.clone(),
+                x: l.x,
+                y: l.y,
+                font_size: l.font_size,
+                color: l.color,
+            }));
+        }
+
+        if self.active_page == 1 {
+            labels.extend(filter_labels(self.toggle_grid_snap.text_labels()));
+            labels.extend(filter_labels(self.toggle_network_grid.text_labels()));
+
+            labels.extend(filter_labels(self.spin_grid_x.text_labels()));
+            labels.extend(filter_labels(self.spin_grid_y.text_labels()));
+            labels.extend(filter_labels(self.spin_skipped_row_h.text_labels()));
+            labels.extend(filter_labels(self.spin_skipped_col_w.text_labels()));
         } else if self.active_page == 2 {
-            labels.push(TextLabel { text: "Viewport Configuration".into(), x: px + 16.0, y: content_y, font_size: 13.0, color: [0xcc, 0xcc, 0xd4] });
-            let grid_text = if self.show_grid_enabled { "[\u{2713}] Show Grid Guide" } else { "[ ] Show Grid Guide" };
-            labels.push(TextLabel { text: grid_text.into(), x: px + 20.0, y: content_y + 26.0, font_size: 12.0, color: [0xaa, 0xaa, 0xbb] });
-            let cube_text = if self.show_cube_enabled { "[\u{2713}] Show Reference Cube" } else { "[ ] Show Reference Cube" };
-            labels.push(TextLabel { text: cube_text.into(), x: px + 20.0, y: content_y + 50.0, font_size: 12.0, color: [0xaa, 0xaa, 0xbb] });
-            let origin_text = if self.show_origin_enabled { "[\u{2713}] Show Origin Axes" } else { "[ ] Show Origin Axes" };
-            labels.push(TextLabel { text: origin_text.into(), x: px + 20.0, y: content_y + 74.0, font_size: 12.0, color: [0xaa, 0xaa, 0xbb] });
-
-            labels.extend(self.color_selector.text_labels());
-        } else {
-            labels.push(TextLabel { text: "Keyboard Shortcuts".into(), x: px + 16.0, y: content_y, font_size: 13.0, color: [0xcc, 0xcc, 0xd4] });
-            let shortcuts = [
-                ("Ctrl+G", "Toggle Grid (viewport)"),
-                ("Ctrl+E", "Toggle Cube"),
-                ("Ctrl+A", "Square Viewport Aspect"),
-                ("Ctrl+,", "Configure Dialog"),
-            ];
-            for (i, (key, desc)) in shortcuts.iter().enumerate() {
-                let y = content_y + 28.0 + i as f32 * 22.0;
-                labels.push(TextLabel { text: (*key).into(), x: px + 32.0, y, font_size: 12.0, color: [0xdd, 0xdd, 0x88] });
-                labels.push(TextLabel { text: (*desc).into(), x: px + 130.0, y, font_size: 12.0, color: [0xaa, 0xaa, 0xbb] });
-            }
+            labels.extend(filter_labels(self.toggle_show_grid.text_labels()));
+            labels.extend(filter_labels(self.toggle_show_cube.text_labels()));
+            labels.extend(filter_labels(self.toggle_show_origin.text_labels()));
+            labels.extend(filter_labels(self.toggle_show_camera_pivot.text_labels()));
+
+            labels.extend(filter_labels(self.spin_grid_thickness.text_labels()));
+            labels.extend(filter_labels(self.spin_origin_size.text_labels()));
+            labels.extend(filter_labels(self.spin_camera_pivot_size.text_labels()));
+            labels.extend(filter_labels(self.color_selector.text_labels()));
         }
 
         labels
@@ -838,6 +1423,7 @@ enum Action {
     ToggleConfigure,
     ToggleSpreadsheet,
     ToggleOrigin,
+    ToggleCameraPivot,
 }
 
 struct KeyBind {
@@ -1017,6 +1603,71 @@ fn sphere_vertex(center: Vec3, point: Vec3) -> GVertex {
     }
 }
 
+fn line_vertices(start: Vec3, end: Vec3, thickness: f32) -> Geometry {
+    let mut vertices = Vec::new();
+    let dir = (end - start).normalize_or_zero();
+    if dir.length_squared() < 0.0001 {
+        return Geometry { vertices };
+    }
+    
+    // Find two orthogonal vectors to dir
+    let up = if dir.x.abs() > 0.9 { Vec3::Y } else { Vec3::X };
+    let u = dir.cross(up).normalize();
+    let v = dir.cross(u).normalize();
+    
+    let t = thickness * 0.5;
+    
+    // 8 corners of the box
+    let c0 = start - t * u - t * v;
+    let c1 = start + t * u - t * v;
+    let c2 = start + t * u + t * v;
+    let c3 = start - t * u + t * v;
+    
+    let c4 = end - t * u - t * v;
+    let c5 = end + t * u - t * v;
+    let c6 = end + t * u + t * v;
+    let c7 = end - t * u + t * v;
+    
+    // Helper to add a triangle face
+    let mut add_quad = |p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, normal: Vec3, color: [f32; 3]| {
+        let make_vertex = |p: Vec3| {
+            let mut attributes = std::collections::HashMap::new();
+            attributes.insert("Norm".to_string(), GAttribute::Float3(normal.to_array()));
+            attributes.insert("UV".to_string(), GAttribute::Float2([0.0, 0.0]));
+            GVertex {
+                pos: p.to_array(),
+                col: color,
+                attributes,
+            }
+        };
+        // Triangle 1: p0, p1, p2
+        vertices.push(make_vertex(p0));
+        vertices.push(make_vertex(p1));
+        vertices.push(make_vertex(p2));
+        // Triangle 2: p0, p2, p3
+        vertices.push(make_vertex(p0));
+        vertices.push(make_vertex(p2));
+        vertices.push(make_vertex(p3));
+    };
+
+    let col = [0.85, 0.45, 0.35]; // distinct color for lines
+    
+    // Front face (start cap)
+    add_quad(c0, c1, c2, c3, -dir, col);
+    // Back face (end cap)
+    add_quad(c5, c4, c7, c6, dir, col);
+    // Left face
+    add_quad(c4, c0, c3, c7, -u, col);
+    // Right face
+    add_quad(c1, c5, c6, c2, u, col);
+    // Top face
+    add_quad(c3, c2, c6, c7, v, col);
+    // Bottom face
+    add_quad(c0, c4, c5, c1, -v, col);
+
+    Geometry { vertices }
+}
+
 fn node_param_f32(node: &FsNode, name: &str, fallback: f32) -> f32 {
     node.params.iter()
         .find(|p| p.name.eq_ignore_ascii_case(name))
@@ -1140,6 +1791,16 @@ fn network_sphere_vertices(root: &FsNode) -> Geometry {
                 let center = Vec3::new((idx % 4) as f32 * 1.25 - 1.875, 0.55, -((idx / 4) as f32) * 1.25);
                 out.merge(sphere_vertices(center, node_param_f32(node, "Radius", 0.5).max(0.05)));
             }
+        } else if node.node_type.eq_ignore_ascii_case("line") {
+            let idx = *count;
+            *count += 1;
+            if node.geometry_visible {
+                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);
+                out.merge(line_vertices(start, end, thickness));
+            }
         }
         for child in &node.children {
             visit(child, count, out);
@@ -1157,7 +1818,7 @@ fn network_sphere_vertices(root: &FsNode) -> Geometry {
 fn find_sphere_index(root: &FsNode, target: &FsNode) -> Option<usize> {
     fn visit(node: &FsNode, target: &FsNode, count: &mut usize) -> Option<usize> {
         let is_target = std::ptr::eq(node, target);
-        if node.node_type.eq_ignore_ascii_case("sphere") {
+        if node.node_type.eq_ignore_ascii_case("sphere") || node.node_type.eq_ignore_ascii_case("line") {
             let idx = *count;
             *count += 1;
             if is_target {
@@ -1306,84 +1967,84 @@ fn add_pyramid_z(base_center: Vec3, base_size: f32, height: f32, color: [f32; 3]
     }
 }
 
-fn origin_vectors_vertices() -> Vec<Vertex3D> {
+fn origin_vectors_vertices(scale: f32) -> Vec<Vertex3D> {
     let mut verts = Vec::new();
     
-    let t = 0.008; 
-    let a_size = 0.024;
-    let a_height = 0.15;
+    let t = 0.008 * scale; 
+    let a_size = 0.024 * scale;
+    let a_height = 0.15 * scale;
+    let axis_len = 0.85 * scale;
+    let half_axis_len = 0.425 * scale;
+    
+    // Red for X-axis (points to +scale)
+    let red = [0.9, 0.1, 0.1];
+    add_box(Vec3::new(half_axis_len, 0.0, 0.0), Vec3::new(axis_len, t, t), red, &mut verts);
+    add_pyramid_x(Vec3::new(axis_len, 0.0, 0.0), a_size, a_height, red, &mut verts);
+
+    // Green for Y-axis (points to +scale)
+    let green = [0.1, 0.8, 0.1];
+    add_box(Vec3::new(0.0, half_axis_len, 0.0), Vec3::new(t, axis_len, t), green, &mut verts);
+    add_pyramid_y(Vec3::new(0.0, axis_len, 0.0), a_size, a_height, green, &mut verts);
+
+    // Blue for Z-axis (points to +scale)
+    let blue = [0.1, 0.1, 0.9];
+    add_box(Vec3::new(0.0, 0.0, half_axis_len), Vec3::new(t, t, axis_len), blue, &mut verts);
+    add_pyramid_z(Vec3::new(0.0, 0.0, axis_len), a_size, a_height, blue, &mut verts);
+
+    verts
+}
+
+fn camera_pivot_vertices(scale: f32) -> Vec<Vertex3D> {
+    let mut verts = Vec::new();
+    let t = 0.002 * scale; 
+    let len = 0.4 * scale;
     
-    // Red for X-axis (points to +1.0)
+    // Red for X-axis
     let red = [0.9, 0.1, 0.1];
-    add_box(Vec3::new(0.425, 0.0, 0.0), Vec3::new(0.85, t, t), red, &mut verts);
-    add_pyramid_x(Vec3::new(0.85, 0.0, 0.0), a_size, a_height, red, &mut verts);
+    add_box(Vec3::new(len * 0.5, 0.0, 0.0), Vec3::new(len, t, t), red, &mut verts);
 
-    // Green for Y-axis (points to +1.0)
+    // Green for Y-axis
     let green = [0.1, 0.8, 0.1];
-    add_box(Vec3::new(0.0, 0.425, 0.0), Vec3::new(t, 0.85, t), green, &mut verts);
-    add_pyramid_y(Vec3::new(0.0, 0.85, 0.0), a_size, a_height, green, &mut verts);
+    add_box(Vec3::new(0.0, len * 0.5, 0.0), Vec3::new(t, len, t), green, &mut verts);
 
-    // Blue for Z-axis (points to +1.0)
+    // Blue for Z-axis
     let blue = [0.1, 0.1, 0.9];
-    add_box(Vec3::new(0.0, 0.0, 0.425), Vec3::new(t, t, 0.85), blue, &mut verts);
-    add_pyramid_z(Vec3::new(0.0, 0.0, 0.85), a_size, a_height, blue, &mut verts);
+    add_box(Vec3::new(0.0, 0.0, len * 0.5), Vec3::new(t, t, len), blue, &mut verts);
 
     verts
 }
 
-fn grid_vertices() -> Vec<Vertex3D> {
-    let half_w = 0.015;
+fn grid_vertices(thickness: f32) -> Vec<Vertex3D> {
     let color = [0.35, 0.35, 0.40];
     let range = 4.0;
     let step = 1.0;
-    let y = 0.0;
-    let mut verts = Vec::new();
+    let mut geom = Geometry::new();
 
     let mut z = -range;
     while z <= range {
-        let z0 = z - half_w;
-        let z1 = z + half_w;
-        // CCW (visible from above)
-        verts.push(Vertex3D { position: [-range, y, z1], color });
-        verts.push(Vertex3D { position: [range, y, z1], color });
-        verts.push(Vertex3D { position: [-range, y, z0], color });
-        verts.push(Vertex3D { position: [range, y, z1], color });
-        verts.push(Vertex3D { position: [range, y, z0], color });
-        verts.push(Vertex3D { position: [-range, y, z0], color });
-
-        // CW (visible from below)
-        verts.push(Vertex3D { position: [-range, y, z0], color });
-        verts.push(Vertex3D { position: [range, y, z0], color });
-        verts.push(Vertex3D { position: [-range, y, z1], color });
-        verts.push(Vertex3D { position: [range, y, z0], color });
-        verts.push(Vertex3D { position: [range, y, z1], color });
-        verts.push(Vertex3D { position: [-range, y, z1], color });
+        let start = Vec3::new(-range, 0.0, z);
+        let end = Vec3::new(range, 0.0, z);
+        let mut line_geom = line_vertices(start, end, thickness);
+        for v in &mut line_geom.vertices {
+            v.col = color;
+        }
+        geom.merge(line_geom);
         z += step;
     }
 
     let mut x = -range;
     while x <= range {
-        let x0 = x - half_w;
-        let x1 = x + half_w;
-        // CCW (visible from above)
-        verts.push(Vertex3D { position: [x0, y, range], color });
-        verts.push(Vertex3D { position: [x1, y, range], color });
-        verts.push(Vertex3D { position: [x0, y, -range], color });
-        verts.push(Vertex3D { position: [x1, y, range], color });
-        verts.push(Vertex3D { position: [x1, y, -range], color });
-        verts.push(Vertex3D { position: [x0, y, -range], color });
-
-        // CW (visible from below)
-        verts.push(Vertex3D { position: [x0, y, -range], color });
-        verts.push(Vertex3D { position: [x1, y, -range], color });
-        verts.push(Vertex3D { position: [x0, y, range], color });
-        verts.push(Vertex3D { position: [x1, y, -range], color });
-        verts.push(Vertex3D { position: [x1, y, range], color });
-        verts.push(Vertex3D { position: [x0, y, range], color });
+        let start = Vec3::new(x, 0.0, -range);
+        let end = Vec3::new(x, 0.0, range);
+        let mut line_geom = line_vertices(start, end, thickness);
+        for v in &mut line_geom.vertices {
+            v.col = color;
+        }
+        geom.merge(line_geom);
         x += step;
     }
 
-    verts
+    geom.to_vertex3d_vec()
 }
 
 fn quad_vertices(
@@ -1456,6 +2117,8 @@ struct State {
     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,
@@ -1467,6 +2130,7 @@ struct State {
     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,
@@ -1475,9 +2139,14 @@ struct State {
     show_grid: bool,
     show_cube: bool,
     show_origin: bool,
+    show_camera_pivot: bool,
     viewport_bg_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>,
@@ -1550,9 +2219,52 @@ struct State {
     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,
 }
 
 impl State {
+    fn save_settings(&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,
+            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,
+        };
+        settings.save();
+    }
+
+    fn update_grid_geometry(&mut self) {
+        let grid_verts = grid_vertices(self.grid_thickness);
+        self.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.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.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 content_left_w(&self) -> f32 { self.splitter1_x }
@@ -1805,6 +2517,27 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
     (headers, rows)
 }
 
+    fn on_path_changed(&mut self) {
+        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 sync_nodes(&mut self) {
         let node_infos: Vec<Option<(String, (f32, f32))>> = {
             let dir = self.current_dir();
@@ -1898,6 +2631,17 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                         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;
+                    }
                 }
             }
 
@@ -2004,6 +2748,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
     }
 
     async fn new(window: Arc<Window>) -> Self {
+        let settings = DesignSettings::load();
         let scale = window.scale_factor();
         let physical_size = window.inner_size();
         let pw = physical_size.width.max(1);
@@ -2159,6 +2904,26 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             }],
         });
 
+        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()),
@@ -2172,20 +2937,28 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             mapped_at_creation: false,
         });
 
-        let grid_verts = grid_vertices();
+        let grid_verts = grid_vertices(settings.grid_thickness);
         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,
+            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
         });
 
-        let origin_verts = origin_vectors_vertices();
+        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,
+            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 {
@@ -2313,7 +3086,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             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_item("File", &["New", "Open", "Save"]).with_item("Edit", &["Undo", "Redo"]).with_item("View", &["Zoom In", "Zoom Out"])),
-            Box::new(MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("1: Viewport").with_item("Camera", &["Perspective", "Orthographic"]).with_item("Display", &["Square Aspect"]).with_item("Guides", &["Show Grid", "Cube", "Origin"])),
+            Box::new(MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("1: Viewport").with_item("Camera", &["Perspective", "Orthographic"]).with_item("Display", &["Square Aspect"]).with_item("Guides", &["Show Grid", "Cube", "Origin", "Camera Pivot"])),
             Box::new(MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("2: Parameters").with_item("Preset", &["Default", "Custom"]).with_item("Reset", &["All"])),
         ];
         for _ in 0..NODE_SLOT_COUNT {
@@ -2321,7 +3094,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
         }
         widgets.push(Box::new(StatusBar::new()));
         widgets.push(Box::new(Breadcrumb::new()));
-        widgets.push(Box::new(ConfigDialog::new()));
+        widgets.push(Box::new(ConfigDialog::new(&settings)));
         widgets.push(Box::new(NodePalette::new()));
         widgets.push(Box::new(Spreadsheet::new()));
         
@@ -2354,6 +3127,8 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             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,
@@ -2365,17 +3140,23 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             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: true,
-            show_cube: false,
-            show_origin: true,
-            viewport_bg_color: [0.05, 0.05, 0.10],
+            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,
             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,
             node_templates,
             current_path,
@@ -2412,13 +3193,13 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             physical_width: pw,
             physical_height: ph,
             scale,
-            square_viewport: false,
-            grid_snap_enabled: true,
-            network_grid_visible: true,
-            grid_size_x: 80.0,
-            grid_size_y: 40.0,
-            skipped_row_h: 20.0,
-            skipped_col_w: 20.0,
+            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,
@@ -2440,6 +3221,13 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             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,
         };
 
         state.sync_nodes();
@@ -2448,10 +3236,12 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
         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.rebuild_positions();
         state.apply_layout();
         state.update_panel_bounds();
+        state.sync_pane_focus();
         state.upload_vertices();
         state
     }
@@ -2613,29 +3403,52 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
         // Unbounded 2D canvas - no clamping
     }
 
+    fn sync_pane_focus(&mut self) {
+        for &menubar_idx in &[LEFT_MENUBAR_IDX, RIGHT_MENUBAR_IDX, PARAM_MENUBAR_IDX, SPREADSHEET_MENUBAR_IDX] {
+            if menubar_idx == self.focused_pane {
+                self.widgets[menubar_idx].focus();
+            } else {
+                self.widgets[menubar_idx].unfocus();
+            }
+        }
+    }
+
     fn sync_layout(&mut self) {
         self.splitter1_x = self.widgets[SPLITTER1_IDX].rect().0;
         self.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::ToggleSquareViewport => self.square_viewport = !self.square_viewport,
             Action::ToggleConfigure => {
                 if self.widgets[CONFIG_DIALOG_IDX].take_click() {
                     self.widgets[CONFIG_DIALOG_IDX].set_visible(false);
@@ -2645,6 +3458,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                     self.widgets[CONFIG_DIALOG_IDX].set_config_toggle(2, self.show_grid);
                     self.widgets[CONFIG_DIALOG_IDX].set_config_toggle(3, self.show_cube);
                     self.widgets[CONFIG_DIALOG_IDX].set_config_toggle(4, self.show_origin);
+                    self.widgets[CONFIG_DIALOG_IDX].set_config_toggle(5, self.show_camera_pivot);
                     self.widgets[CONFIG_DIALOG_IDX].set_config_spin(0, self.grid_size_x);
                     self.widgets[CONFIG_DIALOG_IDX].set_config_spin(1, self.grid_size_y);
                     self.widgets[CONFIG_DIALOG_IDX].set_config_spin(2, self.skipped_row_h);
@@ -2652,6 +3466,9 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                     self.widgets[CONFIG_DIALOG_IDX].set_config_spin(4, self.viewport_bg_color[0]);
                     self.widgets[CONFIG_DIALOG_IDX].set_config_spin(5, self.viewport_bg_color[1]);
                     self.widgets[CONFIG_DIALOG_IDX].set_config_spin(6, self.viewport_bg_color[2]);
+                    self.widgets[CONFIG_DIALOG_IDX].set_config_spin(7, self.origin_size);
+                    self.widgets[CONFIG_DIALOG_IDX].set_config_spin(8, self.grid_thickness);
+                    self.widgets[CONFIG_DIALOG_IDX].set_config_spin(9, self.camera_pivot_size);
                     self.widgets[CONFIG_DIALOG_IDX].set_visible(true);
                 }
                 self.upload_vertices();
@@ -2661,12 +3478,18 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                 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);
+                if !self.show_spreadsheet && self.focused_pane == SPREADSHEET_MENUBAR_IDX {
+                    self.focused_pane = RIGHT_MENUBAR_IDX;
+                }
                 self.rebuild_positions();
                 self.apply_layout();
+                self.sync_pane_focus();
                 self.sync_nodes();
             }
         }
-
+        if settings_changed {
+            self.save_settings();
+        }
     }
 
     fn read_panel_offsets(&mut self) {
@@ -2963,8 +3786,35 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                     && self.cursor_y >= node_area_y
                     && self.cursor_y < self.height - STATUS_H;
 
-                let mut handled = false;
+                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_MENUBAR_IDX].1 {
+                            new_pane = Some(SPREADSHEET_MENUBAR_IDX);
+                        } else {
+                            new_pane = Some(RIGHT_MENUBAR_IDX);
+                        }
+                    } else if self.cursor_x > self.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;
+                if self.widgets[CONFIG_DIALOG_IDX].visible() {
+                    if self.widgets[CONFIG_DIALOG_IDX].mouse_wheel(delta, self.cursor_x, self.cursor_y) {
+                        handled = true;
+                    }
+                } else if !dialog_open {
                     for w in &mut self.widgets {
                         if w.mouse_wheel(delta, self.cursor_x, self.cursor_y) {
                             handled = true;
@@ -2972,7 +3822,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                     }
                 }
 
-                if handled {
+                let result = if handled {
                     if let Some(focused) = self.focused_widget {
                         if self.node_slots.contains(&focused) {
                             if let Some(slot_idx) = self.node_slots.iter().position(|&x| x == focused) {
@@ -3058,41 +3908,110 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                         }
                     }
                 } else if !dialog_open && in_viewport {
-                    match delta {
-                        winit::event::MouseScrollDelta::LineDelta(x, y) => {
-                            let dx = *x * 0.05;
-                            let dy = *y * 0.05;
-                            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;
+                    if self.modifiers.control_key() {
+                        match delta {
+                            winit::event::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);
 
-                            true
+                                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
+                            }
+                            winit::event::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 {
+                                    winit::event::TouchPhase::Started | winit::event::TouchPhase::Moved => true,
+                                    winit::event::TouchPhase::Ended | winit::event::TouchPhase::Cancelled => false,
+                                };
+                                self.last_zoom_time = Instant::now();
+                                self.zoom_accum += dy;
+
+                                true
+                            }
                         }
-                        winit::event::MouseScrollDelta::PixelDelta(pos) => {
-                            let dx = (pos.x as f32 / self.scale as f32) * 0.005;
-                            let dy = (pos.y as f32 / self.scale as f32) * 0.005;
-                            self.rotation_y += dx;
-                            self.rotation_x -= dy;
-
-                            self.is_rotating_viewport = match phase {
-                                winit::event::TouchPhase::Started | winit::event::TouchPhase::Moved => true,
-                                winit::event::TouchPhase::Ended | winit::event::TouchPhase::Cancelled => false,
-                            };
-                            self.last_rotate_time = Instant::now();
-                            self.rotate_accum_yaw += dx;
-                            self.rotate_accum_pitch -= dy;
+                    } else {
+                        match delta {
+                            winit::event::MouseScrollDelta::LineDelta(x, y) => {
+                                self.scroll_lock = 0;
+                                let dx = *x * 0.05;
+                                let dy = *y * 0.05;
+                                self.rotation_y += dx;
+                                self.rotation_x -= dy;
 
-                            true
+                                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
+                            }
+                            winit::event::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 == winit::event::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;
+                                    }
+                                }
+
+                                self.rotation_y += dx;
+                                self.rotation_x -= dy;
+
+                                self.is_rotating_viewport = match phase {
+                                    winit::event::TouchPhase::Started | winit::event::TouchPhase::Moved => true,
+                                    winit::event::TouchPhase::Ended | winit::event::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, .. } => {
@@ -3158,8 +4077,13 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                     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 node_area_y = HEADER_H + MENUBAR_H + BREADCRUMB_H;
                 let dialog_open = self.widgets[CONFIG_DIALOG_IDX].visible() || self.node_palette_visible;
@@ -3216,6 +4140,39 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                     ElementState::Pressed => {
                         let click_target = (0..self.widgets.len()).rev()
                             .find(|&i| self.widgets[i].hit_test(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 >= NODE_SLOT_START && i < NODE_SLOT_START + NODE_SLOT_COUNT) || i == BREADCRUMB_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 {
+                                new_pane = Some(PARAM_MENUBAR_IDX);
+                            } else if i == SPREADSHEET_MENUBAR_IDX || i == SPREADSHEET_IDX {
+                                new_pane = Some(SPREADSHEET_MENUBAR_IDX);
+                            }
+                        } else {
+                            if self.cursor_x < self.splitter1_x {
+                                new_pane = Some(LEFT_MENUBAR_IDX);
+                            } else if self.cursor_x > self.splitter2_x + SPLITTER_W {
+                                new_pane = Some(PARAM_MENUBAR_IDX);
+                            } else {
+                                if self.show_spreadsheet && self.cursor_y >= self.positions[SPREADSHEET_MENUBAR_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();
@@ -3260,24 +4217,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                                         let dir = self.current_dir();
                                         if dir_idx < dir.children.len() && !dir.children[dir_idx].children.is_empty() {
                                             self.current_path.push(dir_idx);
-                                            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();
+                                            self.on_path_changed();
                                             changed = true;
                                         }
                                     }
@@ -3287,6 +4227,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                                 self.last_click = None;
                             }
                         }
+                        self.sync_pane_focus();
                     }
                     ElementState::Released => {
                         if self.drag_widget.is_some() {
@@ -3438,6 +4379,43 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                                                 }
                                             }
                                         }
+                                        "r" | "R" => {
+                                            if self.focused_pane == RIGHT_MENUBAR_IDX {
+                                                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 let Some(focused) = self.focused_widget {
+                                                    if let Some(slot_idx) = self.node_slots.iter().position(|&x| x == focused) {
+                                                        let dir = self.current_dir();
+                                                        if slot_idx < dir.children.len() && !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;
@@ -3515,6 +4493,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
         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;
@@ -3525,6 +4504,17 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             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;
         for w in &mut self.widgets {
             if w.tick(dt) {
@@ -3572,6 +4562,23 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             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();
         }
@@ -3687,6 +4694,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                         camera_pos = Vec3::new(cx, cy, cz);
                     }
                 }
+                camera_pos *= self.viewport_zoom;
                 let view_mat = Mat4::look_at_rh(camera_pos, Vec3::ZERO, Vec3::Y);
                 let model = Mat4::from_rotation_y(self.rotation_y) * Mat4::from_rotation_x(self.rotation_x);
                 let mvp = proj * view_mat * model;
@@ -3695,6 +4703,11 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                 let mvp_grid = proj * view_mat * model;
                 self.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 model_pivot = Mat4::from_rotation_y(self.rotation_y + cam_angle_y);
+                let mvp_pivot = proj * view_mat * model_pivot;
+                self.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 {
@@ -3737,6 +4750,12 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                     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 {
@@ -3785,7 +4804,7 @@ struct App { state: Option<State> }
 
 impl App { fn new() -> Self { Self { state: None } } }
 
-impl ApplicationHandler for App {
+impl ApplicationHandler<CustomEvent> for App {
     fn resumed(&mut self, event_loop: &ActiveEventLoop) {
         if self.state.is_some() { return; }
 
@@ -3839,18 +4858,7 @@ impl ApplicationHandler for App {
                     if let Some(seg) = state.widgets[BREADCRUMB_IDX].path_click() {
                         if seg < state.current_path.len() {
                             state.current_path.truncate(seg);
-                            state.pan_x = 0.0;
-                            state.pan_y = 0.0;
-                            state.pan_velocity_x = 0.0;
-                            state.pan_velocity_y = 0.0;
-                            state.last_frame_pan_x = 0.0;
-                            state.last_frame_pan_y = 0.0;
-                            state.is_scrolling_trackpad = false;
-                            state.scroll_accum_x = 0.0;
-                            state.scroll_accum_y = 0.0;
-                            state.grid_cursor_col = 0;
-                            state.grid_cursor_row = 0;
-                            state.sync_grid_settings();
+                            state.on_path_changed();
                             changed = true;
                         }
                     }
@@ -3942,6 +4950,7 @@ impl ApplicationHandler for App {
                                     0 => Some(Action::ToggleGrid),
                                     1 => Some(Action::ToggleCube),
                                     2 => Some(Action::ToggleOrigin),
+                                    3 => Some(Action::ToggleCameraPivot),
                                     _ => None,
                                 }
                             } else { None };
@@ -3952,6 +4961,7 @@ impl ApplicationHandler for App {
                         }
                     }
 
+                    let mut settings_changed = false;
                     while let Some((id, val)) = state.widgets[CONFIG_DIALOG_IDX].take_config_toggle() {
                         match id {
                             0 => {
@@ -3978,9 +4988,14 @@ impl ApplicationHandler for App {
                                 state.show_origin = val;
                                 state.widgets[RIGHT_MENUBAR_IDX].set_item_checked(2, 2, val);
                             }
+                            5 => {
+                                state.show_camera_pivot = val;
+                                state.widgets[RIGHT_MENUBAR_IDX].set_item_checked(2, 3, val);
+                            }
                             _ => {}
                         }
                         changed = true;
+                        settings_changed = true;
                     }
 
                     while let Some((id, val)) = state.widgets[CONFIG_DIALOG_IDX].take_config_spin() {
@@ -3992,6 +5007,18 @@ impl ApplicationHandler for App {
                             4 => state.viewport_bg_color[0] = val,
                             5 => state.viewport_bg_color[1] = val,
                             6 => state.viewport_bg_color[2] = val,
+                            7 => {
+                                state.origin_size = val;
+                                state.update_origin_geometry();
+                            }
+                            8 => {
+                                state.grid_thickness = val;
+                                state.update_grid_geometry();
+                            }
+                            9 => {
+                                state.camera_pivot_size = val;
+                                state.update_pivot_geometry();
+                            }
                             _ => {}
                         }
                         for &i in &state.node_slots {
@@ -4000,6 +5027,11 @@ impl ApplicationHandler for App {
                         }
                         state.sync_grid_settings();
                         changed = true;
+                        settings_changed = true;
+                    }
+
+                    if settings_changed {
+                        state.save_settings();
                     }
 
                     if changed {
@@ -4060,11 +5092,397 @@ impl ApplicationHandler for App {
             }
         }
     }
+
+    fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: CustomEvent) {
+        let mut needs_redraw = false;
+        match event {
+            CustomEvent::GetState(tx) => {
+                if let Some(state) = &self.state {
+                    let proj = Project {
+                        name: "Project".to_string(),
+                        root: state.fs_root.clone(),
+                        view_state: ProjectViewState {
+                            active_camera: state.active_camera.clone(),
+                            pan: (state.pan_x, state.pan_y),
+                            current_path: state.current_path.clone(),
+                        },
+                    };
+                    let json = serde_json::to_string_pretty(&proj).unwrap_or_default();
+                    let _ = tx.send(json);
+                } else {
+                    let _ = tx.send("null".to_string());
+                }
+            }
+            CustomEvent::PostAction(action, tx) => {
+                if let Some(state) = &mut self.state {
+                    let res = match action {
+                        HttpAction::Up => {
+                            if !state.current_path.is_empty() {
+                                state.current_path.pop();
+                                state.on_path_changed();
+                                needs_redraw = true;
+                                Ok("Moved up".to_string())
+                            } else {
+                                Err("Already at root".to_string())
+                            }
+                        }
+                        HttpAction::Enter { slot } => {
+                            let dir = state.current_dir();
+                            if slot < dir.children.len() && !dir.children[slot].children.is_empty() {
+                                state.current_path.push(slot);
+                                state.on_path_changed();
+                                needs_redraw = true;
+                                Ok("Entered subnet".to_string())
+                            } else {
+                                Err("Not a valid subnet".to_string())
+                            }
+                        }
+                        HttpAction::SetParam { slot, name, value } => {
+                            let dir = state.current_dir_mut();
+                            if let Some(child) = dir.children.get_mut(slot) {
+                                if let Some(p) = child.params.iter_mut().find(|p| p.name == name) {
+                                    p.default = value;
+                                    state.sync_nodes();
+                                    state.rebuild_scene_geometry();
+                                    state.upload_vertices();
+                                    needs_redraw = true;
+                                    Ok("Parameter updated".to_string())
+                                } else {
+                                    Err(format!("Parameter {} not found", name))
+                                }
+                            } else {
+                                Err("Slot index out of bounds".to_string())
+                            }
+                        }
+                        HttpAction::ResetCamera => {
+                            state.rotation_y = 0.0;
+                            state.rotation_x = 0.0;
+                            state.viewport_zoom = 1.0;
+                            state.rotate_velocity_yaw = 0.0;
+                            state.rotate_velocity_pitch = 0.0;
+                            state.zoom_velocity = 0.0;
+                            state.is_rotating_viewport = false;
+                            state.is_zooming_viewport = false;
+                            state.scroll_lock = 0;
+                            needs_redraw = true;
+                            Ok("Camera reset".to_string())
+                        }
+                        HttpAction::Load { path } => {
+                            if let Err(e) = state.load_from_file(Path::new(&path)) {
+                                Err(format!("Load failed: {:?}", e))
+                            } else {
+                                needs_redraw = true;
+                                Ok("Project loaded".to_string())
+                            }
+                        }
+                        HttpAction::Save { path } => {
+                            if let Err(e) = state.save_to_file(Path::new(&path)) {
+                                Err(format!("Save failed: {:?}", e))
+                            } else {
+                                needs_redraw = true;
+                                Ok("Project saved".to_string())
+                            }
+                        }
+                        HttpAction::ToggleGeometry { slot } => {
+                            let active_nodes = state.current_dir().children.len().min(state.node_slots.len());
+                            if slot < active_nodes {
+                                let idx = state.node_slots[slot];
+                                let visible = !state.widgets[idx].geom_visible();
+                                state.widgets[idx].set_geom_visible(visible);
+                                state.current_dir_mut().children[slot].geometry_visible = visible;
+                                state.rebuild_scene_geometry();
+                                needs_redraw = true;
+                                Ok(format!("Geometry visible: {}", visible))
+                            } else {
+                                Err("Slot out of bounds".to_string())
+                            }
+                        }
+                        HttpAction::AddNode { template_name, name, x, y } => {
+                            let child_idx = state.current_dir().children.len();
+                            if child_idx >= state.node_slots.len() {
+                                Err("Max node slots reached".to_string())
+                            } else {
+                                let template_idx = state.node_templates.iter().position(|t| {
+                                    t.label.to_lowercase() == template_name.to_lowercase()
+                                        || t.node.name.to_lowercase() == template_name.to_lowercase()
+                                });
+                                if let Some(idx) = template_idx {
+                                    let mut node = state.node_templates[idx].node.clone();
+                                    node.position = (x, y);
+                                    if let Some(n) = name {
+                                        node.name = n;
+                                    }
+                                    state.current_dir_mut().children.push(node);
+                                    state.left_offsets[child_idx] = (x, y);
+                                    state.sync_nodes();
+                                    state.rebuild_positions();
+                                    state.apply_layout();
+                                    state.update_panel_bounds();
+                                    state.upload_vertices();
+                                    needs_redraw = true;
+                                    Ok("Node added".to_string())
+                                } else {
+                                    Err(format!("Template '{}' not found", template_name))
+                                }
+                            }
+                        }
+                        HttpAction::DeleteNode { slot } => {
+                            let len = state.current_dir().children.len();
+                            if slot < len {
+                                state.current_dir_mut().children.remove(slot);
+                                if let Some(focused) = state.focused_widget {
+                                    if let Some(pos) = state.node_slots.iter().position(|&x| x == focused) {
+                                        if pos >= slot {
+                                            state.widgets[focused].unfocus();
+                                            state.focused_widget = None;
+                                        }
+                                    }
+                                }
+                                state.sync_nodes();
+                                state.rebuild_positions();
+                                state.apply_layout();
+                                state.update_panel_bounds();
+                                state.upload_vertices();
+                                needs_redraw = true;
+                                Ok("Node deleted".to_string())
+                            } else {
+                                Err("Slot out of bounds".to_string())
+                            }
+                        }
+                        HttpAction::RenameNode { slot, new_name } => {
+                            let len = state.current_dir().children.len();
+                            if slot < len {
+                                state.current_dir_mut().children[slot].name = new_name;
+                                state.sync_nodes();
+                                needs_redraw = true;
+                                Ok("Node renamed".to_string())
+                            } else {
+                                Err("Slot out of bounds".to_string())
+                            }
+                        }
+                        HttpAction::MoveNode { slot, x, y } => {
+                            let len = state.current_dir().children.len();
+                            if slot < len {
+                                state.current_dir_mut().children[slot].position = (x, y);
+                                if slot < state.left_offsets.len() {
+                                    state.left_offsets[slot] = (x, y);
+                                }
+                                state.sync_nodes();
+                                state.rebuild_positions();
+                                state.apply_layout();
+                                state.update_panel_bounds();
+                                state.upload_vertices();
+                                needs_redraw = true;
+                                Ok("Node moved".to_string())
+                            } else {
+                                Err("Slot out of bounds".to_string())
+                            }
+                        }
+                        HttpAction::AddParam { slot, name, param_type, default } => {
+                            let len = state.current_dir().children.len();
+                            if slot < len {
+                                let param = ParamDef {
+                                    name,
+                                    label: String::new(),
+                                    param_type,
+                                    default,
+                                    options: vec![],
+                                    min: None,
+                                    max: None,
+                                };
+                                state.current_dir_mut().children[slot].params.push(param);
+                                state.sync_nodes();
+                                needs_redraw = true;
+                                Ok("Parameter added".to_string())
+                            } else {
+                                Err("Slot out of bounds".to_string())
+                            }
+                        }
+                        HttpAction::DeleteParam { slot, name } => {
+                            let len = state.current_dir().children.len();
+                            if slot < len {
+                                let params = &mut state.current_dir_mut().children[slot].params;
+                                if let Some(pos) = params.iter().position(|p| p.name == name) {
+                                    params.remove(pos);
+                                    state.sync_nodes();
+                                    needs_redraw = true;
+                                    Ok("Parameter deleted".to_string())
+                                } else {
+                                    Err(format!("Parameter '{}' not found", name))
+                                }
+                            } else {
+                                Err("Slot out of bounds".to_string())
+                            }
+                        }
+                    };
+                    let _ = tx.send(res);
+                } else {
+                    let _ = tx.send(Err("State not initialized".to_string()));
+                }
+            }
+        }
+        if needs_redraw {
+            if let Some(state) = &mut self.state {
+                state.window.request_redraw();
+            }
+        }
+    }
 }
 
 fn main() {
-    let event_loop = EventLoop::new().unwrap();
+    let event_loop = EventLoop::<CustomEvent>::with_user_event().build().unwrap();
     event_loop.set_control_flow(ControlFlow::Poll);
+
+    let proxy = event_loop.create_proxy();
+    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 proxy = proxy.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 proxy.send_event(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 {
+                        let body = "{\"error\":\"failed to send event 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 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_bytes = vec![0; content_length];
+                    if reader.read_exact(&mut body_bytes).is_ok() {
+                        if let Ok(body_str) = String::from_utf8(body_bytes) {
+                            if let Ok(action) = serde_json::from_str::<HttpAction>(&body_str) {
+                                let (tx, rx) = std::sync::mpsc::channel();
+                                if proxy.send_event(CustomEvent::PostAction(action, tx)).is_ok() {
+                                    match rx.recv() {
+                                        Ok(Ok(msg)) => {
+                                            let body = format!("{{\"status\":\"success\",\"message\":\"{}\"}}", msg);
+                                            let response = format!(
+                                                "HTTP/1.1 200 OK\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());
+                                        }
+                                        Ok(Err(err)) => {
+                                            let body = format!("{{\"status\":\"error\",\"error\":\"{}\"}}", err);
+                                            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());
+                                        }
+                                        Err(_) => {
+                                            let body = "{\"status\":\"error\",\"error\":\"internal receiver error\"}";
+                                            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 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\":\"body is not valid UTF-8\"}";
+                            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();
+            });
+        }
+    });
+
     let mut app = App::new();
     event_loop.run_app(&mut app).unwrap();
 }
@@ -4204,4 +5622,65 @@ mod tests {
         assert_eq!(rows[1][11], "0.3000"); // UV.x
         assert_eq!(rows[1][12], "0.4000"); // UV.y
     }
+
+    #[test]
+    fn test_line_geometry_generation() {
+        let start = Vec3::new(0.0, 0.0, 0.0);
+        let end = Vec3::new(0.0, 1.0, 0.0);
+        let geom = line_vertices(start, end, 0.02);
+        
+        // A box line should contain 36 vertices (6 faces * 2 triangles * 3 vertices)
+        assert_eq!(geom.vertices.len(), 36);
+        
+        // Every vertex should have "Norm" and "UV" attributes
+        for v in &geom.vertices {
+            assert!(v.attributes.contains_key("Norm"));
+            assert!(v.attributes.contains_key("UV"));
+        }
+    }
+
+    #[test]
+    fn test_design_settings_serialization_roundtrip() {
+        let json_without_pivot = r#"
+        {
+            "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
+        }
+        "#;
+        
+        let settings: DesignSettings = serde_json::from_str(json_without_pivot).unwrap();
+        assert_eq!(settings.show_camera_pivot_enabled, false);
+        assert_eq!(settings.camera_pivot_size, 1.0);
+        
+        let serialized = serde_json::to_string(&settings).unwrap();
+        let settings_roundtrip: DesignSettings = serde_json::from_str(&serialized).unwrap();
+        assert_eq!(settings_roundtrip.show_camera_pivot_enabled, false);
+        assert_eq!(settings_roundtrip.camera_pivot_size, 1.0);
+    }
+
+    #[test]
+    fn test_http_action_parsing() {
+        let json_str = "{\"action\": \"add_node\", \"template_name\": \"Sphere\", \"name\": \"MySphere\", \"x\": 5.0, \"y\": 3.0}";
+        let action: HttpAction = serde_json::from_str(json_str).unwrap();
+        match action {
+            HttpAction::AddNode { template_name, name, x, y } => {
+                assert_eq!(template_name, "Sphere");
+                assert_eq!(name, Some("MySphere".to_string()));
+                assert_eq!(x, 5.0);
+                assert_eq!(y, 3.0);
+            }
+            _ => panic!("Expected AddNode"),
+        }
+    }
 }