graphic design tool
git clone https://git.lucas.co/cce-designer.git
Update panning API usage and synchronize design-interface
Cargo.lock | 1 +
default_project.json | 111 +-
nodes/add.json | 14 +
nodes/camera.json | 6 +-
nodes/transform.json | 8 +
src/geometry.rs | 340 ++++-
src/main.rs | 3630 +++++++++++++++++++++++-----------------------
src/shader_textured.wgsl | 41 +
8 files changed, 2324 insertions(+), 1827 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 2f3c713..be18632 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -214,6 +214,7 @@ dependencies = [
"raw-window-handle",
"resvg",
"serde",
+ "serde_json",
"smithay-client-toolkit",
"tokio",
"wayland-backend",
diff --git a/default_project.json b/default_project.json
index 6456a0d..4e09934 100644
--- a/default_project.json
+++ b/default_project.json
@@ -1,35 +1,116 @@
{
"name": "Default Project",
- "view_state": {
- "active_camera": "Camera 1",
- "pan": [0.0, 0.0],
- "current_path": []
- },
"root": {
"name": "root",
"type": "node",
- "position": [0.0, 0.0],
"children": [
{
"name": "Camera 1",
"type": "camera",
- "position": [1.0, 1.0],
+ "children": [],
"params": [
- { "name": "X", "type": "slider", "default": "2.5", "min": -10.0, "max": 10.0 },
- { "name": "Y", "type": "slider", "default": "1.8", "min": -10.0, "max": 10.0 },
- { "name": "Z", "type": "slider", "default": "2.5", "min": -10.0, "max": 10.0 }
+ {
+ "name": "Position",
+ "label": "",
+ "type": "float3",
+ "default": "2.50:1.80:2.50",
+ "options": [],
+ "min": -10.0,
+ "max": 10.0,
+ "step": null
+ },
+ {
+ "name": "Rotation",
+ "label": "",
+ "type": "float3",
+ "default": "23.62:-58.83:0.00",
+ "options": [],
+ "min": -180.0,
+ "max": 180.0,
+ "step": null
+ },
+ {
+ "name": "Pivot",
+ "label": "",
+ "type": "float3",
+ "default": "0.00:0.00:0.00",
+ "options": [],
+ "min": -10.0,
+ "max": 10.0,
+ "step": null
+ }
],
- "children": []
+ "position": [
+ 1.0,
+ 1.0
+ ]
},
{
"name": "Sphere 1",
"type": "sphere",
- "position": [4.0, 2.0],
+ "children": [],
+ "params": [
+ {
+ "name": "Radius",
+ "label": "",
+ "type": "slider",
+ "default": "0.5",
+ "options": [],
+ "min": null,
+ "max": null,
+ "step": null
+ }
+ ],
+ "position": [
+ 4.0,
+ 2.0
+ ]
+ },
+ {
+ "name": "Transform 1",
+ "type": "transform",
+ "children": [],
"params": [
- { "name": "Radius", "type": "slider", "default": "0.5" }
+ {
+ "name": "Input",
+ "label": "",
+ "type": "text",
+ "default": "Sphere 1",
+ "options": [],
+ "min": null,
+ "max": null,
+ "step": null
+ },
+ {
+ "name": "Translation",
+ "label": "",
+ "type": "float3",
+ "default": "1.00:0.50:0.00",
+ "options": [],
+ "min": -10.0,
+ "max": 10.0,
+ "step": null
+ }
],
- "children": []
+ "position": [
+ 4.0,
+ 3.0
+ ]
}
+ ],
+ "params": [],
+ "position": [
+ 0.0,
+ 0.0
]
+ },
+ "view_state": {
+ "active_camera": "Camera 1",
+ "pan": [
+ 0.0,
+ 122.9747
+ ],
+ "current_path": [],
+ "selected_node": null
}
-}
+}
\ No newline at end of file
diff --git a/nodes/add.json b/nodes/add.json
new file mode 100644
index 0000000..a1995a8
--- /dev/null
+++ b/nodes/add.json
@@ -0,0 +1,14 @@
+{
+ "name": "Add 1",
+ "type": "add",
+ "params": [
+ {
+ "name": "Points",
+ "default": "100",
+ "type": "spinbox",
+ "min": 1.0,
+ "max": 1000.0,
+ "step": 1.0
+ }
+ ]
+}
diff --git a/nodes/camera.json b/nodes/camera.json
index 6456cc5..59b3324 100644
--- a/nodes/camera.json
+++ b/nodes/camera.json
@@ -2,8 +2,8 @@
"name": "Camera 1",
"type": "camera",
"params": [
- { "name": "X", "type": "slider", "default": "2.5", "min": -10.0, "max": 10.0 },
- { "name": "Y", "type": "slider", "default": "1.8", "min": -10.0, "max": 10.0 },
- { "name": "Z", "type": "slider", "default": "2.5", "min": -10.0, "max": 10.0 }
+ { "name": "Position", "type": "float3", "default": "2.50:1.80:2.50", "min": -10.0, "max": 10.0 },
+ { "name": "Rotation", "type": "float3", "default": "0.00:0.00:0.00", "min": -180.0, "max": 180.0 },
+ { "name": "Pivot", "type": "float3", "default": "0.00:0.00:0.00", "min": -10.0, "max": 10.0 }
]
}
diff --git a/nodes/transform.json b/nodes/transform.json
new file mode 100644
index 0000000..d2cec77
--- /dev/null
+++ b/nodes/transform.json
@@ -0,0 +1,8 @@
+{
+ "name": "Transform 1",
+ "type": "transform",
+ "params": [
+ { "name": "Input", "default": "Sphere 1", "type": "text" },
+ { "name": "Translation", "default": "0.00:0.00:0.00", "type": "float3", "min": -10.0, "max": 10.0 }
+ ]
+}
diff --git a/src/geometry.rs b/src/geometry.rs
index 62a6cb6..2205fb3 100644
--- a/src/geometry.rs
+++ b/src/geometry.rs
@@ -1,4 +1,4 @@
-use super::FsNode;
+use super::{FsNode, ParamDef};
use std::collections::HashMap;
use opencl3::platform::get_platforms;
use opencl3::device::{Device, CL_DEVICE_TYPE_GPU, CL_DEVICE_TYPE_CPU};
@@ -211,6 +211,110 @@ pub fn node_param_f32(node: &FsNode, name: &str, fallback: f32) -> f32 {
.unwrap_or(fallback)
}
+pub fn node_param_str(node: &FsNode, name: &str, fallback: &str) -> String {
+ node.params.iter()
+ .find(|p| p.name.eq_ignore_ascii_case(name))
+ .map(|p| p.default.clone())
+ .unwrap_or_else(|| fallback.to_string())
+}
+
+pub fn node_param_vec3(node: &FsNode, name: &str, fallback: Vec3) -> Vec3 {
+ node.params.iter()
+ .find(|p| p.name.eq_ignore_ascii_case(name))
+ .and_then(|p| {
+ let parts: Vec<&str> = p.default.split(':').collect();
+ if parts.len() == 3 {
+ let x = parts[0].parse::<f32>().ok()?;
+ let y = parts[1].parse::<f32>().ok()?;
+ let z = parts[2].parse::<f32>().ok()?;
+ Some(Vec3::new(x, y, z))
+ } else {
+ None
+ }
+ })
+ .unwrap_or(fallback)
+}
+
+pub fn find_node_by_name<'a>(root: &'a FsNode, name: &str) -> Option<&'a FsNode> {
+ fn visit<'a>(node: &'a FsNode, name: &str) -> Option<&'a FsNode> {
+ if node.name == name {
+ return Some(node);
+ }
+ for child in &node.children {
+ if let Some(res) = visit(child, name) {
+ return Some(res);
+ }
+ }
+ None
+ }
+ for child in &root.children {
+ if let Some(res) = visit(child, name) {
+ return Some(res);
+ }
+ }
+ None
+}
+
+pub fn generate_single_node_geometry(root: &FsNode, target: &FsNode, visited: &mut Vec<String>) -> Option<Geometry> {
+ if visited.contains(&target.name) {
+ return None;
+ }
+ visited.push(target.name.clone());
+
+ let res = if target.node_type.eq_ignore_ascii_case("sphere") {
+ let idx = find_sphere_index(root, target)?;
+ let center = Vec3::new((idx % 4) as f32 * 1.25 - 1.875, 0.55, -((idx / 4) as f32) * 1.25);
+ Some(sphere_vertices(center, node_param_f32(target, "Radius", 0.5).max(0.05)))
+ } else if target.node_type.eq_ignore_ascii_case("line") {
+ let idx = find_sphere_index(root, target)?;
+ 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(target, "Length", 1.0);
+ let thickness = node_param_f32(target, "Thickness", 0.02);
+ let end = start + Vec3::new(0.0, length, 0.0);
+ Some(line_vertices(start, end, thickness))
+ } else if target.node_type.eq_ignore_ascii_case("add") {
+ let idx = find_sphere_index(root, target)?;
+ let center = Vec3::new((idx % 4) as f32 * 1.25 - 1.875, 0.55, -((idx / 4) as f32) * 1.25);
+ let num_points = node_param_f32(target, "Points", 100.0) as i32;
+ let mut geom = Geometry::new();
+ for i in 0..num_points {
+ let t = i as f32 / num_points.max(1) as f32;
+ let angle = t * std::f32::consts::TAU * 3.0;
+ let r = 0.4 * t;
+ let px = center.x + r * angle.cos();
+ let py = center.y + t * 0.5 - 0.25;
+ let pz = center.z + r * angle.sin();
+ let pt_center = Vec3::new(px, py, pz);
+ geom.merge(sphere_vertices(pt_center, 0.02));
+ }
+ Some(geom)
+ } else if target.node_type.eq_ignore_ascii_case("transform") {
+ resolve_transform_geometry(root, target, visited)
+ } else {
+ None
+ };
+
+ visited.pop();
+ res
+}
+
+pub fn resolve_transform_geometry(root: &FsNode, target: &FsNode, visited: &mut Vec<String>) -> Option<Geometry> {
+ let input_name = node_param_str(target, "Input", "");
+ if input_name.is_empty() {
+ return None;
+ }
+ let input_node = find_node_by_name(root, &input_name)?;
+ let mut geom = generate_single_node_geometry(root, input_node, visited)?;
+ let translation = node_param_vec3(target, "Translation", Vec3::ZERO);
+ for v in &mut geom.vertices {
+ v.pos[0] += translation.x;
+ v.pos[1] += translation.y;
+ v.pos[2] += translation.z;
+ }
+ Some(geom)
+}
+
+
pub fn run_opencl_kernel(code: &str, geom: &mut Geometry) -> Result<(), String> {
let is_generator = code.contains("out_count");
if geom.vertices.is_empty() && !is_generator {
@@ -427,7 +531,7 @@ pub fn run_opencl_kernel(code: &str, geom: &mut Geometry) -> Result<(), String>
}
pub fn network_sphere_vertices(root: &FsNode) -> Geometry {
- fn visit(node: &FsNode, count: &mut usize, out: &mut Geometry) {
+ fn visit(root: &FsNode, node: &FsNode, count: &mut usize, out: &mut Geometry) {
if node.node_type.eq_ignore_ascii_case("sphere") {
let idx = *count;
*count += 1;
@@ -445,16 +549,42 @@ pub fn network_sphere_vertices(root: &FsNode) -> Geometry {
let end = start + Vec3::new(0.0, length, 0.0);
out.merge(line_vertices(start, end, thickness));
}
+ } else if node.node_type.eq_ignore_ascii_case("add") {
+ let idx = *count;
+ *count += 1;
+ if node.geometry_visible {
+ let center = Vec3::new((idx % 4) as f32 * 1.25 - 1.875, 0.55, -((idx / 4) as f32) * 1.25);
+ let num_points = node_param_f32(node, "Points", 100.0) as i32;
+ for i in 0..num_points {
+ let t = i as f32 / num_points.max(1) as f32;
+ let angle = t * std::f32::consts::TAU * 3.0;
+ let r = 0.4 * t;
+ let px = center.x + r * angle.cos();
+ let py = center.y + t * 0.5 - 0.25;
+ let pz = center.z + r * angle.sin();
+ let pt_center = Vec3::new(px, py, pz);
+ out.merge(sphere_vertices(pt_center, 0.02));
+ }
+ }
+ } else if node.node_type.eq_ignore_ascii_case("transform") {
+ let idx = *count;
+ *count += 1;
+ if node.geometry_visible {
+ let mut visited = Vec::new();
+ if let Some(geom) = resolve_transform_geometry(root, node, &mut visited) {
+ out.merge(geom);
+ }
+ }
}
for child in &node.children {
- visit(child, count, out);
+ visit(root, child, count, out);
}
}
let mut out = Geometry::new();
let mut count = 0;
for child in &root.children {
- visit(child, &mut count, &mut out);
+ visit(root, child, &mut count, &mut out);
}
out
}
@@ -462,7 +592,10 @@ pub fn network_sphere_vertices(root: &FsNode) -> Geometry {
pub 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") || node.node_type.eq_ignore_ascii_case("line") {
+ if node.node_type.eq_ignore_ascii_case("sphere")
+ || node.node_type.eq_ignore_ascii_case("line")
+ || node.node_type.eq_ignore_ascii_case("add")
+ || node.node_type.eq_ignore_ascii_case("transform") {
let idx = *count;
*count += 1;
if is_target {
@@ -658,8 +791,7 @@ pub fn camera_pivot_vertices(scale: f32) -> Vec<Vertex3D> {
verts
}
-pub fn grid_vertices(thickness: f32) -> Vec<Vertex3D> {
- let color = [0.35, 0.35, 0.40];
+pub fn grid_vertices(thickness: f32, color: [f32; 3]) -> Vec<Vertex3D> {
let range = 4.0;
let step = 1.0;
let mut geom = Geometry::new();
@@ -783,5 +915,199 @@ mod tests {
assert_eq!(geom.vertices[1].pos, [2.0, 4.0, 6.0]);
assert_eq!(geom.vertices[1].col, [0.5, 0.5, 0.5]);
}
+
+ #[test]
+ fn test_add_node_points() {
+ let add_node = FsNode {
+ name: "Add points test".to_string(),
+ node_type: "add".to_string(),
+ children: vec![],
+ params: vec![
+ ParamDef {
+ name: "Points".to_string(),
+ label: String::new(),
+ param_type: "spinbox".to_string(),
+ default: "5".to_string(),
+ options: vec![],
+ min: Some(1.0),
+ max: Some(10.0),
+ step: Some(1.0),
+ }
+ ],
+ geometry_visible: true,
+ position: (0.0, 0.0),
+ };
+ let root = FsNode {
+ name: "root".to_string(),
+ node_type: "node".to_string(),
+ children: vec![add_node],
+ params: vec![],
+ geometry_visible: true,
+ position: (0.0, 0.0),
+ };
+ let geom = network_sphere_vertices(&root);
+ assert_eq!(geom.vertices.len(), 5 * 2304);
+ }
+
+ #[test]
+ fn test_transform_node() {
+ let sphere = FsNode {
+ name: "Sphere 1".to_string(),
+ node_type: "sphere".to_string(),
+ children: vec![],
+ params: vec![
+ ParamDef {
+ name: "Radius".to_string(),
+ label: String::new(),
+ param_type: "slider".to_string(),
+ default: "0.5".to_string(),
+ options: vec![],
+ min: None,
+ max: None,
+ step: None,
+ }
+ ],
+ geometry_visible: true,
+ position: (0.0, 0.0),
+ };
+ let transform1 = FsNode {
+ name: "Transform 1".to_string(),
+ node_type: "transform".to_string(),
+ children: vec![],
+ params: vec![
+ ParamDef {
+ name: "Input".to_string(),
+ label: String::new(),
+ param_type: "text".to_string(),
+ default: "Sphere 1".to_string(),
+ options: vec![],
+ min: None,
+ max: None,
+ step: None,
+ },
+ ParamDef {
+ name: "Translation".to_string(),
+ label: String::new(),
+ param_type: "float3".to_string(),
+ default: "1.00:2.00:3.00".to_string(),
+ options: vec![],
+ min: None,
+ max: None,
+ step: None,
+ }
+ ],
+ geometry_visible: true,
+ position: (0.0, 0.0),
+ };
+ let root = FsNode {
+ name: "root".to_string(),
+ node_type: "node".to_string(),
+ children: vec![sphere.clone(), transform1.clone()],
+ params: vec![],
+ geometry_visible: true,
+ position: (0.0, 0.0),
+ };
+
+ // Test normal transform
+ let mut visited = Vec::new();
+ let geom1 = resolve_transform_geometry(&root, &transform1, &mut visited).unwrap();
+ assert!(!geom1.vertices.is_empty());
+ let avg_x = geom1.vertices.iter().map(|v| v.pos[0]).sum::<f32>() / geom1.vertices.len() as f32;
+ let avg_y = geom1.vertices.iter().map(|v| v.pos[1]).sum::<f32>() / geom1.vertices.len() as f32;
+ let avg_z = geom1.vertices.iter().map(|v| v.pos[2]).sum::<f32>() / geom1.vertices.len() as f32;
+ assert!((avg_x - -0.875).abs() < 0.01);
+ assert!((avg_y - 2.55).abs() < 0.01);
+ assert!((avg_z - 3.0).abs() < 0.01);
+
+ // Test chained transform
+ let transform2 = FsNode {
+ name: "Transform 2".to_string(),
+ node_type: "transform".to_string(),
+ children: vec![],
+ params: vec![
+ ParamDef {
+ name: "Input".to_string(),
+ label: String::new(),
+ param_type: "text".to_string(),
+ default: "Transform 1".to_string(),
+ options: vec![],
+ min: None,
+ max: None,
+ step: None,
+ },
+ ParamDef {
+ name: "Translation".to_string(),
+ label: String::new(),
+ param_type: "float3".to_string(),
+ default: "-1.00:-1.00:-1.00".to_string(),
+ options: vec![],
+ min: None,
+ max: None,
+ step: None,
+ }
+ ],
+ geometry_visible: true,
+ position: (0.0, 0.0),
+ };
+ let root_chained = FsNode {
+ name: "root".to_string(),
+ node_type: "node".to_string(),
+ children: vec![sphere, transform1, transform2.clone()],
+ params: vec![],
+ geometry_visible: true,
+ position: (0.0, 0.0),
+ };
+ let mut visited = Vec::new();
+ let geom2 = resolve_transform_geometry(&root_chained, &transform2, &mut visited).unwrap();
+ let avg_chained_x = geom2.vertices.iter().map(|v| v.pos[0]).sum::<f32>() / geom2.vertices.len() as f32;
+ let avg_chained_y = geom2.vertices.iter().map(|v| v.pos[1]).sum::<f32>() / geom2.vertices.len() as f32;
+ let avg_chained_z = geom2.vertices.iter().map(|v| v.pos[2]).sum::<f32>() / geom2.vertices.len() as f32;
+ assert!((avg_chained_x - -1.875).abs() < 0.01);
+ assert!((avg_chained_y - 1.55).abs() < 0.01);
+ assert!((avg_chained_z - 2.0).abs() < 0.01);
+
+ // Test loop detection
+ let transform_loop = FsNode {
+ name: "Transform Loop".to_string(),
+ node_type: "transform".to_string(),
+ children: vec![],
+ params: vec![
+ ParamDef {
+ name: "Input".to_string(),
+ label: String::new(),
+ param_type: "text".to_string(),
+ default: "Transform Loop".to_string(),
+ options: vec![],
+ min: None,
+ max: None,
+ step: None,
+ },
+ ParamDef {
+ name: "Translation".to_string(),
+ label: String::new(),
+ param_type: "float3".to_string(),
+ default: "1.00:1.00:1.00".to_string(),
+ options: vec![],
+ min: None,
+ max: None,
+ step: None,
+ }
+ ],
+ geometry_visible: true,
+ position: (0.0, 0.0),
+ };
+ let root_loop = FsNode {
+ name: "root".to_string(),
+ node_type: "node".to_string(),
+ children: vec![transform_loop.clone()],
+ params: vec![],
+ geometry_visible: true,
+ position: (0.0, 0.0),
+ };
+ let mut visited = Vec::new();
+ let geom_loop = resolve_transform_geometry(&root_loop, &transform_loop, &mut visited);
+ assert!(geom_loop.is_none());
+ }
}
+
diff --git a/src/main.rs b/src/main.rs
index 81008ef..b3eda60 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -79,9 +79,8 @@ enum WindowEvent {
use wgpu::util::DeviceExt;
-use clear_ui::widget::{Breadcrumb, Canvas, ColorSelector, ContentBg, MenuBar, Node, ParametersBg, Spinbox, Splitter, Spreadsheet, StatusBar, TextLabel, Toggle, ViewportBg, Widget, GraphNode, Graph};
+use clear_ui::widget::{Breadcrumb, Canvas, MenuBar, ParametersBg, Splitter, Spreadsheet, StatusBar, TextLabel, ViewportBg, Widget, GraphNode, Graph};
use clear_ui::colors;
-use clear_ui::layout::{RenderTarget, Section};
use glyphon::{
Attrs, Buffer, Cache, FontSystem, Metrics, Resolution, SwashCache, TextArea, TextAtlas,
@@ -102,10 +101,9 @@ const RIGHT_MENUBAR_IDX: usize = 8;
const PARAM_MENUBAR_IDX: usize = 9;
const STATUS_IDX: usize = 10;
const BREADCRUMB_IDX: usize = 11;
-const CONFIG_DIALOG_IDX: usize = 12;
-const NODE_PALETTE_IDX: usize = 13;
-const SPREADSHEET_IDX: usize = 14;
-const SPREADSHEET_MENUBAR_IDX: usize = 15;
+const NODE_PALETTE_IDX: usize = 12;
+const SPREADSHEET_IDX: usize = 13;
+const SPREADSHEET_MENUBAR_IDX: usize = 14;
const HEADER_H: f32 = 26.0;
@@ -132,6 +130,8 @@ struct ParamDef {
min: Option<f32>,
#[serde(default)]
max: Option<f32>,
+ #[serde(default)]
+ step: Option<f32>,
}
fn default_param_type() -> String { "string".to_string() }
@@ -165,6 +165,8 @@ struct ProjectViewState {
pan: (f32, f32),
#[serde(default)]
current_path: Vec<usize>,
+ #[serde(default)]
+ selected_node: Option<usize>,
}
fn default_camera() -> String {
@@ -196,6 +198,8 @@ enum HttpAction {
AddParam { slot: usize, name: String, param_type: String, default: String },
DeleteParam { slot: usize, name: String },
ToggleCircularPane,
+ MenuClick { widget_idx: usize, menu_idx: usize, item_idx: usize },
+ MenuClosed { widget_idx: usize, menu_idx: usize },
}
#[derive(Debug)]
@@ -222,6 +226,15 @@ fn param_display(params: &[ParamDef]) -> Vec<(String, String, String)> {
let min = p.min.unwrap_or(0.0);
let max = p.max.unwrap_or(2.0);
format!("slider:{}:{}", min, max)
+ } else if p.param_type == "float3" {
+ let min = p.min.unwrap_or(-10.0);
+ let max = p.max.unwrap_or(10.0);
+ format!("float3:{}:{}", min, max)
+ } else if p.param_type == "spinbox" {
+ let min = p.min.unwrap_or(1.0) as i32;
+ let max = p.max.unwrap_or(10000.0) as i32;
+ let step = p.step.unwrap_or(1.0) as i32;
+ format!("spinbox:{}:{}:{}", min, max, step)
} else {
p.param_type.clone()
};
@@ -283,6 +296,11 @@ fn default_grid_thickness() -> f32 { 0.03 }
fn default_show_camera_pivot() -> bool { false }
fn default_camera_pivot_size() -> f32 { 1.0 }
fn default_node_color() -> [f32; 3] { [0.10, 0.45, 0.70] }
+fn default_grid_color() -> [f32; 3] { [0.35, 0.35, 0.40] }
+fn default_uniform_background() -> bool { false }
+fn default_network_opacity() -> f32 { 0.95 }
+fn default_cell_color() -> [f32; 3] { [0.13, 0.13, 0.16] }
+fn default_gap_color() -> [f32; 3] { [0.07, 0.07, 0.09] }
#[derive(Serialize, Deserialize, Clone, Debug)]
struct DesignSettings {
@@ -306,6 +324,16 @@ struct DesignSettings {
camera_pivot_size: f32,
#[serde(default = "default_node_color")]
node_color: [f32; 3],
+ #[serde(default = "default_grid_color")]
+ grid_color: [f32; 3],
+ #[serde(default = "default_uniform_background")]
+ uniform_background: bool,
+ #[serde(default = "default_network_opacity")]
+ network_opacity: f32,
+ #[serde(default = "default_cell_color")]
+ cell_color: [f32; 3],
+ #[serde(default = "default_gap_color")]
+ gap_color: [f32; 3],
}
impl Default for DesignSettings {
@@ -327,6 +355,11 @@ impl Default for DesignSettings {
show_camera_pivot_enabled: false,
camera_pivot_size: 1.0,
node_color: default_node_color(),
+ grid_color: default_grid_color(),
+ uniform_background: false,
+ network_opacity: 0.95,
+ cell_color: default_cell_color(),
+ gap_color: default_gap_color(),
}
}
}
@@ -362,1112 +395,6 @@ impl DesignSettings {
}
}
-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,
- visible: bool,
- close_hovered: bool,
- panel_w: f32,
- panel_h: f32,
- active_page: usize,
- hovered_tab: Option<usize>,
- toggle_grid_snap: Toggle,
- toggle_network_grid: Toggle,
- grid_size_x: f32,
- grid_size_y: f32,
- skipped_row_h: f32,
- skipped_col_w: f32,
- 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],
- node_color_selector: ColorSelector,
- last_sent_node_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(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 nr = (settings.node_color[0] * 255.0).round().clamp(0.0, 255.0) as u8;
- let ng = (settings.node_color[1] * 255.0).round().clamp(0.0, 255.0) as u8;
- let nb = (settings.node_color[2] * 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,
- 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],
- node_color_selector: ColorSelector::new([nr, ng, nb]).with_label("Node Base Color"),
- last_sent_node_color: [nr, ng, nb],
- 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) {
- let px = self.x + (self.w - self.panel_w) / 2.0;
- let py = self.y + (self.h - self.panel_h) / 2.0;
- (px, py, self.panel_w, self.panel_h)
- }
-
- fn close_rect(&self, px: f32, pw: f32, py: f32) -> (f32, f32, f32, f32) {
- (px + pw - 28.0, py + 8.0, 20.0, 20.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;
- let general_w = "General".len() as f32 * 7.5 + 16.0;
- let network_w = "Network".len() as f32 * 7.5 + 16.0;
- let viewport_w = "Viewport".len() as f32 * 7.5 + 16.0;
- let bindings_w = "Bindings".len() as f32 * 7.5 + 16.0;
- let gap = 4.0;
- [
- (px + 16.0, tab_y, general_w, tab_h),
- (px + 16.0 + general_w + gap, tab_y, network_w, tab_h),
- (px + 16.0 + general_w + gap + network_w + gap, tab_y, viewport_w, tab_h),
- (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);
-
- // Section 3: Node Styling Section (aligned vertically under sec2)
- collector.active = true;
- let mut sec3 = Section::new(&mut collector, px + 20.0, y2 + 12.0, 760.0, "Node Styling");
- collector.active = false;
- sec3.widget(&mut collector, &mut self.node_color_selector, 14.0, 240.0, 24.0);
- sec3.spacing(8.0);
- collector.active = true;
- let y3 = sec3.finish(&mut collector);
-
- let total_h = y3 - (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);
- shift_widget(&mut self.node_color_selector);
- } 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;
- 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;
- 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.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 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
- }
- }
-
- fn set_config_spin(&mut self, id: usize, val: f32) {
- match id {
- 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;
- self.last_sent_color[0] = self.color_selector.color[0];
- }
- 5 => {
- self.viewport_g = val;
- self.color_selector.color[1] = (val * 255.0).round().clamp(0.0, 255.0) as u8;
- self.last_sent_color[1] = self.color_selector.color[1];
- }
- 6 => {
- self.viewport_b = val;
- 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;
- }
- 10 => {
- self.node_color_selector.color[0] = (val * 255.0).round().clamp(0.0, 255.0) as u8;
- self.last_sent_node_color[0] = self.node_color_selector.color[0];
- }
- 11 => {
- self.node_color_selector.color[1] = (val * 255.0).round().clamp(0.0, 255.0) as u8;
- self.last_sent_node_color[1] = self.node_color_selector.color[1];
- }
- 12 => {
- self.node_color_selector.color[2] = (val * 255.0).round().clamp(0.0, 255.0) as u8;
- self.last_sent_node_color[2] = self.node_color_selector.color[2];
- }
- _ => {}
- }
- }
- fn take_config_spin(&mut self) -> Option<(usize, f32)> {
- 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] {
- let val = self.color_selector.color[i] as f32 / 255.0;
- self.last_sent_color[i] = self.color_selector.color[i];
- match i {
- 0 => self.viewport_r = val,
- 1 => self.viewport_g = val,
- 2 => self.viewport_b = val,
- _ => {}
- }
- return Some((4 + i, val));
- }
- }
- for i in 0..3 {
- if self.node_color_selector.color[i] != self.last_sent_node_color[i] {
- let val = self.node_color_selector.color[i] as f32 / 255.0;
- self.last_sent_node_color[i] = self.node_color_selector.color[i];
- return Some((10 + i, val));
- }
- }
- None
- }
-
- fn hit_test(&self, px: f32, py: f32) -> bool {
- if !self.visible { return false; }
- px >= self.x && px <= self.x + self.w && py >= self.y && py <= self.y + self.h
- }
-
- fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
- let was = self.hovered;
- self.hovered = self.hit_test(px, py);
- let (ppx, ppy, pw, _ph) = self.panel_rect();
- let old_close = self.close_hovered;
- let (cx, cy, cw, ch) = self.close_rect(ppx, pw, ppy);
- self.close_hovered = self.visible && px >= cx && px < cx + cw && py >= cy && py < cy + ch;
-
- let old_tab = self.hovered_tab;
- self.hovered_tab = None;
- if self.visible {
- 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.hovered_tab = Some(i);
- break;
- }
- }
- }
-
- let cs_changed = if self.visible && self.active_page == 2 {
- self.color_selector.cursor_moved(px, py)
- } else {
- let was = self.color_selector.hovered();
- self.color_selector.set_hovered(false);
- was
- };
-
- let cs_node_changed = if self.visible && self.active_page == 1 {
- self.node_color_selector.cursor_moved(px, py)
- } else {
- let was = self.node_color_selector.hovered();
- self.node_color_selector.set_hovered(false);
- was
- };
-
- let cs_changed = cs_changed || cs_node_changed;
-
- 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);
- }
- }
-
- was != self.hovered || old_close != self.close_hovered || old_tab != self.hovered_tab
- || cs_changed || widgets_changed
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- 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 state == ElementState::Pressed {
- if !in_panel || on_close {
- self.color_selector.unfocus();
- self.node_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;
- }
- 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.node_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();
- self.node_color_selector.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();
- self.node_color_selector.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();
- self.node_color_selector.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();
- self.node_color_selector.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();
- self.node_color_selector.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();
- self.node_color_selector.unfocus();
- handled = true;
- } else if self.node_color_selector.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();
- self.spin_skipped_col_w.unfocus();
- handled = true;
- }
- } else if self.active_page == 2 {
- 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.node_color_selector.unfocus();
- 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.node_color_selector.unfocus();
- 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.node_color_selector.unfocus();
- 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();
- self.node_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();
- self.node_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();
- self.node_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();
- self.node_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();
- self.node_color_selector.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.node_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 == 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; }
- if self.node_color_selector.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.node_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;
- }
- false
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- if !self.visible { return vec![]; }
- let mut quads = Vec::new();
- quads.push((self.x, self.y, self.w, self.h, [0.0, 0.0, 0.0, 0.5]));
- let (px, py, pw, ph) = self.panel_rect();
- quads.push((px, py, pw, ph, colors::PANEL_MENU_BG));
-
- // Close button hover
- if self.close_hovered {
- let (cx, cy, cw, ch) = self.close_rect(px, pw, py);
- quads.push((cx, cy, cw, ch, colors::PANEL_MENU_HOVER));
- }
-
- // Tab bar background
- let tab_y = py + 30.0;
- let tab_h = 22.0;
- quads.push((px + 8.0, tab_y, pw - 16.0, tab_h, [0.15, 0.15, 0.20, 1.0]));
-
- // Tabs
- let tabs = self.tab_rects(px, py);
- for (i, &(tx, ty, tw, th)) in tabs.iter().enumerate() {
- let bg = if i == self.active_page {
- colors::PANEL_MENU_BG
- } else if Some(i) == self.hovered_tab {
- [0.22, 0.22, 0.30, 1.0]
- } else {
- [0.18, 0.18, 0.25, 1.0]
- };
- quads.push((tx, ty, tw, th, bg));
- }
-
- 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()));
- quads.extend(filter_quads(self.node_color_selector.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_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
- }
-
- fn text_labels(&self) -> Vec<TextLabel> {
- if !self.visible { return vec![]; }
- let (px, py, pw, _) = self.panel_rect();
- let mut labels = Vec::new();
-
- // Title and close button
- labels.push(TextLabel { text: "Configure Clear Design Interface".into(), x: px + 16.0, y: py + 10.0, font_size: 14.0, color: [0xcc, 0xcc, 0xd4] });
- labels.push(TextLabel { text: "\u{2715}".into(), x: px + pw - 22.0, y: py + 10.0, font_size: 14.0, color: [0xaa, 0xaa, 0xbb] });
-
- // Tab labels
- 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 { [0xaa, 0xaa, 0xbb] };
- labels.push(TextLabel {
- text: tab_labels[i].into(),
- x: tx + 8.0,
- y: ty + 4.0,
- font_size: 12.0,
- color,
- });
- }
-
- 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.node_color_selector.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.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
- }
-
- fn take_click(&mut self) -> bool {
- if !self.visible { return false; }
- true
- }
-
- fn z_index(&self) -> i32 {
- 200
- }
-}
-
struct NodePalette {
x: f32, y: f32, w: f32, h: f32,
visible: bool,
@@ -1567,6 +494,7 @@ enum Action {
ToggleOrigin,
ToggleCameraPivot,
ToggleCircularPane,
+ DetachCircularWindow,
}
struct KeyBind {
@@ -1599,24 +527,31 @@ fn default_keybinds() -> Vec<KeyBind> {
]
}
+use clear_ui::engine::{
+ Vertex, quad_vertices_with_clip as quad_vertices, widget_vertices,
+ quad_vertices_clipped, circle_vertices, circle_border_vertices, arc_background_vertices,
+};
+
#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
-struct Vertex {
+struct TexturedVertex {
position: [f32; 2],
+ tex_coords: [f32; 2],
color: [f32; 4],
- clip_circle: [f32; 3], // [cx, cy, r]
+ clip_circle: [f32; 3],
}
-impl Vertex {
- const ATTRIBS: [wgpu::VertexAttribute; 3] = wgpu::vertex_attr_array![
+impl TexturedVertex {
+ const ATTRIBS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
0 => Float32x2,
- 1 => Float32x4,
- 2 => Float32x3,
+ 1 => Float32x2,
+ 2 => Float32x4,
+ 3 => Float32x3,
];
fn desc() -> wgpu::VertexBufferLayout<'static> {
wgpu::VertexBufferLayout {
- array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
+ array_stride: std::mem::size_of::<TexturedVertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &Self::ATTRIBS,
}
@@ -1624,165 +559,6 @@ impl Vertex {
}
-fn quad_vertices(
- x: f32, y: f32, w: f32, h: f32,
- surface_w: f32, surface_h: f32,
- color: [f32; 4],
- clip_circle: [f32; 3],
-) -> [Vertex; 6] {
- let x0 = (x / surface_w) * 2.0 - 1.0;
- let y0 = 1.0 - (y / surface_h) * 2.0;
- let x1 = ((x + w) / surface_w) * 2.0 - 1.0;
- let y1 = 1.0 - ((y + h) / surface_h) * 2.0;
- [
- Vertex { position: [x0, y0], color, clip_circle },
- Vertex { position: [x1, y0], color, clip_circle },
- Vertex { position: [x0, y1], color, clip_circle },
- Vertex { position: [x1, y0], color, clip_circle },
- Vertex { position: [x1, y1], color, clip_circle },
- Vertex { position: [x0, y1], color, clip_circle },
- ]
-}
-
-fn widget_vertices(w: &dyn Widget, sw: f32, sh: f32, clip_circle: [f32; 3]) -> Vec<Vertex> {
- let (x, y, ww, h) = w.rect();
- quad_vertices(x, y, ww, h, sw, sh, w.color(), clip_circle).to_vec()
-}
-
-fn quad_vertices_clipped(
- x: f32, y: f32, w: f32, h: f32,
- surface_w: f32, surface_h: f32,
- color: [f32; 4],
- clip: (f32, f32, f32, f32),
- clip_circle: [f32; 3],
-) -> Vec<Vertex> {
- let (cx0, cy0, cx1, cy1) = clip;
- let ix0 = x.max(cx0);
- let iy0 = y.max(cy0);
- let ix1 = (x + w).min(cx1);
- let iy1 = (y + h).min(cy1);
- if ix1 <= ix0 || iy1 <= iy0 {
- return Vec::new();
- }
- quad_vertices(ix0, iy0, ix1 - ix0, iy1 - iy0, surface_w, surface_h, color, clip_circle).to_vec()
-}
-
-fn widget_vertices_clipped(w: &dyn Widget, sw: f32, sh: f32, clip: (f32, f32, f32, f32), clip_circle: [f32; 3]) -> Vec<Vertex> {
- let (x, y, ww, h) = w.rect();
- quad_vertices_clipped(x, y, ww, h, sw, sh, w.color(), clip, clip_circle)
-}
-
-fn circle_vertices(
- cx: f32, cy: f32, r: f32,
- sw: f32, sh: f32,
- color: [f32; 4],
- segments: usize,
- clip_circle: [f32; 3],
-) -> Vec<Vertex> {
- let mut verts = Vec::new();
- for i in 0..segments {
- let theta1 = (i as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
- let theta2 = ((i + 1) as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
- let x0 = cx;
- let y0 = cy;
- let x1 = cx + r * theta1.cos();
- let y1 = cy + r * theta1.sin();
- let x2 = cx + r * theta2.cos();
- let y2 = cy + r * theta2.sin();
-
- let ndc_x0 = (x0 / sw) * 2.0 - 1.0;
- let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
- let ndc_x1 = (x1 / sw) * 2.0 - 1.0;
- let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
- let ndc_x2 = (x2 / sw) * 2.0 - 1.0;
- let ndc_y2 = 1.0 - (y2 / sh) * 2.0;
-
- verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
- verts.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
- verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
- }
- verts
-}
-
-fn circle_border_vertices(
- cx: f32, cy: f32, r: f32,
- thickness: f32,
- sw: f32, sh: f32,
- color: [f32; 4],
- segments: usize,
- clip_circle: [f32; 3],
-) -> Vec<Vertex> {
- let mut verts = Vec::new();
- for i in 0..segments {
- let theta1 = (i as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
- let theta2 = ((i + 1) as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
-
- let x0 = cx + (r - thickness) * theta1.cos();
- let y0 = cy + (r - thickness) * theta1.sin();
- let x1 = cx + r * theta1.cos();
- let y1 = cy + r * theta1.sin();
-
- let x2 = cx + r * theta2.cos();
- let y2 = cy + r * theta2.sin();
- let x3 = cx + (r - thickness) * theta2.cos();
- let y3 = cy + (r - thickness) * theta2.sin();
-
- let ndc_x0 = (x0 / sw) * 2.0 - 1.0; let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
- let ndc_x1 = (x1 / sw) * 2.0 - 1.0; let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
- let ndc_x2 = (x2 / sw) * 2.0 - 1.0; let ndc_y2 = 1.0 - (y2 / sh) * 2.0;
- let ndc_x3 = (x3 / sw) * 2.0 - 1.0; let ndc_y3 = 1.0 - (y3 / sh) * 2.0;
-
- verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
- verts.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
- verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
-
- verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
- verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
- verts.push(Vertex { position: [ndc_x3, ndc_y3], color, clip_circle });
- }
- verts
-}
-
-fn arc_background_vertices(
- cx: f32, cy: f32, r: f32,
- thickness: f32,
- start_angle: f32, end_angle: f32,
- sw: f32, sh: f32,
- color: [f32; 4],
- segments: usize,
- clip_circle: [f32; 3],
-) -> Vec<Vertex> {
- let mut verts = Vec::new();
- for i in 0..segments {
- let theta1 = start_angle + (i as f32) * (end_angle - start_angle) / (segments as f32);
- let theta2 = start_angle + ((i + 1) as f32) * (end_angle - start_angle) / (segments as f32);
-
- let x0 = cx + (r - thickness) * theta1.cos();
- let y0 = cy + (r - thickness) * theta1.sin();
- let x1 = cx + r * theta1.cos();
- let y1 = cy + r * theta1.sin();
-
- let x2 = cx + r * theta2.cos();
- let y2 = cy + r * theta2.sin();
- let x3 = cx + (r - thickness) * theta2.cos();
- let y3 = cy + (r - thickness) * theta2.sin();
-
- let ndc_x0 = (x0 / sw) * 2.0 - 1.0; let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
- let ndc_x1 = (x1 / sw) * 2.0 - 1.0; let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
- let ndc_x2 = (x2 / sw) * 2.0 - 1.0; let ndc_y2 = 1.0 - (y2 / sh) * 2.0;
- let ndc_x3 = (x3 / sw) * 2.0 - 1.0; let ndc_y3 = 1.0 - (y3 / sh) * 2.0;
-
- verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
- verts.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
- verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
-
- verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
- verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
- verts.push(Vertex { position: [ndc_x3, ndc_y3], color, clip_circle });
- }
- verts
-}
-
fn make_text_buffer(font_system: &mut FontSystem, text: &str, size: f32) -> Buffer {
let metrics = Metrics::new(size, size * 1.4);
let mut buffer = Buffer::new(font_system, metrics);
@@ -1808,14 +584,14 @@ fn get_next_visible_pane(current_pane: usize, show_spreadsheet: bool, shift_pres
}
struct State {
- window: XdgWindow,
- wl_surface: wl_surface::WlSurface,
surface: wgpu::Surface<'static>,
device: wgpu::Device,
queue: wgpu::Queue,
config: wgpu::SurfaceConfiguration,
render_pipeline: wgpu::RenderPipeline,
vertex_buffer: wgpu::Buffer,
+ window: XdgWindow,
+ wl_surface: wl_surface::WlSurface,
vertex_count: u32,
pipeline_3d: wgpu::RenderPipeline,
@@ -1849,6 +625,9 @@ struct State {
show_camera_pivot: bool,
viewport_bg_color: [f32; 3],
node_color: [f32; 3],
+ grid_color: [f32; 3],
+ cell_color: [f32; 3],
+ gap_color: [f32; 3],
vertex_buffer_origin: wgpu::Buffer,
vertex_count_origin: u32,
vertex_buffer_pivot: wgpu::Buffer,
@@ -1868,8 +647,7 @@ struct State {
widgets: Vec<Box<dyn Widget>>,
positions: Vec<(f32, f32, f32, f32)>,
- splitter1_x: f32,
- splitter2_x: f32,
+ splitter_layout: clear_ui::layout::SplitterLayout,
node_palette_visible: bool,
node_palette_query: String,
node_palette_filtered: Vec<usize>,
@@ -1881,7 +659,17 @@ struct State {
text_renderer: TextRenderer,
text_viewport: Viewport,
- status_buffer: Buffer,
+ curved_text_texture: wgpu::Texture,
+ curved_text_texture_view: wgpu::TextureView,
+ curved_text_sampler: wgpu::Sampler,
+ curved_text_bind_group: wgpu::BindGroup,
+ curved_text_pipeline: wgpu::RenderPipeline,
+ curved_text_atlas: TextAtlas,
+ curved_text_renderer: TextRenderer,
+ curved_text_viewport: Viewport,
+ textured_vertex_buffer: wgpu::Buffer,
+ textured_vertex_count: u32,
+
drag_widget: Option<usize>,
focused_widget: Option<usize>,
@@ -1934,14 +722,25 @@ struct State {
focused_pane: usize,
inertial_scroll_enabled: bool,
inertial_scroll_friction: f32,
+ scroll_speed: f32,
last_config_read: Instant,
circular_network_pane: bool,
- network_circle_x: f32,
- network_circle_y: f32,
- network_circle_radius: f32,
+ circular_network_layout: clear_ui::layout::CircularPaneLayout,
is_dragging_network_circle: bool,
circle_drag_ox: f32,
circle_drag_oy: f32,
+ is_detached_network: bool,
+ detached_circular_network: bool,
+ last_project_mod_time: Option<std::time::SystemTime>,
+ last_project_check: std::time::Instant,
+ last_inspector_check: std::time::Instant,
+ pub window_x: i32,
+ pub window_y: i32,
+ pub active_menu_cloud_pid: Option<u32>,
+ pub active_menu_cloud_idx: Option<(usize, usize)>,
+ uniform_background: bool,
+ network_opacity: f32,
+ last_design_mod_time: Option<std::time::SystemTime>,
}
impl State {
@@ -1959,16 +758,21 @@ impl State {
origin_size: self.origin_size,
viewport_bg_color: self.viewport_bg_color,
node_color: self.node_color,
+ grid_color: self.grid_color,
+ cell_color: self.cell_color,
+ gap_color: self.gap_color,
square_viewport: self.square_viewport,
grid_thickness: self.grid_thickness,
show_camera_pivot_enabled: self.show_camera_pivot,
camera_pivot_size: self.camera_pivot_size,
+ uniform_background: self.uniform_background,
+ network_opacity: self.network_opacity,
};
settings.save();
}
fn update_grid_geometry(&mut self) {
- let grid_verts = grid_vertices(self.grid_thickness);
+ let grid_verts = grid_vertices(self.grid_thickness, self.grid_color);
self.queue.write_buffer(&self.vertex_buffer_grid, 0, bytemuck::cast_slice(&grid_verts));
}
@@ -1984,23 +788,33 @@ impl State {
fn body_h(&self) -> f32 { self.height - HEADER_H - STATUS_H }
- fn content_left_w(&self) -> f32 { self.splitter1_x }
+ fn content_left_w(&self) -> f32 { self.splitter_layout.left_col().1 }
- fn content_right_x(&self) -> f32 { self.splitter1_x + SPLITTER_W }
+ fn content_right_x(&self) -> f32 { self.splitter_layout.center_col().0 }
- fn viewport_w(&self) -> f32 { self.splitter2_x - self.content_right_x() }
+ fn viewport_w(&self) -> f32 { self.splitter_layout.center_col().1 }
- fn param_x(&self) -> f32 { self.splitter2_x + SPLITTER_W }
+ fn param_x(&self) -> f32 { self.splitter_layout.right_col(self.width).0 }
- fn param_w(&self) -> f32 { self.width - self.param_x() }
+ fn param_w(&self) -> f32 { self.splitter_layout.right_col(self.width).1 }
+
+ fn in_network_pane(&self) -> bool {
+ let node_area_y = HEADER_H + MENUBAR_H + BREADCRUMB_H;
+ if self.circular_network_pane {
+ self.circular_network_layout.hit_test_content(self.cursor_x, self.cursor_y, MENUBAR_H, BREADCRUMB_H)
+ } else {
+ self.cursor_x >= 0.0
+ && self.cursor_x < self.content_left_w()
+ && self.cursor_y >= node_area_y
+ && self.cursor_y < self.height - STATUS_H
+ }
+ }
fn clamp_splitters(&mut self) {
- let min_s1 = MIN_COLUMN;
- let max_s1 = self.splitter2_x - SPLITTER_W - MIN_COLUMN;
- self.splitter1_x = self.splitter1_x.clamp(min_s1, max_s1);
- let min_s2 = self.splitter1_x + SPLITTER_W + MIN_COLUMN;
- let max_s2 = self.width - SPLITTER_W - MIN_COLUMN;
- self.splitter2_x = self.splitter2_x.clamp(min_s2, max_s2);
+ if self.is_detached_network {
+ return;
+ }
+ self.splitter_layout.clamp(self.width, self.detached_circular_network);
}
fn current_dir(&self) -> &FsNode {
@@ -2019,6 +833,30 @@ impl State {
node
}
+ fn sync_parameters_to_project(&mut self) {
+ if !self.is_detached_network {
+ if let Some(slot_idx) = self.widgets[CONTENT_IDX].selected_node() {
+ let updated_params = self.widgets[PARAM_IDX].node_params();
+ let dir = self.current_dir_mut();
+ if let Some(child) = dir.children.get_mut(slot_idx) {
+ let mut param_changed = false;
+ for (u_name, u_val, _) in &updated_params {
+ if let Some(p) = child.params.iter_mut().find(|p| p.name == *u_name) {
+ if p.default != *u_val {
+ p.default = u_val.clone();
+ param_changed = true;
+ }
+ }
+ }
+ if param_changed {
+ self.rebuild_scene_geometry();
+ self.sync_nodes();
+ }
+ }
+ }
+ }
+ }
+
fn current_path_names(&self) -> Vec<String> {
let mut node = &self.fs_root;
let mut names = Vec::new();
@@ -2115,6 +953,140 @@ impl State {
});
}
+ fn spawn_menu_cloud(&mut self, widget_idx: usize, menu_idx: usize, title: String, items: Vec<String>, rx: f32, ry: f32, _rw: f32, rh: f32) {
+ let is_same_menu = self.active_menu_cloud_idx == Some((widget_idx, menu_idx));
+ let mut process_was_running = false;
+
+ if let Some(pid) = self.active_menu_cloud_pid {
+ let is_running = unsafe {
+ libc::kill(pid as libc::pid_t, 0) == 0
+ };
+ if is_running {
+ process_was_running = true;
+ unsafe {
+ libc::kill(pid as libc::pid_t, libc::SIGTERM);
+ }
+ }
+ }
+
+ self.active_menu_cloud_pid = None;
+ self.active_menu_cloud_idx = None;
+
+ if is_same_menu && process_was_running {
+ return;
+ }
+
+ let x = (self.window_x as f64 + rx as f64) as i32;
+ let y = (self.window_y as f64 + (ry + rh) as f64) as i32;
+
+ use std::io::Write;
+ use std::process::{Command, Stdio};
+
+ let max_len = items.iter().map(|it| it.len()).max().unwrap_or(10);
+ let layout_width = (max_len * 8 + 48).max(140) as u32;
+
+ let mut widgets = vec![
+ serde_json::json!({
+ "type": "label",
+ "text": title.clone(),
+ })
+ ];
+ for (idx, item) in items.iter().enumerate() {
+ widgets.push(serde_json::json!({
+ "type": "button",
+ "id": idx.to_string(),
+ "text": item.clone(),
+ }));
+ }
+
+ let json_config = serde_json::json!({
+ "width": layout_width,
+ "widgets": widgets,
+ });
+
+ let mut child = match Command::new("clear-cloud")
+ .arg("--layout")
+ .arg("-x")
+ .arg(x.to_string())
+ .arg("-y")
+ .arg(y.to_string())
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .spawn()
+ {
+ Ok(c) => c,
+ Err(e) => {
+ eprintln!("Failed to spawn clear-cloud for menu: {:?}", e);
+ return;
+ }
+ };
+
+ let pid = child.id();
+ self.active_menu_cloud_pid = Some(pid);
+ self.active_menu_cloud_idx = Some((widget_idx, menu_idx));
+
+ std::thread::spawn(move || {
+ if let Some(mut stdin) = child.stdin.take() {
+ let _ = stdin.write_all(json_config.to_string().as_bytes());
+ }
+
+ let output = match child.wait_with_output() {
+ Ok(o) => o,
+ Err(e) => {
+ eprintln!("Failed to wait for clear-cloud for menu: {:?}", e);
+ return;
+ }
+ };
+
+ if output.status.success() {
+ let out_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
+ if let Ok(res_val) = serde_json::from_str::<serde_json::Value>(&out_str) {
+ if let Some(btn_id) = res_val["button"].as_str() {
+ if let Ok(item_idx) = btn_id.parse::<usize>() {
+ let body = format!(
+ "{{\"action\":\"menu_click\",\"widget_idx\":{},\"menu_idx\":{},\"item_idx\":{}}}",
+ widget_idx, menu_idx, item_idx
+ );
+ let req = format!(
+ "POST /action HTTP/1.1\r\n\
+ Host: 127.0.0.1:3000\r\n\
+ Content-Type: application/json\r\n\
+ Content-Length: {}\r\n\
+ Connection: close\r\n\r\n\
+ {}",
+ body.len(),
+ body
+ );
+ if let Ok(mut stream) = std::net::TcpStream::connect("127.0.0.1:3000") {
+ let _ = stream.write_all(req.as_bytes());
+ let _ = stream.flush();
+ }
+ }
+ }
+ }
+ }
+
+ let body = format!(
+ "{{\"action\":\"menu_closed\",\"widget_idx\":{},\"menu_idx\":{}}}",
+ widget_idx, menu_idx
+ );
+ let req = format!(
+ "POST /action HTTP/1.1\r\n\
+ Host: 127.0.0.1:3000\r\n\
+ Content-Type: application/json\r\n\
+ Content-Length: {}\r\n\
+ Connection: close\r\n\r\n\
+ {}",
+ body.len(),
+ body
+ );
+ if let Ok(mut stream) = std::net::TcpStream::connect("127.0.0.1:3000") {
+ let _ = stream.write_all(req.as_bytes());
+ let _ = stream.flush();
+ }
+ });
+ }
+
fn close_node_palette(&mut self) {
self.node_palette_visible = false;
self.refresh_node_palette();
@@ -2279,11 +1251,87 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
}
}
}
- rows.push(row);
+ rows.push(row);
+ }
+
+ (headers, rows)
+}
+
+ fn update_active_camera_rotation(&mut self, d_yaw: f32, d_pitch: f32) -> bool {
+ if self.active_camera == "Default Camera" {
+ return false;
+ }
+ let camera_name = self.active_camera.clone();
+ let dir = self.current_dir_mut();
+ if let Some(node) = dir.children.iter_mut().find(|c| c.node_type == "camera" && c.name == camera_name) {
+ if let Some(p) = node.params.iter_mut().find(|p| p.name == "Rotation") {
+ let parts: Vec<&str> = p.default
+ .split(|c| c == ':' || c == ',' || c == ' ')
+ .filter(|s| !s.is_empty())
+ .collect();
+ let mut rx = 0.0f32;
+ let mut ry = 0.0f32;
+ let mut rz = 0.0f32;
+ if parts.len() >= 3 {
+ if let (Ok(vx), Ok(vy), Ok(vz)) = (parts[0].parse::<f32>(), parts[1].parse::<f32>(), parts[2].parse::<f32>()) {
+ rx = vx;
+ ry = vy;
+ rz = vz;
+ }
+ }
+ ry += d_yaw.to_degrees();
+ rx -= d_pitch.to_degrees();
+ while rx > 180.0 { rx -= 360.0; }
+ while rx < -180.0 { rx += 360.0; }
+ while ry > 180.0 { ry -= 360.0; }
+ while ry < -180.0 { ry += 360.0; }
+ p.default = format!("{:.2}:{:.2}:{:.2}", rx, ry, rz);
+
+ self.sync_nodes();
+ let params = if !self.is_detached_network {
+ self.widgets[CONTENT_IDX].selected_node().and_then(|sel_idx| {
+ let dir = self.current_dir();
+ if sel_idx < dir.children.len() {
+ Some(param_display(&dir.children[sel_idx].params))
+ } else { None }
+ }).unwrap_or_default()
+ } else {
+ vec![]
+ };
+ self.widgets[PARAM_IDX].set_display_params(¶ms);
+
+ return true;
+ }
+ }
+ false
}
- (headers, rows)
-}
+ fn update_active_camera_rotation_reset(&mut self) -> bool {
+ if self.active_camera == "Default Camera" {
+ return false;
+ }
+ let camera_name = self.active_camera.clone();
+ let dir = self.current_dir_mut();
+ if let Some(node) = dir.children.iter_mut().find(|c| c.node_type == "camera" && c.name == camera_name) {
+ if let Some(p) = node.params.iter_mut().find(|p| p.name == "Rotation") {
+ p.default = "0.00:0.00:0.00".to_string();
+ self.sync_nodes();
+ let params = if !self.is_detached_network {
+ self.widgets[CONTENT_IDX].selected_node().and_then(|sel_idx| {
+ let dir = self.current_dir();
+ if sel_idx < dir.children.len() {
+ Some(param_display(&dir.children[sel_idx].params))
+ } else { None }
+ }).unwrap_or_default()
+ } else {
+ vec![]
+ };
+ self.widgets[PARAM_IDX].set_display_params(¶ms);
+ return true;
+ }
+ }
+ false
+ }
fn on_path_changed(&mut self) {
self.drag_widget = None;
@@ -2358,7 +1406,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
self.widgets[BREADCRUMB_IDX].set_path(&path_strs);
let mut selected_node = None;
- if self.focused_widget == Some(CONTENT_IDX) {
+ if !self.is_detached_network {
if let Some(slot_idx) = self.widgets[CONTENT_IDX].selected_node() {
let dir = self.current_dir();
if slot_idx < dir.children.len() {
@@ -2409,6 +1457,32 @@ 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("add") {
+ if let Some(idx) = find_sphere_index(&self.fs_root, node) {
+ let center = Vec3::new((idx % 4) as f32 * 1.25 - 1.875, 0.55, -((idx / 4) as f32) * 1.25);
+ let num_points = node_param_f32(node, "Points", 100.0) as i32;
+ let mut geom = Geometry::new();
+ for i in 0..num_points {
+ let t = i as f32 / num_points.max(1) as f32;
+ let angle = t * std::f32::consts::TAU * 3.0;
+ let r = 0.4 * t;
+ let px = center.x + r * angle.cos();
+ let py = center.y + t * 0.5 - 0.25;
+ let pz = center.z + r * angle.sin();
+ let pt_center = Vec3::new(px, py, pz);
+ geom.merge(sphere_vertices(pt_center, 0.02));
+ }
+ let (h, r) = Self::geometry_to_spreadsheet_data(&geom);
+ headers = h;
+ rows = r;
+ }
+ } else if node.node_type.eq_ignore_ascii_case("transform") {
+ let mut visited = Vec::new();
+ if let Some(geom) = resolve_transform_geometry(&self.fs_root, node, &mut visited) {
+ let (h, r) = Self::geometry_to_spreadsheet_data(&geom);
+ headers = h;
+ rows = r;
+ }
}
}
@@ -2420,12 +1494,13 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
fn save_to_file(&self, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let proj = Project {
- name: "Project".to_string(),
+ name: "Default Project".to_string(),
root: self.fs_root.clone(),
view_state: ProjectViewState {
active_camera: self.active_camera.clone(),
pan: (self.pan_x, self.pan_y),
current_path: self.current_path.clone(),
+ selected_node: self.widgets[CONTENT_IDX].selected_node(),
},
};
let content = serde_json::to_string_pretty(&proj)?;
@@ -2449,12 +1524,32 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
self.scroll_accum_y = 0.0;
self.current_path = proj.view_state.current_path;
- self.focused_widget = None;
+ let sel = proj.view_state.selected_node;
+ self.widgets[CONTENT_IDX].set_selected_node(sel);
+ if sel.is_some() {
+ self.focused_widget = Some(CONTENT_IDX);
+ } else {
+ self.focused_widget = None;
+ }
self.drag_widget = None;
self.last_click = None;
self.sync_grid_settings();
self.sync_nodes();
+
+ // Sync Parameters pane with selected node
+ let params = if !self.is_detached_network {
+ self.widgets[CONTENT_IDX].selected_node().and_then(|sel_idx| {
+ let dir = self.current_dir();
+ if sel_idx < dir.children.len() {
+ Some(param_display(&dir.children[sel_idx].params))
+ } else { None }
+ }).unwrap_or_default()
+ } else {
+ vec![]
+ };
+ self.widgets[PARAM_IDX].set_display_params(¶ms);
+
self.rebuild_scene_geometry();
self.rebuild_positions();
self.apply_layout();
@@ -2522,6 +1617,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
pw: u32,
ph: u32,
scale: f64,
+ is_detached_network: bool,
) -> Self {
let settings = DesignSettings::load();
let lw = pw as f32 / scale as f32;
@@ -2531,9 +1627,15 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
let wl_surface = compositor_state.create_surface(qh);
wl_surface.set_buffer_scale(scale as i32);
let window = xdg_shell_state.create_window(wl_surface.clone(), WindowDecorations::None, qh);
- window.set_title("Clear Design Interface");
- window.set_app_id("clear-design-interface");
- window.set_min_size(Some(((480.0 * scale) as u32, (320.0 * scale) as u32)));
+ if is_detached_network {
+ window.set_title("Network Pane");
+ window.set_app_id("circular-network-pane");
+ window.set_min_size(Some(((200.0 * scale) as u32, (200.0 * scale) as u32)));
+ } else {
+ window.set_title("Clear Design Interface");
+ window.set_app_id("clear-design-interface");
+ window.set_min_size(Some(((480.0 * scale) as u32, (320.0 * scale) as u32)));
+ }
window.commit();
let wayland_handle = Box::leak(Box::new(clear_ui::wayland::WaylandSurfaceHandle {
@@ -2572,9 +1674,18 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
.await
.expect("Failed to create device");
- let config = surface
+ let mut config = surface
.get_default_config(&adapter, pw, ph)
.expect("Failed to get surface config");
+ let capabilities = surface.get_capabilities(&adapter);
+ let alpha_mode = if capabilities.alpha_modes.contains(&wgpu::CompositeAlphaMode::PreMultiplied) {
+ wgpu::CompositeAlphaMode::PreMultiplied
+ } else if capabilities.alpha_modes.contains(&wgpu::CompositeAlphaMode::PostMultiplied) {
+ wgpu::CompositeAlphaMode::PostMultiplied
+ } else {
+ capabilities.alpha_modes[0]
+ };
+ config.alpha_mode = alpha_mode;
surface.configure(&device, &config);
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
@@ -2721,7 +1832,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
mapped_at_creation: false,
});
- let grid_verts = grid_vertices(settings.grid_thickness);
+ let grid_verts = grid_vertices(settings.grid_thickness, settings.grid_color);
let vertex_count_grid = grid_verts.len() as u32;
let vertex_buffer_grid = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Grid Vertex Buffer"),
@@ -2794,10 +1905,129 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
let mut text_viewport = Viewport::new(&device, &cache);
text_viewport.update(&queue, Resolution { width: pw, height: ph });
- let status_buffer = make_text_buffer(&mut font_system, "Ready", 12.0);
+ let shader_textured = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("Textured Shader"),
+ source: wgpu::ShaderSource::Wgsl(include_str!("shader_textured.wgsl").into()),
+ });
+
+ let textured_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("Textured Bind Group Layout"),
+ entries: &[
+ wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Texture {
+ multisampled: false,
+ view_dimension: wgpu::TextureViewDimension::D2,
+ sample_type: wgpu::TextureSampleType::Float { filterable: true },
+ },
+ count: None,
+ },
+ wgpu::BindGroupLayoutEntry {
+ binding: 1,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
+ count: None,
+ },
+ ],
+ });
+
+ let textured_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("Textured Pipeline Layout"),
+ bind_group_layouts: &[&textured_bind_group_layout],
+ push_constant_ranges: &[],
+ });
+
+ let curved_text_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: Some("Textured Render Pipeline"),
+ layout: Some(&textured_pipeline_layout),
+ vertex: wgpu::VertexState {
+ module: &shader_textured,
+ entry_point: Some("vs_main"),
+ buffers: &[TexturedVertex::desc()],
+ compilation_options: Default::default(),
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &shader_textured,
+ entry_point: Some("fs_main"),
+ targets: &[Some(wgpu::ColorTargetState {
+ format: config.format,
+ blend: Some(wgpu::BlendState::ALPHA_BLENDING),
+ write_mask: wgpu::ColorWrites::ALL,
+ })],
+ compilation_options: Default::default(),
+ }),
+ primitive: wgpu::PrimitiveState {
+ topology: wgpu::PrimitiveTopology::TriangleList,
+ strip_index_format: None,
+ front_face: wgpu::FrontFace::Ccw,
+ cull_mode: None,
+ unclipped_depth: false,
+ polygon_mode: wgpu::PolygonMode::Fill,
+ conservative: false,
+ },
+ depth_stencil: None,
+ multisample: wgpu::MultisampleState::default(),
+ multiview: None,
+ cache: None,
+ });
+
+ let curved_text_texture = device.create_texture(&wgpu::TextureDescriptor {
+ label: Some("Curved Text Texture"),
+ size: wgpu::Extent3d {
+ width: 1024,
+ height: 1024,
+ depth_or_array_layers: 1,
+ },
+ mip_level_count: 1,
+ sample_count: 1,
+ dimension: wgpu::TextureDimension::D2,
+ format: config.format,
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
+ view_formats: &[],
+ });
+ let curved_text_texture_view = curved_text_texture.create_view(&wgpu::TextureViewDescriptor::default());
+
+ let curved_text_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
+ label: Some("Curved Text Sampler"),
+ address_mode_u: wgpu::AddressMode::ClampToEdge,
+ address_mode_v: wgpu::AddressMode::ClampToEdge,
+ address_mode_w: wgpu::AddressMode::ClampToEdge,
+ mag_filter: wgpu::FilterMode::Linear,
+ min_filter: wgpu::FilterMode::Linear,
+ mipmap_filter: wgpu::FilterMode::Nearest,
+ ..Default::default()
+ });
+
+ let curved_text_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("Curved Text Bind Group"),
+ layout: &textured_bind_group_layout,
+ entries: &[
+ wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::TextureView(&curved_text_texture_view),
+ },
+ wgpu::BindGroupEntry {
+ binding: 1,
+ resource: wgpu::BindingResource::Sampler(&curved_text_sampler),
+ },
+ ],
+ });
+
+ let mut curved_text_atlas = TextAtlas::new(&device, &queue, &cache, config.format);
+ let curved_text_renderer = TextRenderer::new(&mut curved_text_atlas, &device, wgpu::MultisampleState::default(), None);
+ let mut curved_text_viewport = Viewport::new(&device, &cache);
+ curved_text_viewport.update(&queue, Resolution { width: 1024, height: 1024 });
+
+ let textured_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("Textured Vertex Buffer"),
+ size: 1,
+ usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+
- let splitter1_x = (sw - 2.0 * SPLITTER_W) / 3.0;
- let splitter2_x = splitter1_x + SPLITTER_W + (sw - 2.0 * SPLITTER_W) / 3.0;
+ let splitter_layout = clear_ui::layout::SplitterLayout::new(sw, SPLITTER_W, MIN_COLUMN);
let (depth_texture, depth_texture_view) = {
let tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Depth Texture"),
@@ -2857,19 +2087,18 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
vec![]
};
let mut widgets: Vec<Box<dyn Widget>> = vec![
- Box::new(MenuBar::new(0.0, 0.0, 0.0, HEADER_H).with_title("Clear Design Interface").with_item("File", &["New Project", "Open", "Save", "Configure", "Exit"]).with_item("Edit", &["Undo", "Redo"]).with_item("View", &["Zoom In", "Zoom Out", "Reset Zoom"]).with_item("Help", &["About"]).with_z_index(110)),
+ Box::new(MenuBar::new(0.0, 0.0, 0.0, HEADER_H).with_title("Clear Design Interface").with_item("File", &["New Project", "Open", "Save", "Configure", "Exit"]).with_item("Edit", &["Undo", "Redo"]).with_item("View", &["Zoom In", "Zoom Out", "Reset Zoom", "Detach Circular Window"]).with_item("Help", &["About"]).with_z_index(110)),
Box::new(Graph::new()),
Box::new(Splitter::new(SPLITTER_W)),
Box::new(ViewportBg::new()),
Box::new(Splitter::new(SPLITTER_W)),
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", "Circular Pane"])),
+ 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", "Circular Pane", "Detach Pane"])),
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"])),
- Box::new(StatusBar::new()),
+ Box::new(StatusBar::new().with_text("Ready")),
Box::new(Breadcrumb::new()),
- Box::new(ConfigDialog::new(&settings)),
Box::new(NodePalette::new()),
Box::new(Spreadsheet::new()),
];
@@ -2929,6 +2158,9 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
show_camera_pivot: settings.show_camera_pivot_enabled,
viewport_bg_color: settings.viewport_bg_color,
node_color: settings.node_color,
+ grid_color: settings.grid_color,
+ cell_color: settings.cell_color,
+ gap_color: settings.gap_color,
vertex_buffer_origin,
vertex_count_origin,
vertex_buffer_pivot,
@@ -2945,8 +2177,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
exit_requested: false,
widgets,
positions,
- splitter1_x,
- splitter2_x,
+ splitter_layout,
node_palette_visible: false,
node_palette_query: String::new(),
node_palette_filtered: Vec::new(),
@@ -2956,7 +2187,17 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
text_atlas,
text_renderer,
text_viewport,
- status_buffer,
+ curved_text_texture,
+ curved_text_texture_view,
+ curved_text_sampler,
+ curved_text_bind_group,
+ curved_text_pipeline,
+ curved_text_atlas,
+ curved_text_renderer,
+ curved_text_viewport,
+ textured_vertex_buffer,
+ textured_vertex_count: 0,
+
drag_widget: None,
focused_widget: None,
cursor_x: 0.0,
@@ -3006,14 +2247,31 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
focused_pane: LEFT_MENUBAR_IDX,
inertial_scroll_enabled: true,
inertial_scroll_friction: 0.90,
+ scroll_speed: 1.0,
last_config_read: Instant::now(),
- circular_network_pane: false,
- network_circle_x: 250.0,
- network_circle_y: 300.0,
- network_circle_radius: 180.0,
+ circular_network_pane: is_detached_network,
+ circular_network_layout: clear_ui::layout::CircularPaneLayout::new(250.0, 300.0, 180.0),
is_dragging_network_circle: false,
circle_drag_ox: 0.0,
circle_drag_oy: 0.0,
+ is_detached_network,
+ detached_circular_network: false,
+ last_project_mod_time: {
+ let default_proj_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("default_project.json");
+ std::fs::metadata(&default_proj_path).and_then(|m| m.modified()).ok()
+ },
+ last_project_check: std::time::Instant::now(),
+ last_inspector_check: std::time::Instant::now(),
+ window_x: 0,
+ window_y: 0,
+ active_menu_cloud_pid: None,
+ active_menu_cloud_idx: None,
+ uniform_background: settings.uniform_background,
+ network_opacity: settings.network_opacity,
+ last_design_mod_time: {
+ let design_path = DesignSettings::file_path();
+ std::fs::metadata(&design_path).and_then(|m| m.modified()).ok()
+ },
};
state.update_inertial_settings();
@@ -3031,6 +2289,8 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
state.widgets[RIGHT_MENUBAR_IDX].set_item_checked(2, 2, state.show_origin);
state.widgets[RIGHT_MENUBAR_IDX].set_item_checked(2, 3, state.show_camera_pivot);
state.widgets[LEFT_MENUBAR_IDX].set_item_checked(2, 2, state.circular_network_pane);
+ state.widgets[LEFT_MENUBAR_IDX].set_item_checked(2, 3, state.detached_circular_network);
+ state.widgets[HEADER_IDX].set_item_checked(2, 3, state.detached_circular_network);
state.rebuild_positions();
state.apply_layout();
@@ -3043,12 +2303,12 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
fn sync_grid_settings(&mut self) {
let node_area_y = HEADER_H + MENUBAR_H + BREADCRUMB_H;
let active_node_area_y = if self.circular_network_pane {
- self.network_circle_y - self.network_circle_radius + 45.0 + MENUBAR_H + BREADCRUMB_H
+ self.circular_network_layout.y - self.circular_network_layout.r + 45.0 + MENUBAR_H + BREADCRUMB_H
} else {
node_area_y
};
let active_node_area_x = if self.circular_network_pane {
- self.network_circle_x - self.network_circle_radius
+ self.circular_network_layout.x - self.circular_network_layout.r
} else {
0.0
};
@@ -3058,16 +2318,28 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
self.widgets[CONTENT_IDX].set_skipped_sizes(self.skipped_row_h, self.skipped_col_w);
self.widgets[CONTENT_IDX].set_grid_origin(active_node_area_x + self.pan_x, active_node_area_y + self.pan_y);
self.widgets[CONTENT_IDX].set_grid_snap_enabled(self.grid_snap_enabled);
+ self.widgets[CONTENT_IDX].set_uniform_background(self.uniform_background);
+ self.widgets[CONTENT_IDX].set_network_opacity(self.network_opacity);
+ self.widgets[CONTENT_IDX].set_cell_color(self.cell_color);
+ self.widgets[CONTENT_IDX].set_gap_color(self.gap_color);
+ self.widgets[LEFT_MENUBAR_IDX].set_network_opacity(self.network_opacity);
+ self.widgets[BREADCRUMB_IDX].set_network_opacity(self.network_opacity);
}
fn update_inertial_settings(&mut self) {
self.last_config_read = Instant::now();
let config_path = "/home/lsgalante/.config/ccec/config.toml";
+
+ let mut enabled = true;
+ let mut friction = 0.90;
+ let mut speed = 1.0;
+
if let Ok(content) = std::fs::read_to_string(config_path) {
#[derive(serde::Deserialize)]
struct InertialSection {
inertial_scroll: Option<bool>,
scroll_friction: Option<u16>,
+ scroll_speed: Option<f32>,
}
#[derive(serde::Deserialize)]
struct Config {
@@ -3076,18 +2348,21 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
if let Ok(cfg) = toml::from_str::<Config>(&content) {
if let Some(inertial) = cfg.inertial {
if let Some(val) = inertial.inertial_scroll {
- self.inertial_scroll_enabled = val;
+ enabled = val;
}
if let Some(friction_val) = inertial.scroll_friction {
- let friction_f = (friction_val as f32 / 1000.0).clamp(0.1, 0.999);
- self.inertial_scroll_friction = friction_f;
+ friction = (friction_val as f32 / 1000.0).clamp(0.1, 0.999);
+ }
+ if let Some(speed_val) = inertial.scroll_speed {
+ speed = speed_val.clamp(0.01, 10.0);
}
}
}
}
- // Defaults if file read or parsing fails
- self.inertial_scroll_enabled = true;
- self.inertial_scroll_friction = 0.90;
+
+ self.inertial_scroll_enabled = enabled;
+ self.inertial_scroll_friction = friction;
+ self.scroll_speed = speed;
}
@@ -3141,12 +2416,12 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
fn keep_cursor_in_view(&mut self) {
let node_area_y = HEADER_H + MENUBAR_H + BREADCRUMB_H;
let active_node_area_y = if self.circular_network_pane {
- self.network_circle_y - self.network_circle_radius + 45.0 + MENUBAR_H + BREADCRUMB_H
+ self.circular_network_layout.y - self.circular_network_layout.r + 45.0 + MENUBAR_H + BREADCRUMB_H
} else {
node_area_y
};
let active_node_area_x = if self.circular_network_pane {
- self.network_circle_x - self.network_circle_radius
+ self.circular_network_layout.x - self.circular_network_layout.r
} else {
0.0
};
@@ -3157,12 +2432,12 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
let ch = self.grid_size_y;
let active_graph_w = if self.circular_network_pane {
- 2.0 * self.network_circle_radius
+ 2.0 * self.circular_network_layout.r
} else {
self.content_left_w()
};
let active_max_y = if self.circular_network_pane {
- self.network_circle_y + self.network_circle_radius
+ self.circular_network_layout.y + self.circular_network_layout.r
} else {
self.height - STATUS_H
};
@@ -3192,62 +2467,144 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
let px = self.param_x();
let node_area_y = HEADER_H + MENUBAR_H;
- self.positions[0] = (0.0, 0.0, self.width, HEADER_H);
- if self.circular_network_pane {
- let cx = self.network_circle_x;
- let cy = self.network_circle_y;
- let r = self.network_circle_radius;
+ if self.is_detached_network {
+ let cx = self.width / 2.0;
+ let cy = self.height / 2.0;
+ let r = (self.width.min(self.height) / 2.0 - 10.0).max(50.0);
+
+ self.circular_network_layout.x = cx;
+ self.circular_network_layout.y = cy;
+ self.circular_network_layout.r = r;
+ self.positions[0] = (0.0, 0.0, 0.0, 0.0);
self.positions[LEFT_MENUBAR_IDX] = (cx - r, cy - r, 2.0 * r, 35.0);
self.widgets[LEFT_MENUBAR_IDX].set_curved_circle(Some((cx, cy, r)));
self.positions[BREADCRUMB_IDX] = (cx - r, cy - r + 45.0 + MENUBAR_H, 2.0 * r, BREADCRUMB_H);
self.positions[CONTENT_IDX] = (cx - r, cy - r + 45.0 + MENUBAR_H + BREADCRUMB_H, 2.0 * r, 2.0 * r - (45.0 + MENUBAR_H + BREADCRUMB_H));
self.positions[SPLITTER1_IDX] = (0.0, 0.0, 0.0, 0.0);
- self.positions[SPLITTER2_IDX] = (self.splitter2_x, HEADER_H, SPLITTER_W, body_h);
- self.positions[PARAM_IDX] = (px, HEADER_H, self.param_w(), body_h);
+ self.positions[SPLITTER2_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[PARAM_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[VIEWPORT_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[RIGHT_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[SPREADSHEET_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[SPREADSHEET_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[CANVAS_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[PARAM_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[STATUS_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[NODE_PALETTE_IDX] = (0.0, 0.0, self.width, self.height);
+ } else if self.detached_circular_network {
+ // Parent process: Network pane is detached (hidden from main window)
+ let vw = self.splitter_layout.splitter2_x;
+ let px = self.splitter_layout.splitter2_x + SPLITTER_W;
+ let param_w = self.width - px;
+
+ self.positions[0] = (0.0, 0.0, self.width, HEADER_H);
+ self.positions[LEFT_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[BREADCRUMB_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[CONTENT_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[SPLITTER1_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[SPLITTER2_IDX] = (self.splitter_layout.splitter2_x, HEADER_H, SPLITTER_W, body_h);
+ self.positions[PARAM_IDX] = (px, HEADER_H, param_w, body_h);
- let viewport_w = self.splitter2_x;
- self.positions[VIEWPORT_IDX] = (0.0, HEADER_H, viewport_w, body_h);
- self.positions[RIGHT_MENUBAR_IDX] = (0.0, HEADER_H, viewport_w, MENUBAR_H);
+ self.positions[VIEWPORT_IDX] = (0.0, HEADER_H, vw, body_h);
+ self.positions[RIGHT_MENUBAR_IDX] = (0.0, HEADER_H, vw, MENUBAR_H);
if self.show_spreadsheet {
let viewport_h = body_h * 2.0 / 3.0;
let spreadsheet_h = body_h - viewport_h;
- self.positions[SPREADSHEET_MENUBAR_IDX] = (0.0, HEADER_H + viewport_h, viewport_w, MENUBAR_H);
- self.positions[SPREADSHEET_IDX] = (0.0, HEADER_H + viewport_h + MENUBAR_H, viewport_w, spreadsheet_h - MENUBAR_H);
+ self.positions[SPREADSHEET_MENUBAR_IDX] = (0.0, HEADER_H + viewport_h, vw, MENUBAR_H);
+ self.positions[SPREADSHEET_IDX] = (0.0, HEADER_H + viewport_h + MENUBAR_H, vw, spreadsheet_h - MENUBAR_H);
} else {
self.positions[SPREADSHEET_IDX] = (0.0, 0.0, 0.0, 0.0);
self.positions[SPREADSHEET_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
}
- } else {
- self.widgets[LEFT_MENUBAR_IDX].set_curved_circle(None);
- self.positions[CONTENT_IDX] = (0.0, node_area_y + BREADCRUMB_H, clw, self.height - STATUS_H - (node_area_y + BREADCRUMB_H));
- self.positions[SPLITTER1_IDX] = (self.splitter1_x, HEADER_H, SPLITTER_W, body_h);
- self.positions[SPLITTER2_IDX] = (self.splitter2_x, HEADER_H, SPLITTER_W, body_h);
- self.positions[PARAM_IDX] = (px, HEADER_H, self.param_w(), body_h);
- self.positions[VIEWPORT_IDX] = (crx, HEADER_H, vw, body_h);
- self.positions[RIGHT_MENUBAR_IDX] = (crx, HEADER_H, vw, MENUBAR_H);
- self.positions[BREADCRUMB_IDX] = (0.0, node_area_y, clw, BREADCRUMB_H);
- self.positions[LEFT_MENUBAR_IDX] = (0.0, HEADER_H, clw, MENUBAR_H);
-
- if self.show_spreadsheet {
- let viewport_h = body_h * 2.0 / 3.0;
- let spreadsheet_h = body_h - viewport_h;
- self.positions[SPREADSHEET_MENUBAR_IDX] = (crx, HEADER_H + viewport_h, vw, MENUBAR_H);
- self.positions[SPREADSHEET_IDX] = (crx, HEADER_H + viewport_h + MENUBAR_H, vw, spreadsheet_h - MENUBAR_H);
+ self.positions[CANVAS_IDX] = (0.0, HEADER_H, self.width, body_h);
+ self.positions[PARAM_MENUBAR_IDX] = (px, HEADER_H, param_w, MENUBAR_H);
+ self.positions[STATUS_IDX] = (0.0, self.height - STATUS_H, self.width, STATUS_H);
+ self.positions[NODE_PALETTE_IDX] = (0.0, 0.0, self.width, self.height);
+ } else {
+ self.positions[0] = (0.0, 0.0, self.width, HEADER_H);
+ if self.circular_network_pane {
+ let cx = self.circular_network_layout.x;
+ let cy = self.circular_network_layout.y;
+ let r = self.circular_network_layout.r;
+
+ self.positions[LEFT_MENUBAR_IDX] = (cx - r, cy - r, 2.0 * r, 35.0);
+ self.widgets[LEFT_MENUBAR_IDX].set_curved_circle(Some((cx, cy, r)));
+ self.positions[BREADCRUMB_IDX] = (cx - r, cy - r + 45.0 + MENUBAR_H, 2.0 * r, BREADCRUMB_H);
+ self.positions[CONTENT_IDX] = (cx - r, cy - r + 45.0 + MENUBAR_H + BREADCRUMB_H, 2.0 * r, 2.0 * r - (45.0 + MENUBAR_H + BREADCRUMB_H));
+ self.positions[SPLITTER1_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[SPLITTER2_IDX] = (self.splitter_layout.splitter2_x, HEADER_H, SPLITTER_W, body_h);
+ self.positions[PARAM_IDX] = (px, HEADER_H, self.param_w(), body_h);
+
+ let viewport_w = self.splitter_layout.splitter2_x;
+ self.positions[VIEWPORT_IDX] = (0.0, HEADER_H, viewport_w, body_h);
+ self.positions[RIGHT_MENUBAR_IDX] = (0.0, HEADER_H, viewport_w, MENUBAR_H);
+
+ if self.show_spreadsheet {
+ let viewport_h = body_h * 2.0 / 3.0;
+ let spreadsheet_h = body_h - viewport_h;
+ self.positions[SPREADSHEET_MENUBAR_IDX] = (0.0, HEADER_H + viewport_h, viewport_w, MENUBAR_H);
+ self.positions[SPREADSHEET_IDX] = (0.0, HEADER_H + viewport_h + MENUBAR_H, viewport_w, spreadsheet_h - MENUBAR_H);
+ } else {
+ self.positions[SPREADSHEET_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[SPREADSHEET_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
+ }
} else {
- self.positions[SPREADSHEET_IDX] = (0.0, 0.0, 0.0, 0.0);
- self.positions[SPREADSHEET_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.widgets[LEFT_MENUBAR_IDX].set_curved_circle(None);
+ self.positions[CONTENT_IDX] = (0.0, node_area_y + BREADCRUMB_H, clw, self.height - STATUS_H - (node_area_y + BREADCRUMB_H));
+ self.positions[SPLITTER1_IDX] = (self.splitter_layout.splitter1_x, HEADER_H, SPLITTER_W, body_h);
+ self.positions[SPLITTER2_IDX] = (self.splitter_layout.splitter2_x, HEADER_H, SPLITTER_W, body_h);
+ self.positions[PARAM_IDX] = (px, HEADER_H, self.param_w(), body_h);
+
+ self.positions[VIEWPORT_IDX] = (crx, HEADER_H, vw, body_h);
+ self.positions[RIGHT_MENUBAR_IDX] = (crx, HEADER_H, vw, MENUBAR_H);
+ self.positions[BREADCRUMB_IDX] = (0.0, node_area_y, clw, BREADCRUMB_H);
+ self.positions[LEFT_MENUBAR_IDX] = (0.0, HEADER_H, clw, MENUBAR_H);
+
+ if self.show_spreadsheet {
+ let viewport_h = body_h * 2.0 / 3.0;
+ let spreadsheet_h = body_h - viewport_h;
+ self.positions[SPREADSHEET_MENUBAR_IDX] = (crx, HEADER_H + viewport_h, vw, MENUBAR_H);
+ self.positions[SPREADSHEET_IDX] = (crx, HEADER_H + viewport_h + MENUBAR_H, vw, spreadsheet_h - MENUBAR_H);
+ } else {
+ self.positions[SPREADSHEET_IDX] = (0.0, 0.0, 0.0, 0.0);
+ self.positions[SPREADSHEET_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
+ }
}
+
+ self.positions[CANVAS_IDX] = (0.0, HEADER_H, self.width, body_h);
+ self.positions[PARAM_MENUBAR_IDX] = (px, HEADER_H, self.param_w(), MENUBAR_H);
+ self.positions[STATUS_IDX] = (0.0, self.height - STATUS_H, self.width, STATUS_H);
+ self.positions[NODE_PALETTE_IDX] = (0.0, 0.0, self.width, self.height);
}
- self.positions[CANVAS_IDX] = (0.0, HEADER_H, self.width, body_h);
- self.positions[PARAM_MENUBAR_IDX] = (px, HEADER_H, self.param_w(), MENUBAR_H);
- self.positions[STATUS_IDX] = (0.0, self.height - STATUS_H, self.width, STATUS_H);
- self.positions[CONFIG_DIALOG_IDX] = (0.0, 0.0, self.width, self.height);
- self.positions[NODE_PALETTE_IDX] = (0.0, 0.0, self.width, self.height);
+ if self.is_detached_network {
+ for i in 0..self.widgets.len() {
+ if i == LEFT_MENUBAR_IDX || i == BREADCRUMB_IDX || i == CONTENT_IDX {
+ self.widgets[i].set_visible(true);
+ } else if i == NODE_PALETTE_IDX {
+ // controlled dynamically
+ } else {
+ self.widgets[i].set_visible(false);
+ }
+ }
+ } else {
+ // Restore visibility for main window widgets
+ for i in 0..self.widgets.len() {
+ if i == LEFT_MENUBAR_IDX || i == BREADCRUMB_IDX || i == CONTENT_IDX {
+ self.widgets[i].set_visible(!self.detached_circular_network);
+ } else if i == SPREADSHEET_IDX || i == SPREADSHEET_MENUBAR_IDX {
+ self.widgets[i].set_visible(self.show_spreadsheet);
+ } else if i == NODE_PALETTE_IDX {
+ // controlled dynamically
+ } else {
+ self.widgets[i].set_visible(true);
+ }
+ }
+ }
}
@@ -3271,12 +2628,13 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
}
if self.focused_pane != PARAM_MENUBAR_IDX {
self.widgets[PARAM_IDX].unfocus();
+ self.sync_parameters_to_project();
}
}
fn sync_layout(&mut self) {
- self.splitter1_x = self.widgets[SPLITTER1_IDX].rect().0;
- self.splitter2_x = self.widgets[SPLITTER2_IDX].rect().0;
+ self.splitter_layout.splitter1_x = self.widgets[SPLITTER1_IDX].rect().0;
+ self.splitter_layout.splitter2_x = self.widgets[SPLITTER2_IDX].rect().0;
self.rebuild_positions();
self.apply_layout();
self.update_panel_bounds();
@@ -3311,26 +2669,200 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
settings_changed = true;
}
Action::ToggleConfigure => {
- if self.widgets[CONFIG_DIALOG_IDX].take_click() {
- self.widgets[CONFIG_DIALOG_IDX].set_visible(false);
- } else {
- self.widgets[CONFIG_DIALOG_IDX].set_config_toggle(0, self.grid_snap_enabled);
- self.widgets[CONFIG_DIALOG_IDX].set_config_toggle(1, self.network_grid_visible);
- 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);
- self.widgets[CONFIG_DIALOG_IDX].set_config_spin(3, self.skipped_col_w);
- 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);
+ use std::io::Write;
+ let json_config = serde_json::json!({
+ "width": 550,
+ "height": 550,
+ "pages": [
+ {
+ "title": "Network",
+ "widgets": [
+ { "type": "checkbox", "id": "grid_snap_enabled", "text": "Snap to Grid", "checked": self.grid_snap_enabled },
+ { "type": "checkbox", "id": "network_grid_visible", "text": "Network Grid Visible", "checked": self.network_grid_visible },
+ { "type": "spinbox", "id": "grid_size_x", "text": "Grid X", "value": self.grid_size_x as i32, "min": 10, "max": 200, "step": 5 },
+ { "type": "spinbox", "id": "grid_size_y", "text": "Grid Y", "value": self.grid_size_y as i32, "min": 5, "max": 100, "step": 5 },
+ { "type": "spinbox", "id": "skipped_row_h", "text": "Skipped Row H", "value": self.skipped_row_h as i32, "min": 0, "max": 150, "step": 5 },
+ { "type": "spinbox", "id": "skipped_col_w", "text": "Skipped Col W", "value": self.skipped_col_w as i32, "min": 0, "max": 150, "step": 5 },
+ { "type": "color", "id": "node_color", "text": "Node Base Color", "color": [
+ (self.node_color[0] * 255.0).round().clamp(0.0, 255.0) as u8,
+ (self.node_color[1] * 255.0).round().clamp(0.0, 255.0) as u8,
+ (self.node_color[2] * 255.0).round().clamp(0.0, 255.0) as u8,
+ ]},
+ { "type": "color", "id": "cell_color", "text": "Cell Color", "color": [
+ (self.cell_color[0] * 255.0).round().clamp(0.0, 255.0) as u8,
+ (self.cell_color[1] * 255.0).round().clamp(0.0, 255.0) as u8,
+ (self.cell_color[2] * 255.0).round().clamp(0.0, 255.0) as u8,
+ ]},
+ { "type": "color", "id": "gap_color", "text": "Gap Color", "color": [
+ (self.gap_color[0] * 255.0).round().clamp(0.0, 255.0) as u8,
+ (self.gap_color[1] * 255.0).round().clamp(0.0, 255.0) as u8,
+ (self.gap_color[2] * 255.0).round().clamp(0.0, 255.0) as u8,
+ ]},
+ { "type": "checkbox", "id": "uniform_background", "text": "Uniform Background", "checked": self.uniform_background },
+ { "type": "slider", "id": "network_opacity", "text": "Opacity", "value_f32": self.network_opacity, "min_f32": 0.0, "max_f32": 1.0 },
+ { "type": "button", "id": "save", "text": "Save Settings" },
+ { "type": "button", "id": "cancel", "text": "Cancel" }
+ ]
+ },
+ {
+ "title": "Viewport",
+ "widgets": [
+ { "type": "checkbox", "id": "show_grid", "text": "Show Grid Guide", "checked": self.show_grid },
+ { "type": "checkbox", "id": "show_cube", "text": "Show Reference Cube", "checked": self.show_cube },
+ { "type": "checkbox", "id": "show_origin", "text": "Show Origin Axes", "checked": self.show_origin },
+ { "type": "checkbox", "id": "show_camera_pivot", "text": "Show Camera Pivot", "checked": self.show_camera_pivot },
+ { "type": "spinbox", "id": "grid_thickness", "text": "Grid Thickness", "value": (self.grid_thickness * 1000.0).round() as i32, "min": 2, "max": 200, "step": 5, "decimals": 3 },
+ { "type": "spinbox", "id": "origin_size", "text": "Origin Guide Size", "value": (self.origin_size * 10.0).round() as i32, "min": 1, "max": 50, "step": 1, "decimals": 1 },
+ { "type": "spinbox", "id": "camera_pivot_size", "text": "Camera Pivot Size", "value": (self.camera_pivot_size * 10.0).round() as i32, "min": 1, "max": 50, "step": 1, "decimals": 1 },
+ { "type": "color", "id": "viewport_bg_color", "text": "Viewport Background", "color": [
+ (self.viewport_bg_color[0] * 255.0).round().clamp(0.0, 255.0) as u8,
+ (self.viewport_bg_color[1] * 255.0).round().clamp(0.0, 255.0) as u8,
+ (self.viewport_bg_color[2] * 255.0).round().clamp(0.0, 255.0) as u8,
+ ]},
+ { "type": "color", "id": "grid_color", "text": "Grid Guide Color", "color": [
+ (self.grid_color[0] * 255.0).round().clamp(0.0, 255.0) as u8,
+ (self.grid_color[1] * 255.0).round().clamp(0.0, 255.0) as u8,
+ (self.grid_color[2] * 255.0).round().clamp(0.0, 255.0) as u8,
+ ]},
+ { "type": "button", "id": "save", "text": "Save Settings" },
+ { "type": "button", "id": "cancel", "text": "Cancel" }
+ ]
+ },
+ {
+ "title": "Shortcuts",
+ "widgets": [
+ { "type": "label", "text": "Keyboard Shortcuts" },
+ { "type": "label", "text": " Ctrl+G : Toggle Grid (viewport)" },
+ { "type": "label", "text": " Ctrl+E : Toggle Cube" },
+ { "type": "label", "text": " Ctrl+A : Square Viewport Aspect" },
+ { "type": "label", "text": " Ctrl+, : Configure Dialog" },
+ { "type": "button", "id": "save", "text": "Save Settings" },
+ { "type": "button", "id": "cancel", "text": "Cancel" }
+ ]
+ }
+ ]
+ });
+
+ if let Ok(mut child) = std::process::Command::new("clear-cloud")
+ .arg("--layout")
+ .stdin(std::process::Stdio::piped())
+ .stdout(std::process::Stdio::piped())
+ .spawn()
+ {
+ if let Some(mut stdin) = child.stdin.take() {
+ let _ = stdin.write_all(json_config.to_string().as_bytes());
+ }
+ if let Ok(output) = child.wait_with_output() {
+ if output.status.success() {
+ let out_str = String::from_utf8_lossy(&output.stdout);
+ if let Ok(response) = serde_json::from_str::<serde_json::Value>(&out_str) {
+ if response["button"] == "save" {
+ if let Some(val) = response["checkboxes"]["grid_snap_enabled"].as_bool() {
+ self.grid_snap_enabled = val;
+ self.widgets[CONTENT_IDX].set_grid_snap_enabled(val);
+ }
+ if let Some(val) = response["checkboxes"]["network_grid_visible"].as_bool() {
+ self.network_grid_visible = val;
+ self.widgets[CONTENT_IDX].set_show_network_grid(val);
+ }
+ if let Some(val) = response["checkboxes"]["show_grid"].as_bool() {
+ self.show_grid = val;
+ self.widgets[RIGHT_MENUBAR_IDX].set_item_checked(2, 0, val);
+ }
+ if let Some(val) = response["checkboxes"]["show_cube"].as_bool() {
+ self.show_cube = val;
+ self.widgets[RIGHT_MENUBAR_IDX].set_item_checked(2, 1, val);
+ }
+ if let Some(val) = response["checkboxes"]["show_origin"].as_bool() {
+ self.show_origin = val;
+ self.widgets[RIGHT_MENUBAR_IDX].set_item_checked(2, 2, val);
+ }
+ if let Some(val) = response["checkboxes"]["show_camera_pivot"].as_bool() {
+ self.show_camera_pivot = val;
+ self.widgets[RIGHT_MENUBAR_IDX].set_item_checked(2, 3, val);
+ }
+ if let Some(val) = response["checkboxes"]["uniform_background"].as_bool() {
+ self.uniform_background = val;
+ }
+ if let Some(val) = response["sliders"]["network_opacity"].as_f64() {
+ self.network_opacity = val as f32;
+ }
+
+ if let Some(val) = response["spinboxes"]["grid_size_x"].as_f64() {
+ self.grid_size_x = val as f32;
+ }
+ if let Some(val) = response["spinboxes"]["grid_size_y"].as_f64() {
+ self.grid_size_y = val as f32;
+ }
+ if let Some(val) = response["spinboxes"]["skipped_row_h"].as_f64() {
+ self.skipped_row_h = val as f32;
+ }
+ if let Some(val) = response["spinboxes"]["skipped_col_w"].as_f64() {
+ self.skipped_col_w = val as f32;
+ }
+ if let Some(val) = response["spinboxes"]["origin_size"].as_f64() {
+ self.origin_size = val as f32 / 10.0;
+ }
+ if let Some(val) = response["spinboxes"]["grid_thickness"].as_f64() {
+ self.grid_thickness = val as f32 / 1000.0;
+ }
+ if let Some(val) = response["spinboxes"]["camera_pivot_size"].as_f64() {
+ self.camera_pivot_size = val as f32 / 10.0;
+ }
+
+ if let Some(arr) = response["colors"]["viewport_bg_color"].as_array() {
+ if arr.len() == 3 {
+ self.viewport_bg_color[0] = arr[0].as_f64().unwrap_or(0.0) as f32 / 255.0;
+ self.viewport_bg_color[1] = arr[1].as_f64().unwrap_or(0.0) as f32 / 255.0;
+ self.viewport_bg_color[2] = arr[2].as_f64().unwrap_or(0.0) as f32 / 255.0;
+ }
+ }
+
+ if let Some(arr) = response["colors"]["node_color"].as_array() {
+ if arr.len() == 3 {
+ self.node_color[0] = arr[0].as_f64().unwrap_or(0.0) as f32 / 255.0;
+ self.node_color[1] = arr[1].as_f64().unwrap_or(0.0) as f32 / 255.0;
+ self.node_color[2] = arr[2].as_f64().unwrap_or(0.0) as f32 / 255.0;
+ colors::set_node_color([self.node_color[0], self.node_color[1], self.node_color[2], 1.0]);
+ }
+ }
+
+ if let Some(arr) = response["colors"]["grid_color"].as_array() {
+ if arr.len() == 3 {
+ self.grid_color[0] = arr[0].as_f64().unwrap_or(0.0) as f32 / 255.0;
+ self.grid_color[1] = arr[1].as_f64().unwrap_or(0.0) as f32 / 255.0;
+ self.grid_color[2] = arr[2].as_f64().unwrap_or(0.0) as f32 / 255.0;
+ }
+ }
+
+ if let Some(arr) = response["colors"]["cell_color"].as_array() {
+ if arr.len() == 3 {
+ self.cell_color[0] = arr[0].as_f64().unwrap_or(0.0) as f32 / 255.0;
+ self.cell_color[1] = arr[1].as_f64().unwrap_or(0.0) as f32 / 255.0;
+ self.cell_color[2] = arr[2].as_f64().unwrap_or(0.0) as f32 / 255.0;
+ }
+ }
+
+ if let Some(arr) = response["colors"]["gap_color"].as_array() {
+ if arr.len() == 3 {
+ self.gap_color[0] = arr[0].as_f64().unwrap_or(0.0) as f32 / 255.0;
+ self.gap_color[1] = arr[1].as_f64().unwrap_or(0.0) as f32 / 255.0;
+ self.gap_color[2] = arr[2].as_f64().unwrap_or(0.0) as f32 / 255.0;
+ }
+ }
+
+ self.update_origin_geometry();
+ self.update_grid_geometry();
+ self.update_pivot_geometry();
+ self.sync_grid_settings();
+ self.save_settings();
+ self.sync_layout();
+ self.read_panel_offsets();
+ self.sync_cursor_and_selection();
+ }
+ }
+ }
+ }
}
self.upload_vertices();
return;
@@ -3354,6 +2886,26 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
self.apply_layout();
self.sync_grid_settings();
}
+ Action::DetachCircularWindow => {
+ let default_proj_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("default_project.json");
+ if let Err(e) = self.save_to_file(&default_proj_path) {
+ eprintln!("Failed to save default project before detaching: {:?}", e);
+ }
+
+ self.detached_circular_network = !self.detached_circular_network;
+ self.widgets[LEFT_MENUBAR_IDX].set_item_checked(2, 3, self.detached_circular_network);
+ self.widgets[HEADER_IDX].set_item_checked(2, 3, self.detached_circular_network);
+
+ if self.detached_circular_network {
+ let _ = std::process::Command::new(std::env::current_exe().unwrap())
+ .arg("--detached-network")
+ .spawn();
+ }
+
+ self.rebuild_positions();
+ self.apply_layout();
+ self.sync_grid_settings();
+ }
}
if settings_changed {
self.save_settings();
@@ -3411,23 +2963,23 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
let mut verts = Vec::new();
let node_area_y = HEADER_H + MENUBAR_H + BREADCRUMB_H;
- let dialog_open = self.widgets[CONFIG_DIALOG_IDX].visible() || self.node_palette_visible;
+ let dialog_open = self.node_palette_visible;
let show_cursor = self.drag_widget.is_none()
&& !dialog_open;
let clip = if self.circular_network_pane {
(
- self.network_circle_x - self.network_circle_radius,
- self.network_circle_y - self.network_circle_radius,
- self.network_circle_x + self.network_circle_radius,
- self.network_circle_y + self.network_circle_radius,
+ self.circular_network_layout.x - self.circular_network_layout.r,
+ self.circular_network_layout.y - self.circular_network_layout.r,
+ self.circular_network_layout.x + self.circular_network_layout.r,
+ self.circular_network_layout.y + self.circular_network_layout.r,
)
} else {
(0.0, node_area_y, self.content_left_w(), self.height - STATUS_H)
};
let clip_circle_val = if self.circular_network_pane {
- [self.network_circle_x * self.scale as f32, self.network_circle_y * self.scale as f32, self.network_circle_radius * self.scale as f32]
+ [self.circular_network_layout.x * self.scale as f32, self.circular_network_layout.y * self.scale as f32, self.circular_network_layout.r * self.scale as f32]
} else {
[0.0, 0.0, 0.0]
};
@@ -3437,18 +2989,21 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
for &i in &draw_order {
let w = &self.widgets[i];
+ if !w.visible() {
+ continue;
+ }
let is_network_part = i == CONTENT_IDX || i == LEFT_MENUBAR_IDX || i == BREADCRUMB_IDX;
let active_clip_circle = if is_network_part { clip_circle_val } else { [0.0, 0.0, 0.0] };
if i == CONTENT_IDX {
if self.circular_network_pane {
verts.extend(circle_vertices(
- self.network_circle_x,
- self.network_circle_y,
- self.network_circle_radius,
+ self.circular_network_layout.x,
+ self.circular_network_layout.y,
+ self.circular_network_layout.r,
sw,
sh,
- [0.10, 0.10, 0.13, 0.95],
+ [0.10, 0.10, 0.13, self.network_opacity],
64,
active_clip_circle,
));
@@ -3462,21 +3017,21 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
if self.circular_network_pane {
verts.extend(circle_border_vertices(
- self.network_circle_x,
- self.network_circle_y,
- self.network_circle_radius,
+ self.circular_network_layout.x,
+ self.circular_network_layout.y,
+ self.circular_network_layout.r,
3.0,
sw,
sh,
- [0.35, 0.65, 0.95, 0.80],
+ [0.35, 0.65, 0.95, 0.80 * self.network_opacity],
64,
active_clip_circle,
));
}
} else if i == LEFT_MENUBAR_IDX && self.circular_network_pane {
- let cx = self.network_circle_x;
- let cy = self.network_circle_y;
- let r = self.network_circle_radius;
+ let cx = self.circular_network_layout.x;
+ let cy = self.circular_network_layout.y;
+ let r = self.circular_network_layout.r;
let bg_color = w.color();
verts.extend(arc_background_vertices(
@@ -3490,7 +3045,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
active_clip_circle,
));
- let border_color = [0.22, 0.22, 0.28, 0.90];
+ let border_color = [0.22, 0.22, 0.28, 0.90 * self.network_opacity];
verts.extend(arc_background_vertices(
cx, cy, r - MENUBAR_H,
1.5,
@@ -3536,14 +3091,14 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
if i == CONTENT_IDX && show_cursor {
- let cx = active_clip_circle[0] / self.scale as f32 - self.network_circle_radius + self.pan_x; // Wait, let's keep the exact cursor coordinates!
+ let cx = active_clip_circle[0] / self.scale as f32 - self.circular_network_layout.r + self.pan_x; // Wait, let's keep the exact cursor coordinates!
let active_node_area_y = if self.circular_network_pane {
- self.network_circle_y - self.network_circle_radius + 45.0 + MENUBAR_H + BREADCRUMB_H
+ self.circular_network_layout.y - self.circular_network_layout.r + 45.0 + MENUBAR_H + BREADCRUMB_H
} else {
node_area_y
};
let active_node_area_x = if self.circular_network_pane {
- self.network_circle_x - self.network_circle_radius
+ self.circular_network_layout.x - self.circular_network_layout.r
} else {
0.0
};
@@ -3648,65 +3203,68 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
}
fn update_status_text(&mut self, text: &str) {
- self.status_buffer = make_text_buffer(&mut self.font_system, text, 12.0);
+ self.widgets[STATUS_IDX].set_text(text);
}
fn prepare_text(&mut self) {
- let splitter1_x = self.splitter1_x;
+ // 1. Prepare text on all widgets using self.font_system
+ for w in &mut self.widgets {
+ w.prepare_text(&mut self.font_system);
+ }
+
+ // 2. Destructure self
+ let splitter1_x = self.splitter_layout.splitter1_x;
let height = self.height;
+ let sw = self.width;
+ let sh = self.height;
+ let circular_network_pane = self.circular_network_pane;
+ let network_circle_x = self.circular_network_layout.x;
+ let network_circle_y = self.circular_network_layout.y;
+ let network_circle_radius = self.circular_network_layout.r;
+
let Self {
ref mut text_renderer, ref device, ref queue,
ref mut font_system, ref mut text_atlas, ref mut text_viewport,
- ref mut swash_cache, ref status_buffer,
+ ref mut swash_cache,
physical_width, physical_height, scale,
- ref widgets, ..
+ ref widgets,
+ ref curved_text_texture,
+ ref mut curved_text_atlas,
+ ref mut curved_text_renderer,
+ ref mut curved_text_viewport,
+ ref mut textured_vertex_buffer,
+ ref mut textured_vertex_count,
+ ..
} = self;
let viewport = Resolution { width: *physical_width, height: *physical_height };
text_viewport.update(queue, viewport);
let s = *scale as f32;
- let mut areas: Vec<TextArea> = vec![
- TextArea {
- buffer: status_buffer,
- left: 12.0 * s, top: *physical_height as f32 - 24.0 * s, scale: s,
- bounds: TextBounds { left: 0, top: 0, right: *physical_width as i32, bottom: *physical_height as i32 },
- default_color: glyphon::Color::rgb(0xaa, 0xaa, 0xbb),
- custom_glyphs: &[],
- },
- ];
+ let mut areas: Vec<TextArea> = Vec::new();
+
+ // Temporary storage for legacy buffers generated during this frame
+ let mut legacy_buffers: Vec<Buffer> = Vec::new();
+ let mut legacy_labels: Vec<TextLabel> = Vec::new();
+ let mut legacy_bounds: Vec<TextBounds> = Vec::new();
+
+ let mut curved_labels = Vec::new();
- let mut widget_buffers: Vec<Buffer> = Vec::new();
- let mut widget_labels: Vec<TextLabel> = Vec::new();
- let mut widget_is_node: Vec<bool> = Vec::new();
for (i, w) in widgets.iter().enumerate() {
+ if !w.visible() {
+ continue;
+ }
let is_node = i == CONTENT_IDX;
let is_network_part = i == CONTENT_IDX || i == LEFT_MENUBAR_IDX || i == BREADCRUMB_IDX;
- for label in w.text_labels() {
- if self.circular_network_pane && is_network_part && i != LEFT_MENUBAR_IDX {
- let dx = label.x - self.network_circle_x;
- let dy = label.y - self.network_circle_y;
- let dist_sq = dx * dx + dy * dy;
- if dist_sq > self.network_circle_radius * self.network_circle_radius {
- continue;
- }
- }
- widget_buffers.push(make_text_buffer(font_system, &label.text, label.font_size));
- widget_labels.push(label);
- widget_is_node.push(is_node);
- }
- }
-
- for ((buf, label), &is_node) in widget_buffers.iter().zip(widget_labels.iter()).zip(widget_is_node.iter()) {
let bounds = if is_node {
let node_area_y = HEADER_H + MENUBAR_H + BREADCRUMB_H;
- if self.circular_network_pane {
+ if circular_network_pane {
TextBounds {
- left: ((self.network_circle_x - self.network_circle_radius) * s) as i32,
- top: ((self.network_circle_y - self.network_circle_radius) * s) as i32,
- right: ((self.network_circle_x + self.network_circle_radius) * s) as i32,
- bottom: (((self.network_circle_y + self.network_circle_radius) * s) as i32).max(0),
+ left: ((network_circle_x - network_circle_radius) * s) as i32,
+ top: ((network_circle_y - network_circle_radius) * s) as i32,
+ right: ((network_circle_x + network_circle_radius) * s) as i32,
+ bottom: (((network_circle_y + network_circle_radius) * s) as i32).max(0),
}
} else {
TextBounds {
@@ -3717,25 +3275,270 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
}
}
} else {
- TextBounds {
- left: 0,
- top: 0,
- right: *physical_width as i32,
- bottom: *physical_height as i32,
- }
+ TextBounds {
+ left: 0,
+ top: 0,
+ right: *physical_width as i32,
+ bottom: *physical_height as i32,
+ }
+ };
+
+ let cached_items = w.get_text_items();
+ // if i == LEFT_MENUBAR_IDX {
+ // eprintln!("DEBUG_PREPARE: i={} cached_items.len={} circular_network_pane={}", i, cached_items.len(), circular_network_pane);
+ // }
+ if !cached_items.is_empty() && !(circular_network_pane && i == LEFT_MENUBAR_IDX) {
+ for (buf, x, y, color) in cached_items {
+ if circular_network_pane && is_network_part && i != LEFT_MENUBAR_IDX {
+ let dx = x - network_circle_x;
+ let dy = y - network_circle_y;
+ let dist_sq = dx * dx + dy * dy;
+ if dist_sq > network_circle_radius * network_circle_radius {
+ continue;
+ }
+ }
+ areas.push(TextArea {
+ buffer: buf,
+ left: x * s,
+ top: y * s,
+ scale: s,
+ bounds,
+ default_color: color,
+ custom_glyphs: &[],
+ });
+ }
+ } else {
+ let labels = w.text_labels();
+ // if i == LEFT_MENUBAR_IDX {
+ // eprintln!("DEBUG_PREPARE_ELSE: i={} labels.len={}", i, labels.len());
+ // }
+ for label in labels {
+ let mut is_curved = false;
+ if circular_network_pane && i == LEFT_MENUBAR_IDX && label.text.chars().count() == 1 {
+ let dx = label.x - network_circle_x;
+ let dy = label.y - network_circle_y;
+ let dist = (dx * dx + dy * dy).sqrt();
+ // eprintln!("DEBUG_CURVED: label='{}' count={} dist={} req_min={} req_max={}", label.text, label.text.chars().count(), dist, network_circle_radius - 35.0, network_circle_radius + 5.0);
+ if dist >= network_circle_radius - 35.0 && dist <= network_circle_radius + 5.0 {
+ is_curved = true;
+ }
+ }
+
+ if is_curved {
+ curved_labels.push(label);
+ } else {
+ if circular_network_pane && is_network_part && i != LEFT_MENUBAR_IDX {
+ let dx = label.x - network_circle_x;
+ let dy = label.y - network_circle_y;
+ let dist_sq = dx * dx + dy * dy;
+ if dist_sq > network_circle_radius * network_circle_radius {
+ continue;
+ }
+ }
+ legacy_buffers.push(make_text_buffer(font_system, &label.text, label.font_size));
+ legacy_labels.push(label);
+ legacy_bounds.push(bounds);
+ }
+ }
+ }
+ }
+
+ // Add the legacy buffered items (references are safe now that legacy_buffers is not reallocated)
+ for ((buf, label), bounds) in legacy_buffers.iter().zip(legacy_labels.iter()).zip(legacy_bounds.iter()) {
+ areas.push(TextArea {
+ buffer: buf,
+ left: label.x * s,
+ top: label.y * s,
+ scale: s,
+ bounds: *bounds,
+ default_color: glyphon::Color::rgb(label.color[0], label.color[1], label.color[2]),
+ custom_glyphs: &[],
+ });
+ }
+
+ // for label in &legacy_labels {
+ // if label.text.len() == 1 || label.text.contains("Network") || label.text.contains("File") || label.text.contains("Edit") || label.text.contains("View") {
+ // eprintln!("DEBUG_LEGACY_LABEL: text='{}' x={} y={}", label.text, label.x, label.y);
+ // }
+ // }
+
+ text_renderer.prepare(device, queue, font_system, text_atlas, text_viewport, areas, swash_cache).unwrap();
+
+ // Process curved labels
+ let mut textured_verts = Vec::new();
+
+ if !curved_labels.is_empty() {
+ struct CurvedDrawInfo {
+ label: TextLabel,
+ tx: f32,
+ ty: f32,
+ tw: f32,
+ th: f32,
+ buffer: Buffer,
+ }
+
+ let mut curved_draws = Vec::new();
+ let mut current_x = 4.0;
+ let mut current_y = 4.0;
+ let font_size = 12.0;
+ let row_height = (font_size + 8.0) * s;
+
+ for label in curved_labels {
+ let char_w = TextLabel::estimate_width(&label.text, font_size);
+ let physical_w = char_w * s;
+ if current_x + physical_w + 4.0 > 1024.0 {
+ current_x = 4.0;
+ current_y += row_height;
+ }
+ let buf = make_text_buffer(font_system, &label.text, font_size);
+ curved_draws.push(CurvedDrawInfo {
+ label: label.clone(),
+ tx: current_x,
+ ty: current_y,
+ tw: char_w,
+ th: font_size,
+ buffer: buf,
+ });
+ current_x += physical_w + 8.0 * s;
+ }
+
+ let mut curved_areas = Vec::new();
+ for draw in &curved_draws {
+ curved_areas.push(TextArea {
+ buffer: &draw.buffer,
+ left: draw.tx,
+ top: draw.ty,
+ scale: s,
+ bounds: TextBounds {
+ left: 0,
+ top: 0,
+ right: 1024,
+ bottom: 1024,
+ },
+ default_color: glyphon::Color::rgb(255, 255, 255),
+ custom_glyphs: &[],
+ });
+ }
+
+ curved_text_renderer.prepare(
+ device,
+ queue,
+ font_system,
+ curved_text_atlas,
+ curved_text_viewport,
+ curved_areas,
+ swash_cache,
+ ).unwrap();
+
+ let mut texture_encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
+ label: Some("Curved Text Texture Encoder"),
+ });
+ {
+ let view_for_pass = curved_text_texture.create_view(&wgpu::TextureViewDescriptor::default());
+ let mut pass = texture_encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some("Curved Text Render Pass"),
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+ view: &view_for_pass,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }),
+ store: wgpu::StoreOp::Store,
+ },
+ })],
+ depth_stencil_attachment: None,
+ timestamp_writes: None,
+ occlusion_query_set: None,
+ });
+ curved_text_renderer.render(curved_text_atlas, curved_text_viewport, &mut pass).unwrap();
+ }
+ queue.submit(std::iter::once(texture_encoder.finish()));
+
+ let clip_circle_val = if circular_network_pane {
+ [network_circle_x * s, network_circle_y * s, network_circle_radius * s]
+ } else {
+ [0.0, 0.0, 0.0]
};
- areas.push(TextArea {
- buffer: buf,
- left: label.x * s, top: label.y * s, scale: s,
- bounds,
- default_color: glyphon::Color::rgb(label.color[0], label.color[1], label.color[2]),
- custom_glyphs: &[],
- });
+
+ for draw in curved_draws {
+ let dx = (draw.label.x + draw.tw / 2.0) - network_circle_x;
+ let dy = (draw.label.y + draw.th / 2.0) - network_circle_y;
+ let theta = dy.atan2(dx);
+ let angle = theta + std::f32::consts::FRAC_PI_2;
+
+ let cx = draw.label.x + draw.tw / 2.0;
+ let cy = draw.label.y + draw.th / 2.0;
+ let w_half = draw.tw / 2.0;
+ let h_half = draw.th / 2.0;
+
+ let cos_a = angle.cos();
+ let sin_a = angle.sin();
+
+ let local_pts = [
+ [-w_half, -h_half],
+ [w_half, -h_half],
+ [-w_half, h_half],
+ [w_half, h_half],
+ ];
+
+ let mut screen_pts = [[0.0; 2]; 4];
+ for (k, pt) in local_pts.iter().enumerate() {
+ let rx = pt[0] * cos_a - pt[1] * sin_a;
+ let ry = pt[0] * sin_a + pt[1] * cos_a;
+ screen_pts[k] = [cx + rx, cy + ry];
+ }
+
+ let ndc_pts = screen_pts.map(|pt| [
+ (pt[0] / sw) * 2.0 - 1.0,
+ 1.0 - (pt[1] / sh) * 2.0,
+ ]);
+
+ // eprintln!("DEBUG_NDCPTS: char='{}' ndc0={:?} ndc1={:?} ndc2={:?} ndc3={:?}",
+ // draw.label.text, ndc_pts[0], ndc_pts[1], ndc_pts[2], ndc_pts[3]);
+
+ let u0 = draw.tx / 1024.0;
+ let v0 = draw.ty / 1024.0;
+ let u1 = (draw.tx + draw.tw * s) / 1024.0;
+ let v1 = (draw.ty + draw.th * s) / 1024.0;
+
+ let c = [
+ draw.label.color[0] as f32 / 255.0,
+ draw.label.color[1] as f32 / 255.0,
+ draw.label.color[2] as f32 / 255.0,
+ 1.0,
+ ];
+
+ let v_tl = TexturedVertex { position: ndc_pts[0], tex_coords: [u0, v0], color: c, clip_circle: clip_circle_val };
+ let v_tr = TexturedVertex { position: ndc_pts[1], tex_coords: [u1, v0], color: c, clip_circle: clip_circle_val };
+ let v_bl = TexturedVertex { position: ndc_pts[2], tex_coords: [u0, v1], color: c, clip_circle: clip_circle_val };
+ let v_br = TexturedVertex { position: ndc_pts[3], tex_coords: [u1, v1], color: c, clip_circle: clip_circle_val };
+
+ textured_verts.push(v_tl);
+ textured_verts.push(v_tr);
+ textured_verts.push(v_bl);
+
+ textured_verts.push(v_tr);
+ textured_verts.push(v_br);
+ textured_verts.push(v_bl);
+ }
}
- text_renderer.prepare(device, queue, font_system, text_atlas, text_viewport, areas, swash_cache).unwrap();
+ *textured_vertex_count = textured_verts.len() as u32;
+ if *textured_vertex_count > 0 {
+ let data = bytemuck::cast_slice(&textured_verts);
+ let needed = data.len() as wgpu::BufferAddress;
+ if needed > textured_vertex_buffer.size() {
+ *textured_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("Textured Vertex Buffer"),
+ size: needed,
+ usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+ }
+ queue.write_buffer(textured_vertex_buffer, 0, data);
+ }
}
+
fn resize(&mut self, width: u32, height: u32) {
if width > 0 && height > 0 {
let old_width = self.width;
@@ -3753,11 +3556,10 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
if old_width > 0.0 {
let r = self.width / old_width;
- self.splitter1_x *= r;
- self.splitter2_x *= r;
+ self.splitter_layout.scale(r);
let body_h = self.body_h();
- self.widgets[SPLITTER1_IDX].set_rect(self.splitter1_x, HEADER_H, SPLITTER_W, body_h);
- self.widgets[SPLITTER2_IDX].set_rect(self.splitter2_x, HEADER_H, SPLITTER_W, body_h);
+ self.widgets[SPLITTER1_IDX].set_rect(self.splitter_layout.splitter1_x, HEADER_H, SPLITTER_W, body_h);
+ self.widgets[SPLITTER2_IDX].set_rect(self.splitter_layout.splitter2_x, HEADER_H, SPLITTER_W, body_h);
}
self.sync_layout();
@@ -3770,15 +3572,13 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
fn handle_event(&mut self, event: &WindowEvent) -> bool {
match event {
WindowEvent::MouseWheel { delta, phase, .. } => {
+ let dialog_open = self.node_palette_visible;
+ let in_network_pane = self.in_network_pane();
+ eprintln!("DEBUG MOUSEWHEEL: delta={:?}, phase={:?}, cursor=({}, {}), in_network_pane={}", delta, phase, self.cursor_x, self.cursor_y, in_network_pane);
let node_area_y = HEADER_H + MENUBAR_H + BREADCRUMB_H;
- let dialog_open = self.widgets[CONFIG_DIALOG_IDX].visible() || self.node_palette_visible;
- let in_network_pane = self.cursor_x >= 0.0
- && self.cursor_x < self.content_left_w()
- && self.cursor_y >= node_area_y
- && self.cursor_y < self.height - STATUS_H;
let in_viewport = self.cursor_x >= self.content_right_x()
- && self.cursor_x < self.splitter2_x
+ && self.cursor_x < self.splitter_layout.splitter2_x
&& self.cursor_y >= node_area_y
&& self.cursor_y < self.height - STATUS_H;
@@ -3793,7 +3593,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
} 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 {
+ } else if self.cursor_x > self.splitter_layout.splitter2_x + SPLITTER_W && self.cursor_y >= node_area_y && self.cursor_y < self.height - STATUS_H {
new_pane = Some(PARAM_MENUBAR_IDX);
}
}
@@ -3806,43 +3606,64 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
}
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 {
+ let mut needs_sync_grid = false;
+ if !dialog_open && !self.modifiers.control_key() {
+ for (i, w) in self.widgets.iter_mut().enumerate() {
if w.mouse_wheel(delta, self.cursor_x, self.cursor_y) {
handled = true;
- }
- }
- }
+ if i == CONTENT_IDX {
+ let node_area_y = HEADER_H + MENUBAR_H + BREADCRUMB_H;
+ let active_node_area_y = if self.circular_network_pane {
+ self.circular_network_layout.y - self.circular_network_layout.r + 45.0 + MENUBAR_H + BREADCRUMB_H
+ } else {
+ node_area_y
+ };
+ let active_node_area_x = if self.circular_network_pane {
+ self.circular_network_layout.x - self.circular_network_layout.r
+ } else {
+ 0.0
+ };
+ let (gx, gy) = w.grid_origin();
+ let prev_pan_x = self.pan_x;
+ let prev_pan_y = self.pan_y;
+ self.pan_x = gx - active_node_area_x;
+ self.pan_y = gy - active_node_area_y;
+
+ let dx = prev_pan_x - self.pan_x;
+ let dy = prev_pan_y - self.pan_y;
+
+ let dt_scroll = Instant::now().duration_since(self.last_frame).as_secs_f32().min(0.1);
+ let vel_x = if dt_scroll > 1e-4 { -dx / dt_scroll } else { -dx * 60.0 };
+ let vel_y = if dt_scroll > 1e-4 { -dy / dt_scroll } else { -dy * 60.0 };
+ self.pan_velocity_x = self.pan_velocity_x * 0.4 + vel_x * 0.6;
+ self.pan_velocity_y = self.pan_velocity_y * 0.4 + vel_y * 0.6;
- let result = if handled {
- if self.focused_widget == Some(CONTENT_IDX) {
- if let Some(slot_idx) = self.widgets[CONTENT_IDX].selected_node() {
- let updated_params = self.widgets[PARAM_IDX].node_params();
- let dir = self.current_dir_mut();
- if let Some(child) = dir.children.get_mut(slot_idx) {
- let mut param_changed = false;
- for (u_name, u_val, _type) in &updated_params {
- if let Some(p) = child.params.iter_mut().find(|p| p.name == *u_name) {
- if p.default != *u_val {
- p.default = u_val.clone();
- param_changed = true;
- }
+ self.is_scrolling_trackpad = match delta {
+ MouseScrollDelta::LineDelta(_, _) => false,
+ MouseScrollDelta::PixelDelta(_) => match phase {
+ TouchPhase::Started | TouchPhase::Moved => true,
+ TouchPhase::Ended | TouchPhase::Cancelled => false,
}
+ };
+ if self.is_scrolling_trackpad {
+ self.last_scroll_time = Instant::now();
+ self.scroll_accum_x += dx;
+ self.scroll_accum_y += dy;
}
- if param_changed {
- self.rebuild_scene_geometry();
- self.sync_nodes();
- }
+ needs_sync_grid = true;
}
}
}
+ }
+ if needs_sync_grid {
+ self.sync_grid_settings();
+ }
+
+ let result = if handled {
+ self.sync_parameters_to_project();
// Sync Parameters pane with selected node
- let params = if self.focused_widget == Some(CONTENT_IDX) {
+ let params = if !self.is_detached_network {
if let Some(slot_idx) = self.widgets[CONTENT_IDX].selected_node() {
let dir = self.current_dir();
if slot_idx < dir.children.len() {
@@ -3883,8 +3704,8 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
} else {
match delta {
MouseScrollDelta::LineDelta(x, y) => {
- let dx = *x * 30.0;
- let dy = *y * 30.0;
+ let dx = *x * 30.0 * self.scroll_speed;
+ let dy = *y * 30.0 * self.scroll_speed;
self.pan_x -= dx;
self.pan_y -= dy;
self.is_scrolling_trackpad = false;
@@ -3897,8 +3718,8 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
true
}
MouseScrollDelta::PixelDelta(pos) => {
- let dx = pos.x as f32 / self.scale as f32;
- let dy = pos.y as f32 / self.scale as f32;
+ let dx = (pos.x as f32 / self.scale as f32) * self.scroll_speed;
+ let dy = (pos.y as f32 / self.scale as f32) * self.scroll_speed;
self.pan_x -= dx;
self.pan_y -= dy;
self.is_scrolling_trackpad = match phase {
@@ -3949,8 +3770,12 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
self.scroll_lock = 0;
let dx = *x * 0.05;
let dy = *y * 0.05;
- self.rotation_y += dx;
- self.rotation_x -= dy;
+ if self.active_camera != "Default Camera" {
+ self.update_active_camera_rotation(dx, -dy);
+ } else {
+ self.rotation_y += dx;
+ self.rotation_x -= dy;
+ }
self.is_rotating_viewport = false;
let dt_scroll = Instant::now().duration_since(self.last_frame).as_secs_f32().min(0.1);
@@ -3991,8 +3816,12 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
}
}
- self.rotation_y += dx;
- self.rotation_x -= dy;
+ if self.active_camera != "Default Camera" {
+ self.update_active_camera_rotation(dx, -dy);
+ } else {
+ self.rotation_y += dx;
+ self.rotation_x -= dy;
+ }
self.is_rotating_viewport = match phase {
TouchPhase::Started | TouchPhase::Moved => true,
@@ -4021,12 +3850,8 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
}
}
WindowEvent::PinchGesture { delta, .. } => {
- let node_area_y = HEADER_H + MENUBAR_H + BREADCRUMB_H;
- let dialog_open = self.widgets[CONFIG_DIALOG_IDX].visible() || self.node_palette_visible;
- let in_network_pane = self.cursor_x >= 0.0
- && self.cursor_x < self.content_left_w()
- && self.cursor_y >= node_area_y
- && self.cursor_y < self.height - STATUS_H;
+ let dialog_open = self.node_palette_visible;
+ let in_network_pane = self.in_network_pane();
if !dialog_open && in_network_pane {
if delta.is_finite() && *delta != 0.0 {
@@ -4048,8 +3873,8 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
let mut changed = false;
if self.is_dragging_network_circle {
- self.network_circle_x = self.cursor_x - self.circle_drag_ox;
- self.network_circle_y = self.cursor_y - self.circle_drag_oy;
+ self.circular_network_layout.x = self.cursor_x - self.circle_drag_ox;
+ self.circular_network_layout.y = self.cursor_y - self.circle_drag_oy;
self.rebuild_positions();
self.apply_layout();
self.sync_grid_settings();
@@ -4076,17 +3901,12 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
let (cx, cy) = (self.cursor_x, self.cursor_y);
let is_network_part = i == CONTENT_IDX || i == LEFT_MENUBAR_IDX || i == BREADCRUMB_IDX;
let inside = if self.circular_network_pane && is_network_part {
- let cx_c = self.network_circle_x;
- let cy_c = self.network_circle_y;
- let r = self.network_circle_radius;
if i == CONTENT_IDX {
- let dx = cx - cx_c;
- let dy = cy - cy_c;
- dx * dx + dy * dy <= r * r && cy >= cy_c - r + 45.0 + MENUBAR_H + BREADCRUMB_H
+ self.circular_network_layout.hit_test_content(cx, cy, MENUBAR_H, BREADCRUMB_H)
} else if i == LEFT_MENUBAR_IDX {
- self.widgets[LEFT_MENUBAR_IDX].hit_test(cx, cy)
+ self.circular_network_layout.hit_test_menubar(cx, cy, MENUBAR_H)
} else if i == BREADCRUMB_IDX {
- cx >= cx_c - r && cx <= cx_c + r && cy >= cy_c - r + 45.0 + MENUBAR_H && cy <= cy_c - r + 45.0 + MENUBAR_H + BREADCRUMB_H
+ self.circular_network_layout.hit_test_breadcrumb(cx, cy, MENUBAR_H, BREADCRUMB_H)
} else {
false
}
@@ -4122,12 +3942,9 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
self.is_zooming_viewport = false;
self.zoom_accum = 0.0;
}
+ let dialog_open = self.node_palette_visible;
+ let in_network_pane = self.in_network_pane();
let node_area_y = HEADER_H + MENUBAR_H + BREADCRUMB_H;
- let dialog_open = self.widgets[CONFIG_DIALOG_IDX].visible() || self.node_palette_visible;
- let in_network_pane = self.cursor_x >= 0.0
- && self.cursor_x < self.content_left_w()
- && self.cursor_y >= node_area_y
- && self.cursor_y < self.height - STATUS_H;
let is_pan_trigger = !dialog_open
&& in_network_pane
@@ -4174,17 +3991,12 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
let hits_widget = |state: &State, i: usize, x: f32, y: f32| -> bool {
if state.circular_network_pane && (i == CONTENT_IDX || i == LEFT_MENUBAR_IDX || i == BREADCRUMB_IDX) {
- let cx_c = state.network_circle_x;
- let cy_c = state.network_circle_y;
- let r = state.network_circle_radius;
if i == CONTENT_IDX {
- let dx = x - cx_c;
- let dy = y - cy_c;
- dx * dx + dy * dy <= r * r && y >= cy_c - r + 45.0 + MENUBAR_H + BREADCRUMB_H
+ state.circular_network_layout.hit_test_content(x, y, MENUBAR_H, BREADCRUMB_H)
} else if i == LEFT_MENUBAR_IDX {
- state.widgets[LEFT_MENUBAR_IDX].hit_test(x, y)
+ state.circular_network_layout.hit_test_menubar(x, y, MENUBAR_H)
} else if i == BREADCRUMB_IDX {
- x >= cx_c - r && x <= cx_c + r && y >= cy_c - r + 45.0 + MENUBAR_H && y <= cy_c - r + 45.0 + MENUBAR_H + BREADCRUMB_H
+ state.circular_network_layout.hit_test_breadcrumb(x, y, MENUBAR_H, BREADCRUMB_H)
} else {
false
}
@@ -4194,10 +4006,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
};
let in_circle_network_pane = if self.circular_network_pane {
- let dx = self.cursor_x - self.network_circle_x;
- let dy = self.cursor_y - self.network_circle_y;
- let r = self.network_circle_radius;
- dx * dx + dy * dy <= r * r && self.cursor_y >= self.network_circle_y - r + 45.0 + MENUBAR_H + BREADCRUMB_H
+ self.circular_network_layout.hit_test_content(self.cursor_x, self.cursor_y, MENUBAR_H, BREADCRUMB_H)
} else {
in_network_pane
};
@@ -4206,41 +4015,47 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
ElementState::Pressed => {
println!("DEBUG: Mouse Pressed button={:?} position=({}, {})", button, self.cursor_x, self.cursor_y);
let _ = std::io::Write::flush(&mut std::io::stdout());
+
+ let hits_any_menu = (0..self.widgets.len()).any(|i| {
+ hits_widget(self, i, self.cursor_x, self.cursor_y)
+ && self.widgets[i].get_menu_items_at(self.cursor_x, self.cursor_y).is_some()
+ });
+
+ if !hits_any_menu {
+ if let Some(pid) = self.active_menu_cloud_pid {
+ let is_running = unsafe {
+ libc::kill(pid as libc::pid_t, 0) == 0
+ };
+ if is_running {
+ unsafe {
+ libc::kill(pid as libc::pid_t, libc::SIGTERM);
+ }
+ }
+ self.active_menu_cloud_pid = None;
+ self.active_menu_cloud_idx = None;
+ }
+ }
+
if *button == MouseButton::Left && self.circular_network_pane {
- let dx = self.cursor_x - self.network_circle_x;
- let dy = self.cursor_y - self.network_circle_y;
- let dist = (dx * dx + dy * dy).sqrt();
- let on_border = dist >= self.network_circle_radius - 12.0 && dist <= self.network_circle_radius;
- let hit_menubar = self.widgets[LEFT_MENUBAR_IDX].hit_test(self.cursor_x, self.cursor_y);
- let mut clicked_menu_item = false;
+ let on_border = self.circular_network_layout.hit_test_border(self.cursor_x, self.cursor_y, 12.0);
+ let hit_menubar = self.circular_network_layout.hit_test_menubar(self.cursor_x, self.cursor_y, MENUBAR_H);
if hit_menubar {
- clicked_menu_item = self.widgets[LEFT_MENUBAR_IDX].mouse_input(*button, *btn_state, self.cursor_x, self.cursor_y);
+ if let Some((menu_idx, title, items, rx, ry, rw, rh)) = self.widgets[LEFT_MENUBAR_IDX].get_menu_items_at(self.cursor_x, self.cursor_y) {
+ self.spawn_menu_cloud(LEFT_MENUBAR_IDX, menu_idx, title, items, rx, ry, rw, rh);
+ return true;
+ }
}
- if on_border || (hit_menubar && !clicked_menu_item) {
+ if on_border || hit_menubar {
self.is_dragging_network_circle = true;
- self.circle_drag_ox = self.cursor_x - self.network_circle_x;
- self.circle_drag_oy = self.cursor_y - self.network_circle_y;
+ self.circle_drag_ox = self.cursor_x - self.circular_network_layout.x;
+ self.circle_drag_oy = self.cursor_y - self.circular_network_layout.y;
self.focused_pane = LEFT_MENUBAR_IDX;
if let Some(old) = self.focused_widget {
self.widgets[old].unfocus();
self.focused_widget = None;
}
self.widgets[PARAM_IDX].unfocus();
- return true;
- } else if clicked_menu_item {
- self.focused_pane = LEFT_MENUBAR_IDX;
- if let Some(old) = self.focused_widget {
- if old != LEFT_MENUBAR_IDX {
- self.widgets[old].unfocus();
- }
- }
- self.widgets[PARAM_IDX].unfocus();
- self.widgets[LEFT_MENUBAR_IDX].focus();
- self.focused_widget = Some(LEFT_MENUBAR_IDX);
- if !self.widgets[LEFT_MENUBAR_IDX].is_menu_open() {
- self.widgets[LEFT_MENUBAR_IDX].unfocus();
- self.focused_widget = None;
- }
+ self.sync_parameters_to_project();
return true;
}
}
@@ -4282,7 +4097,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
}
} else {
if self.circular_network_pane {
- if self.cursor_x > self.splitter2_x + SPLITTER_W {
+ if self.cursor_x > self.splitter_layout.splitter2_x + SPLITTER_W {
new_pane = Some(PARAM_MENUBAR_IDX);
} else {
if self.show_spreadsheet && self.cursor_y >= self.positions[SPREADSHEET_MENUBAR_IDX].1 {
@@ -4292,9 +4107,9 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
}
}
} else {
- if self.cursor_x < self.splitter1_x {
+ if self.cursor_x < self.splitter_layout.splitter1_x {
new_pane = Some(LEFT_MENUBAR_IDX);
- } else if self.cursor_x > self.splitter2_x + SPLITTER_W {
+ } else if self.cursor_x > self.splitter_layout.splitter2_x + SPLITTER_W {
new_pane = Some(PARAM_MENUBAR_IDX);
} else {
if self.show_spreadsheet && self.cursor_y >= self.positions[SPREADSHEET_MENUBAR_IDX].1 {
@@ -4320,6 +4135,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
}
if click_target != Some(PARAM_IDX) {
self.widgets[PARAM_IDX].unfocus();
+ self.sync_parameters_to_project();
}
if click_target.is_none() && !dialog_open && in_circle_network_pane {
let col = ((self.cursor_x - self.pan_x) / (self.grid_size_x + self.skipped_col_w)).floor() as i32;
@@ -4329,8 +4145,15 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
changed = true;
}
if let Some(i) = click_target {
+ if let Some((menu_idx, title, items, rx, ry, rw, rh)) = self.widgets[i].get_menu_items_at(self.cursor_x, self.cursor_y) {
+ self.spawn_menu_cloud(i, menu_idx, title, items, rx, ry, rw, rh);
+ return true;
+ }
if self.widgets[i].mouse_input(*button, *btn_state, self.cursor_x, self.cursor_y) {
changed = true;
+ if i == PARAM_IDX {
+ self.sync_parameters_to_project();
+ }
}
if self.widgets[i].draggable() {
self.widgets[i].drag_begin(self.cursor_x, self.cursor_y);
@@ -4339,7 +4162,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
if i != PARAM_IDX {
self.widgets[i].focus();
self.focused_widget = Some(i);
- if self.widgets[i].is_menu_bar() && !self.widgets[i].is_menu_open() {
+ if self.widgets[i].is_menu_bar() && !self.widgets[i].focused() {
self.widgets[i].unfocus();
self.focused_widget = None;
}
@@ -4445,21 +4268,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
}
if self.widgets[PARAM_IDX].keyboard_input(event) {
- if self.focused_widget == Some(CONTENT_IDX) {
- if let Some(slot_idx) = self.widgets[CONTENT_IDX].selected_node() {
- let updated_params = self.widgets[PARAM_IDX].node_params();
- let dir = self.current_dir_mut();
- if let Some(child) = dir.children.get_mut(slot_idx) {
- for (u_name, u_val, _) in &updated_params {
- if let Some(p) = child.params.iter_mut().find(|p| p.name == *u_name) {
- p.default = u_val.clone();
- }
- }
- }
- }
- }
- self.rebuild_scene_geometry();
- self.sync_nodes();
+ self.sync_parameters_to_project();
return true;
}
@@ -4468,7 +4277,6 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
}
let mut changed = false;
if event.state == ElementState::Pressed {
- if !self.widgets[CONFIG_DIALOG_IDX].visible() {
let is_plain_key = !self.modifiers.control_key() && !self.modifiers.alt_key() && !self.modifiers.super_key();
let is_alt_key = self.modifiers.alt_key() && !self.modifiers.control_key() && !self.modifiers.super_key();
@@ -4551,8 +4359,12 @@ 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;
+ if self.active_camera != "Default Camera" {
+ self.update_active_camera_rotation_reset();
+ } else {
+ self.rotation_y = 0.0;
+ self.rotation_x = 0.0;
+ }
self.viewport_zoom = 1.0;
self.rotate_velocity_yaw = 0.0;
self.rotate_velocity_pitch = 0.0;
@@ -4688,14 +4500,13 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
}
}
}
- }
if changed {
self.keep_cursor_in_view();
}
if !changed {
- if event.logical_key == Key::Named(NamedKey::Tab) && !self.widgets[CONFIG_DIALOG_IDX].visible() {
+ if event.logical_key == Key::Named(NamedKey::Tab) {
self.open_node_palette();
return true;
}
@@ -4723,6 +4534,44 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
if now.duration_since(self.last_config_read).as_secs_f32() > 2.0 {
self.update_inertial_settings();
+ let design_path = DesignSettings::file_path();
+ if let Ok(m) = std::fs::metadata(&design_path) {
+ if let Ok(mod_time) = m.modified() {
+ if Some(mod_time) != self.last_design_mod_time {
+ self.last_design_mod_time = Some(mod_time);
+ let settings = DesignSettings::load();
+ self.square_viewport = settings.square_viewport;
+ self.grid_snap_enabled = settings.grid_snap_enabled;
+ self.network_grid_visible = settings.network_grid_enabled;
+ self.grid_size_x = settings.grid_size_x;
+ self.grid_size_y = settings.grid_size_y;
+ self.skipped_row_h = settings.skipped_row_h;
+ self.skipped_col_w = settings.skipped_col_w;
+ self.grid_thickness = settings.grid_thickness;
+ self.show_grid = settings.show_grid_enabled;
+ self.show_cube = settings.show_cube_enabled;
+ self.show_origin = settings.show_origin_enabled;
+ self.show_camera_pivot = settings.show_camera_pivot_enabled;
+ self.viewport_bg_color = settings.viewport_bg_color;
+ self.node_color = settings.node_color;
+ self.grid_color = settings.grid_color;
+ self.origin_size = settings.origin_size;
+ self.camera_pivot_size = settings.camera_pivot_size;
+ self.uniform_background = settings.uniform_background;
+ self.network_opacity = settings.network_opacity;
+ self.cell_color = settings.cell_color;
+ self.gap_color = settings.gap_color;
+
+ colors::set_node_color([self.node_color[0], self.node_color[1], self.node_color[2], 1.0]);
+
+ self.update_origin_geometry();
+ self.update_grid_geometry();
+ self.update_pivot_geometry();
+ self.sync_grid_settings();
+ self.upload_vertices();
+ }
+ }
+ }
}
// Panning velocity tracking
@@ -4814,8 +4663,14 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
// Viewport rotation kinetic slide
if !self.is_rotating_viewport && (self.rotate_velocity_yaw.abs() > 0.001 || self.rotate_velocity_pitch.abs() > 0.001) {
- self.rotation_y += self.rotate_velocity_yaw * dt;
- self.rotation_x += self.rotate_velocity_pitch * dt;
+ let dx = self.rotate_velocity_yaw * dt;
+ let dy = self.rotate_velocity_pitch * dt;
+ if self.active_camera != "Default Camera" {
+ self.update_active_camera_rotation(dx, dy);
+ } else {
+ self.rotation_y += dx;
+ self.rotation_x += dy;
+ }
// Apply friction decay
let friction = 5.0_f32;
@@ -4913,7 +4768,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
});
// 3D canvas render pass (background layer)
- {
+ if !self.is_detached_network {
let cx_logical = 0.0;
let cy_logical = HEADER_H;
let cw_logical = self.width;
@@ -4936,31 +4791,68 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
let proj = Mat4::perspective_rh(0.9, aspect, 0.1, 100.0);
let mut camera_pos = Vec3::new(2.5, 1.8, 2.5);
+ let mut rx = 0.0f32;
+ let mut ry = 0.0f32;
+ let mut rz = 0.0f32;
+ let mut pivot = Vec3::ZERO;
if self.active_camera != "Default Camera" {
if let Some(node) = self.current_dir().children.iter().find(|c| c.node_type == "camera" && c.name == self.active_camera) {
let mut cx = 2.5f32;
let mut cy = 1.8f32;
let mut cz = 2.5f32;
for p in &node.params {
- if p.name == "X" {
- if let Ok(val) = p.default.parse::<f32>() {
- cx = val;
+ if p.name == "Position" {
+ let parts: Vec<&str> = p.default
+ .split(|c| c == ':' || c == ',' || c == ' ')
+ .filter(|s| !s.is_empty())
+ .collect();
+ if parts.len() >= 3 {
+ if let (Ok(vx), Ok(vy), Ok(vz)) = (parts[0].parse::<f32>(), parts[1].parse::<f32>(), parts[2].parse::<f32>()) {
+ cx = vx;
+ cy = vy;
+ cz = vz;
+ }
}
- } else if p.name == "Y" {
- if let Ok(val) = p.default.parse::<f32>() {
- cy = val;
+ } else if p.name == "Rotation" {
+ let parts: Vec<&str> = p.default
+ .split(|c| c == ':' || c == ',' || c == ' ')
+ .filter(|s| !s.is_empty())
+ .collect();
+ if parts.len() >= 3 {
+ if let (Ok(vx), Ok(vy), Ok(vz)) = (parts[0].parse::<f32>(), parts[1].parse::<f32>(), parts[2].parse::<f32>()) {
+ rx = vx;
+ ry = vy;
+ rz = vz;
+ }
}
- } else if p.name == "Z" {
- if let Ok(val) = p.default.parse::<f32>() {
- cz = val;
+ } else if p.name == "Pivot" {
+ let parts: Vec<&str> = p.default
+ .split(|c| c == ':' || c == ',' || c == ' ')
+ .filter(|s| !s.is_empty())
+ .collect();
+ if parts.len() >= 3 {
+ if let (Ok(vx), Ok(vy), Ok(vz)) = (parts[0].parse::<f32>(), parts[1].parse::<f32>(), parts[2].parse::<f32>()) {
+ pivot = Vec3::new(vx, vy, vz);
+ }
}
}
}
camera_pos = Vec3::new(cx, cy, cz);
}
}
- camera_pos *= self.viewport_zoom;
- let view_mat = Mat4::look_at_rh(camera_pos, Vec3::ZERO, Vec3::Y);
+
+ let base_offset = camera_pos - pivot;
+ let distance = base_offset.length();
+ let yaw0 = base_offset.x.atan2(base_offset.z);
+ let pitch0 = (base_offset.y / distance.max(1e-5)).asin();
+
+ let total_ry = ry.to_radians() + yaw0;
+ let total_rx = rx.to_radians() + pitch0;
+
+ let view_rot_pos = Mat4::from_rotation_y(total_ry) * Mat4::from_rotation_x(-total_rx);
+ let camera_up = view_rot_pos.transform_vector3(Vec3::Y);
+ let camera_world_pos = pivot + view_rot_pos.transform_vector3(Vec3::new(0.0, 0.0, distance) * self.viewport_zoom);
+ let view_mat = Mat4::from_rotation_z(rz.to_radians()) * Mat4::look_at_rh(camera_world_pos, pivot, camera_up);
let model = Mat4::from_rotation_y(self.rotation_y) * Mat4::from_rotation_x(self.rotation_x);
let mvp = proj * view_mat * model;
self.queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[mvp.to_cols_array_2d()]));
@@ -4969,7 +4861,12 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
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 rot_angle = if self.active_camera != "Default Camera" {
+ total_ry
+ } else {
+ self.rotation_y + cam_angle_y
+ };
+ let model_pivot = Mat4::from_translation(pivot) * Mat4::from_rotation_y(rot_angle);
let mvp_pivot = proj * view_mat * model_pivot;
self.queue.write_buffer(&self.uniform_buffer_pivot, 0, bytemuck::cast_slice(&[mvp_pivot.to_cols_array_2d()]));
@@ -5037,13 +4934,19 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
// UI render pass (foreground layer)
{
+ let load_op = if self.is_detached_network {
+ wgpu::LoadOp::Clear(wgpu::Color { r: 0.0, g: 0.0, b: 0.0, a: 0.0 })
+ } else {
+ wgpu::LoadOp::Load
+ };
+
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("UI Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
- load: wgpu::LoadOp::Load,
+ load: load_op,
store: wgpu::StoreOp::Store,
},
})],
@@ -5056,7 +4959,16 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
pass.draw(0..self.vertex_count, 0..1);
+ if self.textured_vertex_count > 0 {
+ // eprintln!("DEBUG_RENDER: textured_vertex_count={}", self.textured_vertex_count);
+ pass.set_pipeline(&self.curved_text_pipeline);
+ pass.set_bind_group(0, &self.curved_text_bind_group, &[]);
+ pass.set_vertex_buffer(0, self.textured_vertex_buffer.slice(..));
+ pass.draw(0..self.textured_vertex_count, 0..1);
+ }
+
self.text_renderer.render(&self.text_atlas, &self.text_viewport, &mut pass).unwrap();
+
}
self.queue.submit(std::iter::once(encoder.finish()));
@@ -5108,6 +5020,7 @@ struct AppState {
redraw: bool,
pressed_key: Option<PressedKey>,
inspector: Option<clear_ui::protocol::zclear_inspector_v1::ZclearInspectorV1>,
+ _sender: calloop::channel::Sender<CustomEvent>,
}
impl CompositorHandler for AppState {
@@ -5195,6 +5108,7 @@ impl SeatHandler for AppState {
fn new_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
self.seats.push(seat);
+ eprintln!("DEBUG SEAT: new_seat called, total seats now: {}", self.seats.len());
}
fn new_capability(
@@ -5204,6 +5118,7 @@ impl SeatHandler for AppState {
seat: wl_seat::WlSeat,
capability: Capability,
) {
+ eprintln!("DEBUG SEAT: new_capability: {:?}", capability);
if capability == Capability::Pointer && self.pointer.is_none() {
let surface = self.compositor_state.create_surface(qh);
let themed_pointer = self.seat_state.get_pointer_with_theme(
@@ -5241,6 +5156,7 @@ impl SeatHandler for AppState {
fn remove_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
self.seats.retain(|s| s != &seat);
+ eprintln!("DEBUG SEAT: remove_seat called, total seats now: {}", self.seats.len());
}
}
@@ -5266,6 +5182,50 @@ impl PointerHandler for AppState {
st.cursor_y = event.position.1 as f32;
match &event.kind {
PointerEventKind::Motion { .. } => {
+ if st.is_detached_network {
+ let lx = event.position.0 as f32;
+ let ly = event.position.1 as f32;
+ let dx = lx - st.circular_network_layout.x;
+ let dy = ly - st.circular_network_layout.y;
+ let dist = (dx * dx + dy * dy).sqrt();
+ let on_border = dist >= st.circular_network_layout.r - 12.0 && dist <= st.circular_network_layout.r;
+ let hits_any_menu = st.widgets[LEFT_MENUBAR_IDX].get_menu_items_at(lx, ly).is_some();
+
+ if let Some(ref themed_pointer) = self.pointer {
+ if on_border && !hits_any_menu {
+ let nx = dx / dist;
+ let ny = dy / dist;
+ let mut cursor = CursorIcon::Default;
+ if ny < -0.382 {
+ if nx < -0.382 {
+ cursor = CursorIcon::NwResize;
+ } else if nx > 0.382 {
+ cursor = CursorIcon::NeResize;
+ } else {
+ cursor = CursorIcon::NResize;
+ }
+ } else if ny > 0.382 {
+ if nx < -0.382 {
+ cursor = CursorIcon::SwResize;
+ } else if nx > 0.382 {
+ cursor = CursorIcon::SeResize;
+ } else {
+ cursor = CursorIcon::SResize;
+ }
+ } else {
+ if nx < -0.382 {
+ cursor = CursorIcon::WResize;
+ } else if nx > 0.382 {
+ cursor = CursorIcon::EResize;
+ }
+ }
+ let _ = themed_pointer.set_cursor(_conn, cursor);
+ } else {
+ let _ = themed_pointer.set_cursor(_conn, CursorIcon::Default);
+ }
+ }
+ }
+
let ev = WindowEvent::CursorMoved {
position: LocalPosition {
x: event.position.0,
@@ -5274,7 +5234,7 @@ impl PointerHandler for AppState {
};
self.process_event(ev);
}
- PointerEventKind::Press { button, .. } => {
+ PointerEventKind::Press { button, serial, .. } => {
let btn = match *button {
272 => clear_ui::widget::MouseButton::Left,
273 => clear_ui::widget::MouseButton::Right,
@@ -5282,6 +5242,68 @@ impl PointerHandler for AppState {
_ => continue,
};
eprintln!("DEBUG MOUSE PRESS: button={:?}, pos={:?}, local=({}, {})", btn, event.position, cx, cy);
+
+ if let Some(ref st) = self.state {
+ if st.is_detached_network && btn == clear_ui::widget::MouseButton::Left {
+ let lx = event.position.0 as f32;
+ let ly = event.position.1 as f32;
+ let dx = lx - st.circular_network_layout.x;
+ let dy = ly - st.circular_network_layout.y;
+ let dist = (dx * dx + dy * dy).sqrt();
+ let on_border = dist >= st.circular_network_layout.r - 12.0 && dist <= st.circular_network_layout.r;
+ let in_menubar_bg = dy < 0.0 && dist >= st.circular_network_layout.r - 35.0 && dist <= st.circular_network_layout.r;
+ let hits_any_menu = st.widgets[LEFT_MENUBAR_IDX].get_menu_items_at(lx, ly).is_some();
+
+ eprintln!("DEBUG DRAG: lx={}, ly={}, cx={}, cy={}, r={}, dx={}, dy={}, dist={}, on_border={}, in_menubar_bg={}, hits_any_menu={}, seats_len={}, has_window={}",
+ lx, ly, st.circular_network_layout.x, st.circular_network_layout.y, st.circular_network_layout.r,
+ dx, dy, dist, on_border, in_menubar_bg, hits_any_menu, self.seats.len(), self.window.is_some());
+
+ if (on_border || in_menubar_bg) && !hits_any_menu {
+ if let Some(ref window) = self.window {
+ let seat = self.seats.first().cloned().or_else(|| self.seat_state.seats().next());
+ if let Some(ref seat) = seat {
+ if on_border {
+ use smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge;
+ let nx = dx / dist;
+ let ny = dy / dist;
+ let mut edge = ResizeEdge::None;
+ if ny < -0.382 {
+ if nx < -0.382 {
+ edge = ResizeEdge::TopLeft;
+ } else if nx > 0.382 {
+ edge = ResizeEdge::TopRight;
+ } else {
+ edge = ResizeEdge::Top;
+ }
+ } else if ny > 0.382 {
+ if nx < -0.382 {
+ edge = ResizeEdge::BottomLeft;
+ } else if nx > 0.382 {
+ edge = ResizeEdge::BottomRight;
+ } else {
+ edge = ResizeEdge::Bottom;
+ }
+ } else {
+ if nx < -0.382 {
+ edge = ResizeEdge::Left;
+ } else if nx > 0.382 {
+ edge = ResizeEdge::Right;
+ }
+ }
+ eprintln!("DEBUG RESIZE INITIATING window.resize with edge={:?}, serial={}", edge, serial);
+ window.resize(seat, *serial, edge);
+ continue;
+ } else {
+ eprintln!("DEBUG DRAG INITIATING window.move_ with serial={}", serial);
+ window.move_(seat, *serial);
+ continue;
+ }
+ }
+ }
+ }
+ }
+ }
+
let ev = WindowEvent::MouseInput {
state: clear_ui::widget::ElementState::Pressed,
button: btn,
@@ -5305,6 +5327,7 @@ impl PointerHandler for AppState {
PointerEventKind::Axis { horizontal, vertical, .. } => {
let h_val = horizontal.absolute as f32;
let v_val = vertical.absolute as f32;
+ eprintln!("DEBUG AXIS EVENT: horizontal={:?}, vertical={:?}, scale={}", horizontal, vertical, st.scale);
let ev = WindowEvent::MouseWheel {
delta: clear_ui::widget::MouseScrollDelta::LineDelta(-h_val / 10.0, -v_val / 10.0),
phase: TouchPhase::Moved,
@@ -5400,6 +5423,7 @@ impl WindowHandler for AppState {
_serial: u32,
) {
let (w, h) = configure.new_size;
+ eprintln!("DEBUG CONFIGURE: new_size={:?}, configure={:?}", configure.new_size, configure);
if let (Some(w), Some(h)) = (w, h) {
let width = w.get();
let height = h.get();
@@ -5442,13 +5466,30 @@ impl ProvidesRegistryState for AppState {
impl wayland_client::Dispatch<clear_ui::protocol::zclear_inspector_v1::ZclearInspectorV1, ()> for AppState {
fn event(
- _state: &mut Self,
+ state: &mut Self,
_proxy: &clear_ui::protocol::zclear_inspector_v1::ZclearInspectorV1,
- _event: clear_ui::protocol::zclear_inspector_v1::Event,
+ event: clear_ui::protocol::zclear_inspector_v1::Event,
_data: &(),
_conn: &Connection,
_qh: &QueueHandle<Self>,
- ) {}
+ ) {
+ match event {
+ clear_ui::protocol::zclear_inspector_v1::Event::InspectedSurface { app_id, x, y, .. } => {
+ if let Some(ref mut st) = state.state {
+ let expected_id = if st.is_detached_network {
+ "circular-network-pane"
+ } else {
+ "clear-design-interface"
+ };
+ if app_id == expected_id {
+ st.window_x = x;
+ st.window_y = y;
+ }
+ }
+ }
+ _ => {}
+ }
+ }
}
delegate_compositor!(AppState);
@@ -5596,6 +5637,14 @@ impl AppState {
}
_ => {}
}
+ } else if menu_idx == 2 { // View
+ match item_idx {
+ 3 => { // Detach Circular Window
+ state.execute_action(Action::DetachCircularWindow);
+ changed = true;
+ }
+ _ => {}
+ }
}
}
@@ -5632,6 +5681,10 @@ impl AppState {
state.sync_grid_settings();
changed = true;
}
+ 3 => { // Detach Pane
+ state.execute_action(Action::DetachCircularWindow);
+ changed = true;
+ }
_ => {}
}
}
@@ -5671,82 +5724,6 @@ impl AppState {
}
}
- let mut settings_changed = false;
- while let Some((id, val)) = state.widgets[CONFIG_DIALOG_IDX].take_config_toggle() {
- match id {
- 0 => {
- state.grid_snap_enabled = val;
- state.widgets[CONTENT_IDX].set_grid_snap_enabled(val);
- }
- 1 => {
- state.network_grid_visible = val;
- state.widgets[CONTENT_IDX].set_show_network_grid(val);
- }
- 2 => {
- state.show_grid = val;
- state.widgets[RIGHT_MENUBAR_IDX].set_item_checked(2, 0, val);
- }
- 3 => {
- state.show_cube = val;
- state.widgets[RIGHT_MENUBAR_IDX].set_item_checked(2, 1, val);
- }
- 4 => {
- 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() {
- match id {
- 0 => state.grid_size_x = val,
- 1 => state.grid_size_y = val,
- 2 => state.skipped_row_h = val,
- 3 => state.skipped_col_w = val,
- 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();
- }
- 10 => {
- state.node_color[0] = val;
- colors::set_node_color([state.node_color[0], state.node_color[1], state.node_color[2], 1.0]);
- }
- 11 => {
- state.node_color[1] = val;
- colors::set_node_color([state.node_color[0], state.node_color[1], state.node_color[2], 1.0]);
- }
- 12 => {
- state.node_color[2] = val;
- colors::set_node_color([state.node_color[0], state.node_color[1], state.node_color[2], 1.0]);
- }
- _ => {}
- }
- state.sync_grid_settings();
- changed = true;
- settings_changed = true;
- }
-
- if settings_changed {
- state.save_settings();
- }
if changed {
state.sync_layout();
@@ -5754,32 +5731,13 @@ impl AppState {
state.sync_cursor_and_selection();
if state.drag_widget == Some(PARAM_IDX) && state.widgets[PARAM_IDX].is_dragging() {
- if state.focused_widget == Some(CONTENT_IDX) {
- if let Some(slot_idx) = state.widgets[CONTENT_IDX].selected_node() {
- let updated_params = state.widgets[PARAM_IDX].node_params();
- let dir = state.current_dir_mut();
- if let Some(child) = dir.children.get_mut(slot_idx) {
- let mut param_changed = false;
- for (u_name, u_val, _type) in &updated_params {
- if let Some(p) = child.params.iter_mut().find(|p| p.name == *u_name) {
- if p.default != *u_val {
- p.default = u_val.clone();
- param_changed = true;
- }
- }
- }
- if param_changed {
- state.rebuild_scene_geometry();
- }
- }
- }
- }
+ state.sync_parameters_to_project();
}
state.sync_nodes();
// Sync Parameters pane with selected node
- let params = if state.focused_widget == Some(CONTENT_IDX) {
+ let params = if !state.is_detached_network {
state.widgets[CONTENT_IDX].selected_node().and_then(|sel_idx| {
let dir = state.current_dir();
if sel_idx < dir.children.len() {
@@ -5800,6 +5758,16 @@ impl AppState {
));
if changed {
+ if state.is_detached_network || state.detached_circular_network {
+ let default_proj_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("default_project.json");
+ if let Err(e) = state.save_to_file(&default_proj_path) {
+ eprintln!("Failed to auto-save default project in process_event: {:?}", e);
+ } else if let Ok(m) = std::fs::metadata(&default_proj_path) {
+ if let Ok(mod_time) = m.modified() {
+ state.last_project_mod_time = Some(mod_time);
+ }
+ }
+ }
self.redraw = true;
}
}
@@ -5817,6 +5785,7 @@ impl AppState {
active_camera: state.active_camera.clone(),
pan: (state.pan_x, state.pan_y),
current_path: state.current_path.clone(),
+ selected_node: state.widgets[CONTENT_IDX].selected_node(),
},
};
let json = serde_json::to_string_pretty(&proj).unwrap_or_default();
@@ -5863,8 +5832,12 @@ impl AppState {
}
}
HttpAction::ResetCamera => {
- state.rotation_y = 0.0;
- state.rotation_x = 0.0;
+ if state.active_camera != "Default Camera" {
+ state.update_active_camera_rotation_reset();
+ } else {
+ 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;
@@ -5992,6 +5965,7 @@ impl AppState {
options: vec![],
min: None,
max: None,
+ step: None,
};
state.current_dir_mut().children[slot].params.push(param);
state.sync_nodes();
@@ -6027,6 +6001,19 @@ impl AppState {
needs_redraw = true;
Ok(format!("Circular pane: {}", state.circular_network_pane))
}
+ HttpAction::MenuClick { widget_idx, menu_idx, item_idx } => {
+ state.widgets[widget_idx].trigger_menu_click(menu_idx, item_idx);
+ self.process_event(WindowEvent::CursorMoved { position: LocalPosition { x: -9999.0, y: -9999.0 } });
+ needs_redraw = true;
+ Ok("Menu clicked".to_string())
+ }
+ HttpAction::MenuClosed { widget_idx, menu_idx } => {
+ if state.active_menu_cloud_idx == Some((widget_idx, menu_idx)) {
+ state.active_menu_cloud_pid = None;
+ state.active_menu_cloud_idx = None;
+ }
+ Ok("Menu closed".to_string())
+ }
};
let _ = tx.send(res);
}
@@ -6042,12 +6029,27 @@ impl AppState {
}
}
if needs_redraw {
+ if let Some(state) = &mut self.state {
+ if state.is_detached_network || state.detached_circular_network {
+ let default_proj_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("default_project.json");
+ if let Err(e) = state.save_to_file(&default_proj_path) {
+ eprintln!("Failed to auto-save default project in handle_user_event: {:?}", e);
+ } else if let Ok(m) = std::fs::metadata(&default_proj_path) {
+ if let Ok(mod_time) = m.modified() {
+ state.last_project_mod_time = Some(mod_time);
+ }
+ }
+ }
+ }
self.redraw = true;
}
}
}
fn main() {
+ let args: Vec<String> = std::env::args().collect();
+ let is_detached_network = args.iter().any(|arg| arg == "--detached-network");
+
let conn = Connection::connect_to_env().unwrap();
let (globals, mut event_queue) = registry_queue_init(&conn).unwrap();
let qh = event_queue.handle();
@@ -6058,6 +6060,7 @@ fn main() {
let seat_state = SeatState::new(&globals, &qh);
let output_state = OutputState::new(&globals, &qh);
let inspector = globals.bind(&qh, 1..=1, ()).ok();
+ let (sender, channel) = calloop::channel::channel::<CustomEvent>();
let mut app = AppState {
registry_state: RegistryState::new(&globals),
@@ -6076,6 +6079,7 @@ fn main() {
redraw: true,
pressed_key: None,
inspector,
+ _sender: sender.clone(),
};
// Perform a roundtrip to populate output_state with active output scales
@@ -6083,8 +6087,11 @@ fn main() {
let scale = clear_ui::wayland::detect_scale_factor(&app.output_state);
- let pw = (1280.0 * scale) as u32;
- let ph = (800.0 * scale) as u32;
+ let (pw, ph) = if is_detached_network {
+ ((400.0 * scale) as u32, (400.0 * scale) as u32)
+ } else {
+ ((1280.0 * scale) as u32, (800.0 * scale) as u32)
+ };
let state = pollster::block_on(State::new(
&conn,
@@ -6093,6 +6100,7 @@ fn main() {
&app.xdg_shell_state,
pw, ph,
scale,
+ is_detached_network,
));
app.window = Some(state.window.clone());
@@ -6105,108 +6113,90 @@ fn main() {
}
}
- let (sender, channel) = calloop::channel::channel::<CustomEvent>();
-
- let server_sender = sender.clone();
- 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,
+ if !is_detached_network {
+ let server_sender = sender.clone();
+ 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");
- let server_sender = server_sender.clone();
- std::thread::spawn(move || {
- let mut write_stream = match stream.try_clone() {
+ for stream in listener.incoming() {
+ let stream = match stream {
Ok(s) => s,
- Err(_) => return,
+ Err(_) => continue,
};
- let mut reader = BufReader::new(stream);
- let mut request_line = String::new();
- if reader.read_line(&mut request_line).is_err() {
- return;
- }
- if request_line.starts_with("GET /state") {
- let (tx, rx) = std::sync::mpsc::channel();
- if server_sender.send(CustomEvent::GetState(tx)).is_ok() {
- let response_body = rx.recv().unwrap_or_else(|_| "null".to_string());
- let response = format!(
- "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
- response_body.len(),
- response_body
- );
- let _ = write_stream.write_all(response.as_bytes());
- } else {
- 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());
+ let server_sender = server_sender.clone();
+ std::thread::spawn(move || {
+ let mut write_stream = match stream.try_clone() {
+ Ok(s) => s,
+ Err(_) => return,
+ };
+ let mut reader = BufReader::new(stream);
+ let mut request_line = String::new();
+ if reader.read_line(&mut request_line).is_err() {
+ return;
}
- } 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;
+
+ if request_line.starts_with("GET /state") {
+ let (tx, rx) = std::sync::mpsc::channel();
+ if server_sender.send(CustomEvent::GetState(tx)).is_ok() {
+ let response_body = rx.recv().unwrap_or_else(|_| "null".to_string());
+ let response = format!(
+ "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
+ response_body.len(),
+ response_body
+ );
+ let _ = write_stream.write_all(response.as_bytes());
}
- 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;
+ } 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) {
+ let mut body = vec![0; content_length];
+ if reader.read_exact(&mut body).is_ok() {
+ let body_str = String::from_utf8_lossy(&body);
if let Ok(action) = serde_json::from_str::<HttpAction>(&body_str) {
let (tx, rx) = std::sync::mpsc::channel();
if server_sender.send(CustomEvent::PostAction(action, tx)).is_ok() {
- match rx.recv() {
- Ok(Ok(msg)) => {
+ let res = rx.recv().unwrap_or_else(|_| Err("internal error".to_string()));
+ let response = match res {
+ Ok(msg) => {
let body = format!("{{\"status\":\"success\",\"message\":\"{}\"}}", msg);
- let response = format!(
+ 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)) => {
+ Err(err) => {
let body = format!("{{\"status\":\"error\",\"error\":\"{}\"}}", err);
- let response = format!(
+ 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());
- }
- }
+ };
+ let _ = write_stream.write_all(response.as_bytes());
} else {
let body = "{\"status\":\"error\",\"error\":\"failed to send action to event loop\"}";
let response = format!(
@@ -6226,7 +6216,7 @@ fn main() {
let _ = write_stream.write_all(response.as_bytes());
}
} else {
- let body = "{\"status\":\"error\",\"error\":\"body is not valid UTF-8\"}";
+ 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(),
@@ -6234,15 +6224,6 @@ fn main() {
);
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!(
@@ -6256,6 +6237,7 @@ fn main() {
});
}
});
+ }
let mut event_loop = calloop::EventLoop::try_new().unwrap();
let loop_handle = event_loop.handle();
@@ -6283,6 +6265,36 @@ fn main() {
break;
}
+ if let Some(state) = &mut app.state {
+ if let Some(ref inspector) = app.inspector {
+ let now = std::time::Instant::now();
+ if now.duration_since(state.last_inspector_check) >= std::time::Duration::from_millis(250) {
+ state.last_inspector_check = now;
+ inspector.get_inspected_surfaces();
+ }
+ }
+
+ if state.is_detached_network || state.detached_circular_network {
+ let now = std::time::Instant::now();
+ if now.duration_since(state.last_project_check) >= std::time::Duration::from_millis(100) {
+ state.last_project_check = now;
+ let default_proj_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("default_project.json");
+ if let Ok(m) = std::fs::metadata(&default_proj_path) {
+ if let Ok(mod_time) = m.modified() {
+ if Some(mod_time) != state.last_project_mod_time {
+ state.last_project_mod_time = Some(mod_time);
+ if let Err(e) = state.load_from_file(&default_proj_path) {
+ eprintln!("Failed to auto-reload project: {:?}", e);
+ } else {
+ app.redraw = true;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
if let Some(ref mut pk) = app.pressed_key {
let now = std::time::Instant::now();
if now.duration_since(pk.first_pressed) >= KEY_REPEAT_DELAY {
@@ -6353,11 +6365,13 @@ mod tests {
assert_eq!(proj.name, "Default Project");
assert_eq!(proj.view_state.active_camera, "Camera 1");
assert_eq!(proj.root.name, "root");
- assert_eq!(proj.root.children.len(), 2);
+ assert_eq!(proj.root.children.len(), 3);
assert_eq!(proj.root.children[0].name, "Camera 1");
assert_eq!(proj.root.children[0].position, (1.0, 1.0));
assert_eq!(proj.root.children[1].name, "Sphere 1");
assert_eq!(proj.root.children[1].position, (4.0, 2.0));
+ assert_eq!(proj.root.children[2].name, "Transform 1");
+ assert_eq!(proj.root.children[2].position, (4.0, 3.0));
}
#[test]
@@ -6383,6 +6397,7 @@ mod tests {
active_camera: "child1".to_string(),
pan: (1.5, -2.5),
current_path: vec![0],
+ selected_node: Some(2),
};
let proj = Project {
name: "Test Project".to_string(),
@@ -6397,6 +6412,7 @@ mod tests {
assert_eq!(proj.view_state.active_camera, proj2.view_state.active_camera);
assert_eq!(proj.view_state.pan, proj2.view_state.pan);
assert_eq!(proj.view_state.current_path, proj2.view_state.current_path);
+ assert_eq!(proj.view_state.selected_node, proj2.view_state.selected_node);
assert_eq!(proj.root.name, proj2.root.name);
assert_eq!(proj.root.children.len(), proj2.root.children.len());
assert_eq!(proj.root.children[0].name, proj2.root.children[0].name);
@@ -6516,11 +6532,21 @@ mod tests {
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);
+ assert_eq!(settings.grid_color, [0.35, 0.35, 0.40]);
+ assert_eq!(settings.uniform_background, false);
+ assert_eq!(settings.network_opacity, 0.95);
+ assert_eq!(settings.cell_color, [0.13, 0.13, 0.16]);
+ assert_eq!(settings.gap_color, [0.07, 0.07, 0.09]);
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);
+ assert_eq!(settings_roundtrip.grid_color, [0.35, 0.35, 0.40]);
+ assert_eq!(settings_roundtrip.uniform_background, false);
+ assert_eq!(settings_roundtrip.network_opacity, 0.95);
+ assert_eq!(settings_roundtrip.cell_color, [0.13, 0.13, 0.16]);
+ assert_eq!(settings_roundtrip.gap_color, [0.07, 0.07, 0.09]);
}
#[test]
diff --git a/src/shader_textured.wgsl b/src/shader_textured.wgsl
new file mode 100644
index 0000000..24a34f0
--- /dev/null
+++ b/src/shader_textured.wgsl
@@ -0,0 +1,41 @@
+struct VertexInput {
+ @location(0) position: vec2f,
+ @location(1) tex_coords: vec2f,
+ @location(2) color: vec4f,
+ @location(3) clip_circle: vec3f,
+}
+
+struct VertexOutput {
+ @builtin(position) clip_position: vec4f,
+ @location(0) tex_coords: vec2f,
+ @location(1) color: vec4f,
+ @location(2) clip_circle: vec3f,
+}
+
+@vertex
+fn vs_main(in: VertexInput) -> VertexOutput {
+ var out: VertexOutput;
+ out.clip_position = vec4f(in.position, 0.0, 1.0);
+ out.tex_coords = in.tex_coords;
+ out.color = in.color;
+ out.clip_circle = in.clip_circle;
+ return out;
+}
+
+@group(0) @binding(0) var t_diffuse: texture_2d<f32>;
+@group(0) @binding(1) var s_diffuse: sampler;
+
+@fragment
+fn fs_main(in: VertexOutput) -> @location(0) vec4f {
+ if (in.clip_circle.z > 0.0) {
+ let dx = in.clip_position.x - in.clip_circle.x;
+ let dy = in.clip_position.y - in.clip_circle.y;
+ if (dx * dx + dy * dy > in.clip_circle.z * in.clip_circle.z) {
+ discard;
+ }
+ }
+ let tex_color = textureSample(t_diffuse, s_diffuse, in.tex_coords);
+ return tex_color * in.color;
+}
+
+