diff --git a/Cargo.lock b/Cargo.lock index 56929b4b79..67227b62fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -377,6 +377,7 @@ dependencies = [ "serde", "serde_json", "skia-safe", + "taffy", "tokio", "uuid", "winit", @@ -1094,6 +1095,12 @@ dependencies = [ "gl_generator", ] +[[package]] +name = "grid" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12101ecc8225ea6d675bc70263074eab6169079621c2186fe0c66590b2df9681" + [[package]] name = "grida-canvas-wasm" version = "0.0.0" @@ -2728,6 +2735,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.15.0" @@ -2854,6 +2870,18 @@ dependencies = [ "libc", ] +[[package]] +name = "taffy" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b25026fb8cc9ab51ab9fdabe5d11706796966f6d1c78e19871ef63be2b8f0644" +dependencies = [ + "arrayvec", + "grid", + "serde", + "slotmap", +] + [[package]] name = "tar" version = "0.4.44" diff --git a/crates/grida-canvas-fonts/examples/ttf_parser_chained_sequence_features.rs b/crates/grida-canvas-fonts/examples/ttf_parser_chained_sequence_features.rs index 615b5d3029..1820a88fa3 100644 --- a/crates/grida-canvas-fonts/examples/ttf_parser_chained_sequence_features.rs +++ b/crates/grida-canvas-fonts/examples/ttf_parser_chained_sequence_features.rs @@ -102,9 +102,7 @@ fn main() { #[derive(Debug, Clone)] struct ChainedFeature { pub tag: String, - pub name: String, pub glyphs: Vec, - pub source_table: String, } fn build_glyph_map(face: &ttf_parser::Face) -> std::collections::HashMap { @@ -150,7 +148,7 @@ fn extract_features_via_chained_sequences( } fn extract_gsub_features_simplified( - face: &ttf_parser::Face, + _face: &ttf_parser::Face, gsub_table: ttf_parser::opentype_layout::LayoutTable, glyph_map: &std::collections::HashMap, ) -> Vec { @@ -160,7 +158,6 @@ fn extract_gsub_features_simplified( for i in 0..gsub_table.features.len() { if let Some(feature) = gsub_table.features.get(i as u16) { let tag = feature.tag.to_string(); - let name = get_feature_name_from_font(face, &tag); // Extract glyphs from all lookups in this feature let mut all_glyphs = std::collections::HashSet::new(); @@ -224,9 +221,7 @@ fn extract_gsub_features_simplified( features.push(ChainedFeature { tag, - name, glyphs: glyph_chars, - source_table: "GSUB".to_string(), }); } } @@ -235,7 +230,7 @@ fn extract_gsub_features_simplified( } fn extract_gpos_features_simplified( - face: &ttf_parser::Face, + _face: &ttf_parser::Face, gpos_table: ttf_parser::opentype_layout::LayoutTable, glyph_map: &std::collections::HashMap, ) -> Vec { @@ -245,7 +240,6 @@ fn extract_gpos_features_simplified( for i in 0..gpos_table.features.len() { if let Some(feature) = gpos_table.features.get(i as u16) { let tag = feature.tag.to_string(); - let name = get_feature_name_from_font(face, &tag); // Extract glyphs from all lookups in this feature let mut all_glyphs = std::collections::HashSet::new(); @@ -301,9 +295,7 @@ fn extract_gpos_features_simplified( features.push(ChainedFeature { tag, - name, glyphs: glyph_chars, - source_table: "GPOS".to_string(), }); } } @@ -337,31 +329,3 @@ fn extract_coverage_glyphs(coverage: &ttf_parser::opentype_layout::Coverage) -> glyphs } - -fn get_feature_name_from_font(_face: &ttf_parser::Face, tag: &str) -> String { - // Simplified feature name mapping - match tag { - "kern" => "Kerning".to_string(), - "liga" => "Ligatures".to_string(), - "ss01" => "Stylistic Set 1".to_string(), - "cv01" => "Character Variant 1".to_string(), - "locl" => "Localized Forms".to_string(), - "zero" => "Slashed Zero".to_string(), - "sinf" => "Scientific Inferiors".to_string(), - "aalt" => "Access All Alternates".to_string(), - "numr" => "Numerators".to_string(), - "ordn" => "Ordinals".to_string(), - "case" => "Case-Sensitive Forms".to_string(), - "pnum" => "Proportional Numbers".to_string(), - "ccmp" => "Glyph Composition/Decomposition".to_string(), - "dlig" => "Discretionary Ligatures".to_string(), - "sups" => "Superscript".to_string(), - "tnum" => "Tabular Numbers".to_string(), - "subs" => "Subscript".to_string(), - "salt" => "Stylistic Alternates".to_string(), - "dnom" => "Denominators".to_string(), - "frac" => "Fractions".to_string(), - "calt" => "Contextual Alternates".to_string(), - _ => format!("Feature {}", tag), - } -} diff --git a/crates/grida-canvas/Cargo.toml b/crates/grida-canvas/Cargo.toml index 21848789ea..d16c6807d7 100644 --- a/crates/grida-canvas/Cargo.toml +++ b/crates/grida-canvas/Cargo.toml @@ -19,6 +19,7 @@ futures = "0.3.31" gl = "0.14.0" figma-api = { version = "0.31.3", optional = true } seahash = "4.1.0" +taffy = "0.9.1" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] @@ -50,4 +51,4 @@ harness = false [[example]] name = "app_figma" -required-features = ["figma"] \ No newline at end of file +required-features = ["figma"] diff --git a/crates/grida-canvas/benches/bench_rectangles.rs b/crates/grida-canvas/benches/bench_rectangles.rs index a17a4f8d02..c64ad4c689 100644 --- a/crates/grida-canvas/benches/bench_rectangles.rs +++ b/crates/grida-canvas/benches/bench_rectangles.rs @@ -1,5 +1,5 @@ use cg::cg::types::*; -use cg::node::repository::NodeRepository; +use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::*; use cg::runtime::camera::Camera2D; use cg::runtime::scene::{Backend, Renderer}; @@ -7,47 +7,45 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; use math2::transform::AffineTransform; fn create_rectangles(count: usize, with_effects: bool) -> Scene { - let mut repository = NodeRepository::new(); - let mut ids = Vec::new(); + let mut graph = SceneGraph::new(); // Create rectangles - for i in 0..count { - let id = format!("rect-{}", i); - ids.push(id.clone()); - - let rect = RectangleNodeRec { - id: id.clone(), - name: None, - active: true, - opacity: 1.0, - blend_mode: LayerBlendMode::default(), - mask: None, - transform: AffineTransform::identity(), - size: Size { - width: 100.0, - height: 100.0, - }, - corner_radius: RectangularCornerRadius::zero(), - fills: Paints::new([Paint::from(CGColor(255, 0, 0, 255))]), - strokes: Paints::default(), - stroke_width: 1.0, - stroke_align: StrokeAlign::Inside, - stroke_dash_array: None, - effects: if with_effects { - LayerEffects::from_array(vec![FilterEffect::DropShadow(FeShadow { - dx: 2.0, - dy: 2.0, - blur: 4.0, - spread: 0.0, - color: CGColor(0, 0, 0, 128), - })]) - } else { - LayerEffects::default() - }, - }; - - repository.insert(Node::Rectangle(rect)); - } + let rectangles: Vec = (0..count) + .map(|i| { + let id = format!("rect-{}", i); + + Node::Rectangle(RectangleNodeRec { + id: id.clone(), + name: None, + active: true, + opacity: 1.0, + blend_mode: LayerBlendMode::default(), + mask: None, + transform: AffineTransform::identity(), + size: Size { + width: 100.0, + height: 100.0, + }, + corner_radius: RectangularCornerRadius::zero(), + fills: Paints::new([Paint::from(CGColor(255, 0, 0, 255))]), + strokes: Paints::default(), + stroke_width: 1.0, + stroke_align: StrokeAlign::Inside, + stroke_dash_array: None, + effects: if with_effects { + LayerEffects::from_array(vec![FilterEffect::DropShadow(FeShadow { + dx: 2.0, + dy: 2.0, + blur: 4.0, + spread: 0.0, + color: CGColor(0, 0, 0, 128), + })]) + } else { + LayerEffects::default() + }, + }) + }) + .collect(); // Create root group let root_group = GroupNodeRec { @@ -55,20 +53,18 @@ fn create_rectangles(count: usize, with_effects: bool) -> Scene { name: Some("Root Group".to_string()), active: true, transform: None, - children: ids.clone(), opacity: 1.0, blend_mode: LayerBlendMode::default(), mask: None, }; - repository.insert(Node::Group(root_group)); + let root_id = graph.append_child(Node::Group(root_group), Parent::Root); + graph.append_children(rectangles, Parent::NodeId(root_id)); Scene { - id: "scene".to_string(), - name: "Test Scene".to_string(), - children: vec!["root".to_string()], - nodes: repository, + name: "Test Scene".into(), background_color: None, + graph, } } diff --git a/crates/grida-canvas/examples/app_figma.rs b/crates/grida-canvas/examples/app_figma.rs index 64ae6e0a37..8ad8db99be 100644 --- a/crates/grida-canvas/examples/app_figma.rs +++ b/crates/grida-canvas/examples/app_figma.rs @@ -241,9 +241,8 @@ async fn main() { .expect("Failed to load scene"); println!("Rendering scene: {}", scene.name); - println!("Scene ID: {}", scene.id); - println!("Number of children: {}", scene.children.len()); - println!("Total nodes in repository: {}", scene.nodes.len()); + println!("Number of roots: {}", scene.graph.roots().len()); + println!("Total nodes in graph: {}", scene.graph.node_count()); // Load webfonts metadata and find matching font files let webfonts_metadata = load_webfonts_metadata() diff --git a/crates/grida-canvas/examples/app_grida.rs b/crates/grida-canvas/examples/app_grida.rs index 1cb7957a5a..0b2574d6ce 100644 --- a/crates/grida-canvas/examples/app_grida.rs +++ b/crates/grida-canvas/examples/app_grida.rs @@ -1,5 +1,6 @@ use cg::cg::types::*; use cg::io::io_grida::parse; +use cg::node::scene_graph::SceneGraph; use cg::node::schema::*; use cg::window; use clap::Parser; @@ -40,37 +41,37 @@ async fn load_scene_from_file(file_path: &str) -> Scene { .and_then(|c| c.clone()) .unwrap_or_default(); - // Convert nodes to repository, filtering out scene nodes and populating children from links - let mut node_repo = cg::node::repository::NodeRepository::new(); - for (node_id, json_node) in canvas_file.document.nodes { - // Skip scene nodes - they're handled separately - if matches!(json_node, cg::io::io_grida::JSONNode::Scene(_)) { - continue; - } + // Build scene graph from nodes and links using snapshot + let scene_node_ids: std::collections::HashSet = canvas_file + .document + .nodes + .iter() + .filter(|(_, json_node)| matches!(json_node, cg::io::io_grida::JSONNode::Scene(_))) + .map(|(id, _)| id.clone()) + .collect(); - let mut node: cg::node::schema::Node = json_node.into(); + // Convert all nodes (skip scene nodes) + let nodes: Vec = canvas_file + .document + .nodes + .into_iter() + .filter(|(_, json_node)| !matches!(json_node, cg::io::io_grida::JSONNode::Scene(_))) + .map(|(_, json_node)| json_node.into()) + .collect(); - // Populate children from links - if let Some(children_opt) = links.get(&node_id) { - if let Some(children) = children_opt { - match &mut node { - cg::node::schema::Node::Container(n) => n.children = children.clone(), - cg::node::schema::Node::Group(n) => n.children = children.clone(), - cg::node::schema::Node::BooleanOperation(n) => n.children = children.clone(), - _ => {} // Other nodes don't have children - } - } - } + // Filter links (skip scene nodes as parents) + let filtered_links: std::collections::HashMap> = links + .into_iter() + .filter(|(parent_id, _)| !scene_node_ids.contains(parent_id)) + .filter_map(|(parent_id, children_opt)| children_opt.map(|children| (parent_id, children))) + .collect(); - node_repo.insert(node); - } + let graph = SceneGraph::new_from_snapshot(nodes, filtered_links, scene_children); Scene { - nodes: node_repo, - id: scene_id, name: scene_name, - children: scene_children, background_color: Some(CGColor(230, 230, 230, 255)), + graph, } } diff --git a/crates/grida-canvas/examples/bench_100k.rs b/crates/grida-canvas/examples/bench_100k.rs index 25592aa3a1..9cc1aebd4d 100644 --- a/crates/grida-canvas/examples/bench_100k.rs +++ b/crates/grida-canvas/examples/bench_100k.rs @@ -1,6 +1,6 @@ use cg::cg::types::*; use cg::node::factory::NodeFactory; -use cg::node::repository::NodeRepository; +use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::*; use cg::window; use math2::transform::AffineTransform; @@ -8,8 +8,7 @@ use math2::transform::AffineTransform; async fn demo_n_shapes(n: usize) -> Scene { let nf = NodeFactory::new(); - let mut repository = NodeRepository::new(); - let mut all_shape_ids = Vec::new(); + let mut graph = SceneGraph::new(); // Grid parameters let shape_size = 100.0; // Fixed size of 100x100 per shape @@ -52,15 +51,12 @@ async fn demo_n_shapes(n: usize) -> Scene { rect.set_fill(Paint::from(CGColor(r, g, b, 255))); - all_shape_ids.push(rect.id.clone()); - repository.insert(Node::Rectangle(rect)); + graph.append_child(Node::Rectangle(rect), Parent::Root); } Scene { - id: "scene".to_string(), name: format!("{} Shapes Performance Test", n), - children: all_shape_ids, - nodes: repository, + graph, background_color: None, } } diff --git a/crates/grida-canvas/examples/bench_cache_picture.rs b/crates/grida-canvas/examples/bench_cache_picture.rs index 80f18a94fb..f8532c2e8c 100644 --- a/crates/grida-canvas/examples/bench_cache_picture.rs +++ b/crates/grida-canvas/examples/bench_cache_picture.rs @@ -216,9 +216,6 @@ fn main() { let surface = unsafe { &mut *surface_ptr }; let size = window.inner_size(); let scene = CachedScene::new(size.width as f32, size.height as f32); - let mut camera_x = 0.0; - let mut camera_y = 0.0; - let mut zoom = 1.0; let start_time = Instant::now(); let mut frame_count = 0; let mut last_fps_time = Instant::now(); @@ -246,12 +243,12 @@ fn main() { let now = Instant::now(); let elapsed = now.duration_since(start_time).as_secs_f32(); let angle = elapsed * 2.0; - camera_x = angle.cos() * 100.0; - camera_y = angle.sin() * 100.0; + let camera_x = angle.cos() * 100.0; + let camera_y = angle.sin() * 100.0; // Add zoom animation let zoom_angle = elapsed * 1.0; // Slower zoom cycle - zoom = 1.0 + zoom_angle.sin() * 0.5; // Oscillate between 0.5 and 1.5 + let zoom = 1.0 + zoom_angle.sin() * 0.5; // Oscillate between 0.5 and 1.5 // Clear and render let canvas = surface.canvas(); diff --git a/crates/grida-canvas/examples/golden_container_stroke.rs b/crates/grida-canvas/examples/golden_container_stroke.rs index 1b72118cd5..72e2de1ab7 100644 --- a/crates/grida-canvas/examples/golden_container_stroke.rs +++ b/crates/grida-canvas/examples/golden_container_stroke.rs @@ -1,6 +1,6 @@ use cg::cg::types::*; use cg::node::factory::NodeFactory; -use cg::node::repository::NodeRepository; +use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::*; use cg::runtime::camera::Camera2D; use cg::runtime::scene::{Backend, Renderer}; @@ -8,7 +8,7 @@ use math2::{rect::Rectangle, transform::AffineTransform}; async fn scene() -> Scene { let nf = NodeFactory::new(); - let mut repo = NodeRepository::new(); + let mut graph = SceneGraph::new(); let mut container = nf.create_container_node(); container.size = Size { @@ -35,19 +35,13 @@ async fn scene() -> Scene { // But we want it to overlap with the stroke, so position it at the edge circle.transform = AffineTransform::new(200.0, 200.0, 0.0); - let circle_id = circle.id.clone(); - repo.insert(Node::Ellipse(circle)); - - let container_id = container.id.clone(); - // Add the circle as a child of the container - container.children = vec![circle_id]; - repo.insert(Node::Container(container)); + // Add container as root, then add circle as its child + let container_id = graph.append_child(Node::Container(container), Parent::Root); + graph.append_child(Node::Ellipse(circle), Parent::NodeId(container_id)); Scene { - id: "scene".into(), name: "container stroke".into(), - children: vec![container_id], - nodes: repo, + graph, background_color: None, } } diff --git a/crates/grida-canvas/examples/golden_pdf.rs b/crates/grida-canvas/examples/golden_pdf.rs index b7c973b3b8..86bf878558 100644 --- a/crates/grida-canvas/examples/golden_pdf.rs +++ b/crates/grida-canvas/examples/golden_pdf.rs @@ -1,6 +1,6 @@ use cg::cg::types::*; use cg::node::factory::NodeFactory; -use cg::node::repository::NodeRepository; +use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::*; use cg::runtime::camera::Camera2D; use cg::runtime::scene::{Backend, Renderer, RendererOptions}; @@ -10,7 +10,7 @@ use std::fs::File; async fn demo_scene() -> Scene { let nf = NodeFactory::new(); - let mut repo = NodeRepository::new(); + let mut graph = SceneGraph::new(); // Create a root container let mut root_container = nf.create_container_node(); @@ -20,8 +20,6 @@ async fn demo_scene() -> Scene { height: 700.0, }; - let mut all_node_ids = Vec::new(); - // Title text let mut title_text = nf.create_text_span_node(); title_text.name = Some("Title".to_string()); @@ -47,8 +45,6 @@ async fn demo_scene() -> Scene { title_text.text_align = TextAlign::Center; title_text.text_align_vertical = TextAlignVertical::Center; title_text.fills = Paints::new([Paint::from(CGColor(50, 50, 50, 255))]); - all_node_ids.push(title_text.id.clone()); - repo.insert(Node::TextSpan(title_text)); // Subtitle text let mut subtitle_text = nf.create_text_span_node(); @@ -76,8 +72,6 @@ async fn demo_scene() -> Scene { subtitle_text.text_align = TextAlign::Center; subtitle_text.text_align_vertical = TextAlignVertical::Center; subtitle_text.fills = Paints::new([Paint::from(CGColor(100, 100, 100, 255))]); - all_node_ids.push(subtitle_text.id.clone()); - repo.insert(Node::TextSpan(subtitle_text)); // Rectangle with gradient fill let mut rect_gradient = nf.create_rectangle_node(); @@ -117,8 +111,6 @@ async fn demo_scene() -> Scene { spread: 0.0, color: CGColor(0, 0, 0, 100), })]); - all_node_ids.push(rect_gradient.id.clone()); - repo.insert(Node::Rectangle(rect_gradient)); // Ellipse with radial gradient let mut ellipse_radial = nf.create_ellipse_node(); @@ -150,8 +142,6 @@ async fn demo_scene() -> Scene { })]); ellipse_radial.stroke_width = 4.0; ellipse_radial.strokes = Paints::new([Paint::from(CGColor(0, 0, 0, 255))]); - all_node_ids.push(ellipse_radial.id.clone()); - repo.insert(Node::Ellipse(ellipse_radial)); // Polygon (hexagon) let hexagon_points = (0..6) @@ -178,8 +168,6 @@ async fn demo_scene() -> Scene { spread: 0.0, color: CGColor(0, 0, 0, 150), })]); - all_node_ids.push(hexagon.id.clone()); - repo.insert(Node::Polygon(hexagon)); // Star polygon let mut star = nf.create_regular_star_polygon_node(); @@ -194,8 +182,6 @@ async fn demo_scene() -> Scene { star.fills = Paints::new([Paint::from(CGColor(255, 215, 0, 255))]); star.stroke_width = 2.0; star.strokes = Paints::new([Paint::from(CGColor(139, 69, 19, 255))]); - all_node_ids.push(star.id.clone()); - repo.insert(Node::RegularStarPolygon(star)); // Path (complex shape) let mut path = nf.create_path_node(); @@ -205,8 +191,6 @@ async fn demo_scene() -> Scene { path.fills = Paints::new([Paint::from(CGColor(255, 20, 147, 255))]); path.stroke_width = 2.0; path.strokes = Paints::new([Paint::from(CGColor(0, 0, 0, 255))]); - all_node_ids.push(path.id.clone()); - repo.insert(Node::SVGPath(path)); // Line with gradient stroke let mut line = nf.create_line_node(); @@ -237,8 +221,6 @@ async fn demo_scene() -> Scene { active: true, })]); line.stroke_width = 8.0; - all_node_ids.push(line.id.clone()); - repo.insert(Node::Line(line)); // Regular polygon (octagon) let mut octagon = nf.create_regular_polygon_node(); @@ -252,8 +234,6 @@ async fn demo_scene() -> Scene { octagon.fills = Paints::new([Paint::from(CGColor(0, 255, 255, 255))]); octagon.stroke_width = 3.0; octagon.strokes = Paints::new([Paint::from(CGColor(0, 0, 0, 255))]); - all_node_ids.push(octagon.id.clone()); - repo.insert(Node::RegularPolygon(octagon)); // Description text let mut description_text = nf.create_text_span_node(); @@ -280,19 +260,29 @@ async fn demo_scene() -> Scene { description_text.text_align = TextAlign::Center; description_text.text_align_vertical = TextAlignVertical::Center; description_text.fills = Paints::new([Paint::from(CGColor(80, 80, 80, 255))]); - all_node_ids.push(description_text.id.clone()); - repo.insert(Node::TextSpan(description_text)); - // Set up the root container - root_container.children = all_node_ids; - let root_container_id = root_container.id.clone(); - repo.insert(Node::Container(root_container)); + // Add root container and all its children + let root_container_id = graph.append_child(Node::Container(root_container), Parent::Root); + + graph.append_children( + vec![ + Node::TextSpan(title_text), + Node::TextSpan(subtitle_text), + Node::Rectangle(rect_gradient), + Node::Ellipse(ellipse_radial), + Node::Polygon(hexagon), + Node::RegularStarPolygon(star), + Node::SVGPath(path), + Node::Line(line), + Node::RegularPolygon(octagon), + Node::TextSpan(description_text), + ], + Parent::NodeId(root_container_id), + ); Scene { - id: "scene".into(), name: "PDF Demo".into(), - children: vec![root_container_id], - nodes: repo, + graph, background_color: Some(CGColor(255, 255, 255, 255)), } } diff --git a/crates/grida-canvas/examples/golden_svg.rs b/crates/grida-canvas/examples/golden_svg.rs index e2e8af02df..1eb24d9af9 100644 --- a/crates/grida-canvas/examples/golden_svg.rs +++ b/crates/grida-canvas/examples/golden_svg.rs @@ -1,6 +1,6 @@ use cg::cg::types::*; use cg::node::factory::NodeFactory; -use cg::node::repository::NodeRepository; +use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::*; use cg::runtime::camera::Camera2D; use cg::runtime::scene::{Backend, Renderer, RendererOptions}; @@ -11,7 +11,7 @@ use std::io::Write; async fn demo_scene() -> Scene { let nf = NodeFactory::new(); - let mut repo = NodeRepository::new(); + let mut graph = SceneGraph::new(); // Create a root container let mut root_container = nf.create_container_node(); @@ -21,7 +21,7 @@ async fn demo_scene() -> Scene { height: 700.0, }; - let mut all_node_ids = Vec::new(); + let root_container_id = graph.append_child(Node::Container(root_container), Parent::Root); // Title text let mut title_text = nf.create_text_span_node(); @@ -48,8 +48,6 @@ async fn demo_scene() -> Scene { title_text.text_align = TextAlign::Center; title_text.text_align_vertical = TextAlignVertical::Center; title_text.fills = Paints::new([Paint::from(CGColor(50, 50, 50, 255))]); - all_node_ids.push(title_text.id.clone()); - repo.insert(Node::TextSpan(title_text)); // Subtitle text let mut subtitle_text = nf.create_text_span_node(); @@ -62,8 +60,6 @@ async fn demo_scene() -> Scene { subtitle_text.text_align = TextAlign::Center; subtitle_text.text_align_vertical = TextAlignVertical::Center; subtitle_text.fills = Paints::new([Paint::from(CGColor(100, 100, 100, 255))]); - all_node_ids.push(subtitle_text.id.clone()); - repo.insert(Node::TextSpan(subtitle_text)); // Rectangle with gradient fill let mut rect_gradient = nf.create_rectangle_node(); @@ -103,8 +99,6 @@ async fn demo_scene() -> Scene { spread: 0.0, color: CGColor(0, 0, 0, 100), })]); - all_node_ids.push(rect_gradient.id.clone()); - repo.insert(Node::Rectangle(rect_gradient)); // Ellipse with radial gradient let mut ellipse_radial = nf.create_ellipse_node(); @@ -136,8 +130,6 @@ async fn demo_scene() -> Scene { })]); ellipse_radial.stroke_width = 4.0; ellipse_radial.strokes = Paints::new([Paint::from(CGColor(0, 0, 0, 255))]); - all_node_ids.push(ellipse_radial.id.clone()); - repo.insert(Node::Ellipse(ellipse_radial)); // Polygon (hexagon) let hexagon_points = (0..6) @@ -164,8 +156,6 @@ async fn demo_scene() -> Scene { spread: 0.0, color: CGColor(0, 0, 0, 150), })]); - all_node_ids.push(hexagon.id.clone()); - repo.insert(Node::Polygon(hexagon)); // Star polygon let mut star = nf.create_regular_star_polygon_node(); @@ -180,8 +170,6 @@ async fn demo_scene() -> Scene { star.fills = Paints::new([Paint::from(CGColor(255, 215, 0, 255))]); star.stroke_width = 2.0; star.strokes = Paints::new([Paint::from(CGColor(139, 69, 19, 255))]); - all_node_ids.push(star.id.clone()); - repo.insert(Node::RegularStarPolygon(star)); // Path (complex shape) let mut path = nf.create_path_node(); @@ -191,8 +179,6 @@ async fn demo_scene() -> Scene { path.fills = Paints::new([Paint::from(CGColor(255, 20, 147, 255))]); path.stroke_width = 2.0; path.strokes = Paints::new([Paint::from(CGColor(0, 0, 0, 255))]); - all_node_ids.push(path.id.clone()); - repo.insert(Node::SVGPath(path)); // Line with gradient stroke let mut line = nf.create_line_node(); @@ -223,8 +209,6 @@ async fn demo_scene() -> Scene { active: true, })]); line.stroke_width = 8.0; - all_node_ids.push(line.id.clone()); - repo.insert(Node::Line(line)); // Regular polygon (octagon) let mut octagon = nf.create_regular_polygon_node(); @@ -238,8 +222,6 @@ async fn demo_scene() -> Scene { octagon.fills = Paints::new([Paint::from(CGColor(0, 255, 255, 255))]); octagon.stroke_width = 3.0; octagon.strokes = Paints::new([Paint::from(CGColor(0, 0, 0, 255))]); - all_node_ids.push(octagon.id.clone()); - repo.insert(Node::RegularPolygon(octagon)); // Description text let mut description_text = nf.create_text_span_node(); @@ -251,20 +233,28 @@ async fn demo_scene() -> Scene { description_text.text_align = TextAlign::Center; description_text.text_align_vertical = TextAlignVertical::Center; description_text.fills = Paints::new([Paint::from(CGColor(80, 80, 80, 255))]); - all_node_ids.push(description_text.id.clone()); - repo.insert(Node::TextSpan(description_text)); - // Set up the root container - root_container.children = all_node_ids; - let root_container_id = root_container.id.clone(); - repo.insert(Node::Container(root_container)); + // Add all nodes to root container + graph.append_children( + vec![ + Node::TextSpan(title_text), + Node::TextSpan(subtitle_text), + Node::Rectangle(rect_gradient), + Node::Ellipse(ellipse_radial), + Node::Polygon(hexagon), + Node::RegularStarPolygon(star), + Node::SVGPath(path), + Node::Line(line), + Node::RegularPolygon(octagon), + Node::TextSpan(description_text), + ], + Parent::NodeId(root_container_id), + ); Scene { - id: "scene".into(), name: "SVG Demo".into(), - children: vec![root_container_id], - nodes: repo, background_color: Some(CGColor(255, 255, 255, 255)), + graph, } } diff --git a/crates/grida-canvas/examples/golden_type_line_height.rs b/crates/grida-canvas/examples/golden_type_line_height.rs index 4c75ee5c20..37f2e32846 100644 --- a/crates/grida-canvas/examples/golden_type_line_height.rs +++ b/crates/grida-canvas/examples/golden_type_line_height.rs @@ -253,7 +253,6 @@ fn main() { let x = start_x + (i as f32) * (col_width + col_spacing); draw_text_block(canvas, x, y_pos, col_width, label, line_height.clone()); } - y_pos += row_height + section_spacing; // Save the result let image = surface.image_snapshot(); diff --git a/crates/grida-canvas/examples/golden_type_stroke.rs b/crates/grida-canvas/examples/golden_type_stroke.rs index 25e917e33b..b0cbd78702 100644 --- a/crates/grida-canvas/examples/golden_type_stroke.rs +++ b/crates/grida-canvas/examples/golden_type_stroke.rs @@ -1,5 +1,5 @@ use cg::cg::types::*; -use cg::node::repository::NodeRepository; +use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::*; use cg::runtime::camera::Camera2D; use cg::runtime::scene::{Backend, Renderer, RendererOptions}; @@ -7,7 +7,7 @@ use math2::{rect::Rectangle, transform::AffineTransform}; use uuid::Uuid; async fn scene() -> Scene { - let mut repo = NodeRepository::new(); + let mut graph = SceneGraph::new(); // Text with Outside stroke alignment let text_outside = TextSpanNodeRec { @@ -32,8 +32,6 @@ async fn scene() -> Scene { mask: None, effects: LayerEffects::default(), }; - let text_outside_id = text_outside.id.clone(); - repo.insert(Node::TextSpan(text_outside)); // Text with Center stroke alignment let text_center = TextSpanNodeRec { @@ -58,8 +56,6 @@ async fn scene() -> Scene { mask: None, effects: LayerEffects::default(), }; - let text_center_id = text_center.id.clone(); - repo.insert(Node::TextSpan(text_center)); // Text with Inside stroke alignment let text_inside = TextSpanNodeRec { @@ -84,14 +80,20 @@ async fn scene() -> Scene { mask: None, effects: LayerEffects::default(), }; - let text_inside_id = text_inside.id.clone(); - repo.insert(Node::TextSpan(text_inside)); + + // Add all text nodes as root children in one operation + graph.append_children( + vec![ + Node::TextSpan(text_outside), + Node::TextSpan(text_center), + Node::TextSpan(text_inside), + ], + Parent::Root, + ); Scene { - id: "scene".into(), name: "type stroke".into(), - children: vec![text_outside_id, text_center_id, text_inside_id], - nodes: repo, + graph, background_color: Some(CGColor(255, 255, 255, 255)), } } diff --git a/crates/grida-canvas/examples/grida_basic.rs b/crates/grida-canvas/examples/grida_basic.rs index 913eebd55d..ec7301594b 100644 --- a/crates/grida-canvas/examples/grida_basic.rs +++ b/crates/grida-canvas/examples/grida_basic.rs @@ -1,6 +1,6 @@ use cg::cg::types::*; use cg::node::factory::NodeFactory; -use cg::node::repository::NodeRepository; +use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::*; use cg::window; use math2::transform::AffineTransform; @@ -149,8 +149,7 @@ async fn demo_basic() -> Scene { line_node.stroke_width = 4.0; // Create a group node for the shapes (rectangle, ellipse, polygon) - let mut shapes_group_node = nf.create_group_node(); - shapes_group_node.name = Some("Shapes Group".to_string()); + let shapes_group_node = nf.create_group_node(); // Create a root container node containing the shapes group, text, and line let mut root_container_node = nf.create_container_node(); @@ -160,45 +159,44 @@ async fn demo_basic() -> Scene { }; root_container_node.name = Some("Root Container".to_string()); - // Create a node map and add all nodes - let mut repository = NodeRepository::new(); - - // First, collect all the IDs we'll need - let rect_id = rect_node.id.clone(); - let ellipse_id = ellipse_node.id.clone(); - let polygon_id = polygon_node.id.clone(); - let regular_polygon_id = regular_polygon_node.id.clone(); - let text_span_id = text_span_node.id.clone(); - let line_id = line_node.id.clone(); - let image_id = image_node.id.clone(); - let path_id = path_node.id.clone(); - - // Now add all nodes to the map - repository.insert(Node::Rectangle(rect_node)); - repository.insert(Node::Ellipse(ellipse_node)); - repository.insert(Node::Polygon(polygon_node)); - repository.insert(Node::RegularPolygon(regular_polygon_node)); - repository.insert(Node::TextSpan(text_span_node)); - repository.insert(Node::Line(line_node)); - repository.insert(Node::Image(image_node)); - repository.insert(Node::SVGPath(path_node)); - - // Now set up the shapes group with the IDs we collected - shapes_group_node.children = vec![rect_id, ellipse_id, polygon_id, regular_polygon_id]; - let shapes_group_id = shapes_group_node.id.clone(); - repository.insert(Node::Group(shapes_group_node)); - - // Finally set up the root container with all IDs - root_container_node.children = vec![shapes_group_id, text_span_id, line_id, path_id, image_id]; - let root_container_id = root_container_node.id.clone(); - repository.insert(Node::Container(root_container_node)); + // Build the scene graph + let mut graph = SceneGraph::new(); + + // Add root container + let root_container_id = graph.append_child(Node::Container(root_container_node), Parent::Root); + + // Add shapes group to container + let shapes_group_id = graph.append_child( + Node::Group(shapes_group_node), + Parent::NodeId(root_container_id.clone()), + ); + + // Add shapes to group + graph.append_children( + vec![ + Node::Rectangle(rect_node), + Node::Ellipse(ellipse_node), + Node::Polygon(polygon_node), + Node::RegularPolygon(regular_polygon_node), + ], + Parent::NodeId(shapes_group_id), + ); + + // Add other elements to container + graph.append_children( + vec![ + Node::TextSpan(text_span_node), + Node::Line(line_node), + Node::SVGPath(path_node), + Node::Image(image_node), + ], + Parent::NodeId(root_container_id), + ); Scene { - id: "scene".to_string(), name: "Demo".to_string(), - children: vec![root_container_id], - nodes: repository, background_color: Some(CGColor(250, 250, 250, 255)), + graph, } } diff --git a/crates/grida-canvas/examples/grida_blendmode.rs b/crates/grida-canvas/examples/grida_blendmode.rs index f6b6574759..fc5bbcc426 100644 --- a/crates/grida-canvas/examples/grida_blendmode.rs +++ b/crates/grida-canvas/examples/grida_blendmode.rs @@ -1,13 +1,15 @@ +// FIXME: broken demo - make this golden_ not grida_ + use cg::cg::types::*; use cg::node::factory::NodeFactory; -use cg::node::repository::NodeRepository; +use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::*; use cg::window; use math2::transform::AffineTransform; async fn demo_blendmode() -> Scene { let nf = NodeFactory::new(); - let mut repository = NodeRepository::new(); + let mut graph = SceneGraph::new(); // Create a root container node let mut root_container_node = nf.create_container_node(); @@ -17,7 +19,7 @@ async fn demo_blendmode() -> Scene { }; root_container_node.name = Some("Root Container".to_string()); - let mut all_blendmode_ids = Vec::new(); + let root_container_id = graph.append_child(Node::Container(root_container_node), Parent::Root); let spacing = 400.0; let start_x = 50.0; let base_size = 256.0; @@ -80,8 +82,10 @@ async fn demo_blendmode() -> Scene { active: true, })); - let background_id = background.id.clone(); - repository.insert(Node::Rectangle(background)); + graph.append_child( + Node::Rectangle(background), + Parent::NodeId(root_container_id.clone()), + ); // Create a sweep gradient overlay (similar to C++ example's sweep gradient) let mut sweep_overlay = nf.create_rectangle_node(); @@ -136,8 +140,10 @@ async fn demo_blendmode() -> Scene { active: true, })); - let sweep_overlay_id = sweep_overlay.id.clone(); - repository.insert(Node::Rectangle(sweep_overlay)); + graph.append_child( + Node::Rectangle(sweep_overlay), + Parent::NodeId(root_container_id.clone()), + ); // Create a group for the colored circles with the specific blend mode let mut circle_group = nf.create_group_node(); @@ -145,7 +151,11 @@ async fn demo_blendmode() -> Scene { circle_group.transform = Some(AffineTransform::new(x, y, 0.0)); circle_group.blend_mode = LayerBlendMode::Blend(*blend_mode); - let mut circle_ids = Vec::new(); + // Add group to root container first + let circle_group_id = graph.append_child( + Node::Group(circle_group), + Parent::NodeId(root_container_id.clone()), + ); // Create three colored circles (green, red, blue) like in the C++ example let circle_radius = 80.0; @@ -160,9 +170,10 @@ async fn demo_blendmode() -> Scene { }; green_circle.set_fill(Paint::from(CGColor(0, 255, 0, 255))); green_circle.blend_mode = LayerBlendMode::default(); - let green_circle_id = green_circle.id.clone(); - repository.insert(Node::Ellipse(green_circle)); - circle_ids.push(green_circle_id); + graph.append_child( + Node::Ellipse(green_circle), + Parent::NodeId(circle_group_id.clone()), + ); // Red circle (bottom left) let mut red_circle = nf.create_ellipse_node(); @@ -174,9 +185,10 @@ async fn demo_blendmode() -> Scene { }; red_circle.set_fill(Paint::from(CGColor(255, 0, 0, 255))); red_circle.blend_mode = LayerBlendMode::default(); - let red_circle_id = red_circle.id.clone(); - repository.insert(Node::Ellipse(red_circle)); - circle_ids.push(red_circle_id); + graph.append_child( + Node::Ellipse(red_circle), + Parent::NodeId(circle_group_id.clone()), + ); // Blue circle (bottom right) let mut blue_circle = nf.create_ellipse_node(); @@ -189,14 +201,10 @@ async fn demo_blendmode() -> Scene { }; blue_circle.set_fill(Paint::from(CGColor(0, 0, 255, 255))); blue_circle.blend_mode = LayerBlendMode::default(); - let blue_circle_id = blue_circle.id.clone(); - repository.insert(Node::Ellipse(blue_circle)); - circle_ids.push(blue_circle_id); - - // Set up the circle group - circle_group.children = circle_ids; - let circle_group_id = circle_group.id.clone(); - repository.insert(Node::Group(circle_group)); + graph.append_child( + Node::Ellipse(blue_circle), + Parent::NodeId(circle_group_id.clone()), + ); // Create a text label for the blend mode let mut label = nf.create_text_span_node(); @@ -208,27 +216,16 @@ async fn demo_blendmode() -> Scene { label.text_align = TextAlign::Left; label.text_align_vertical = TextAlignVertical::Top; label.fills = Paints::new([Paint::from(CGColor(0, 0, 0, 255))]); - let label_id = label.id.clone(); - repository.insert(Node::TextSpan(label)); - - // Add all elements for this blend mode - all_blendmode_ids.push(background_id); - all_blendmode_ids.push(sweep_overlay_id); - all_blendmode_ids.push(circle_group_id); - all_blendmode_ids.push(label_id); + graph.append_child( + Node::TextSpan(label), + Parent::NodeId(root_container_id.clone()), + ); } - // Set up the root container - root_container_node.children = all_blendmode_ids; - let root_container_id = root_container_node.id.clone(); - repository.insert(Node::Container(root_container_node)); - Scene { - id: "scene".to_string(), name: "Blend Mode Demo".to_string(), - children: vec![root_container_id], - nodes: repository, background_color: Some(CGColor(240, 240, 240, 255)), + graph, } } diff --git a/crates/grida-canvas/examples/grida_booleans.rs b/crates/grida-canvas/examples/grida_booleans.rs index 3d67549373..763639e29a 100644 --- a/crates/grida-canvas/examples/grida_booleans.rs +++ b/crates/grida-canvas/examples/grida_booleans.rs @@ -1,13 +1,15 @@ +// FIXME: broken demo - make this golden_ not grida_ + use cg::cg::types::*; use cg::node::factory::NodeFactory; -use cg::node::repository::NodeRepository; +use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::*; use cg::window; use math2::transform::AffineTransform; async fn demo_booleans() -> Scene { let nf = NodeFactory::new(); - let mut repository = NodeRepository::new(); + let mut graph = SceneGraph::new(); // Create a root container node let mut root_container_node = nf.create_container_node(); @@ -17,7 +19,7 @@ async fn demo_booleans() -> Scene { height: 1080.0, }; - let mut all_shape_ids = Vec::new(); + let root_container_id = graph.append_child(Node::Container(root_container_node), Parent::Root); let spacing = 200.0; let start_x = 100.0; let base_size = 100.0; @@ -65,7 +67,6 @@ async fn demo_booleans() -> Scene { transform: Some(AffineTransform::new(start_x + spacing * 2.0, y_offset, 0.0)), op: BooleanPathOperation::Union, corner_radius: None, - children: vec![rect.id.clone(), circle.id.clone()], fills: Paints::new([Paint::from(CGColor(100, 100, 200, 255))]), strokes: Paints::new([Paint::from(CGColor(0, 0, 0, 255))]), stroke_width: 2.0, @@ -73,17 +74,19 @@ async fn demo_booleans() -> Scene { stroke_dash_array: None, }; - // Collect IDs before moving nodes - all_shape_ids.push(rect.id.clone()); - all_shape_ids.push(circle.id.clone()); - all_shape_ids.push(text.id.clone()); - all_shape_ids.push(bool_node.id.clone()); - - // Insert all nodes - repository.insert(Node::Rectangle(rect)); - repository.insert(Node::Ellipse(circle)); - repository.insert(Node::TextSpan(text)); - repository.insert(Node::BooleanOperation(bool_node)); + // Add boolean operation to root, then add operands to it + let bool_id = graph.append_child( + Node::BooleanOperation(bool_node), + Parent::NodeId(root_container_id.clone()), + ); + graph.append_children( + vec![Node::Rectangle(rect), Node::Ellipse(circle)], + Parent::NodeId(bool_id), + ); + graph.append_child( + Node::TextSpan(text), + Parent::NodeId(root_container_id.clone()), + ); } // Example 2: Two Circles Intersection @@ -128,7 +131,6 @@ async fn demo_booleans() -> Scene { effects: LayerEffects::default(), transform: Some(AffineTransform::new(start_x + spacing * 2.0, y_offset, 0.0)), op: BooleanPathOperation::Intersection, - children: vec![circle1.id.clone(), circle2.id.clone()], corner_radius: None, fills: Paints::new([Paint::from(CGColor(100, 100, 200, 255))]), strokes: Paints::new([Paint::from(CGColor(0, 0, 0, 255))]), @@ -138,16 +140,19 @@ async fn demo_booleans() -> Scene { }; // Collect IDs before moving nodes - all_shape_ids.push(circle1.id.clone()); - all_shape_ids.push(circle2.id.clone()); - all_shape_ids.push(text.id.clone()); - all_shape_ids.push(bool_node.id.clone()); - - // Insert all nodes - repository.insert(Node::Ellipse(circle1)); - repository.insert(Node::Ellipse(circle2)); - repository.insert(Node::TextSpan(text)); - repository.insert(Node::BooleanOperation(bool_node)); + // Add boolean operation to root, then add operands to it + let bool_id = graph.append_child( + Node::BooleanOperation(bool_node), + Parent::NodeId(root_container_id.clone()), + ); + graph.append_children( + vec![Node::Ellipse(circle1), Node::Ellipse(circle2)], + Parent::NodeId(bool_id), + ); + graph.append_child( + Node::TextSpan(text), + Parent::NodeId(root_container_id.clone()), + ); } // Example 3: Star and Rectangle Difference @@ -193,7 +198,6 @@ async fn demo_booleans() -> Scene { transform: Some(AffineTransform::new(start_x + spacing * 2.0, y_offset, 0.0)), op: BooleanPathOperation::Difference, corner_radius: None, - children: vec![star.id.clone(), rect.id.clone()], fills: Paints::new([Paint::from(CGColor(100, 100, 200, 255))]), strokes: Paints::new([Paint::from(CGColor(0, 0, 0, 255))]), stroke_width: 2.0, @@ -202,19 +206,22 @@ async fn demo_booleans() -> Scene { }; // Collect IDs before moving nodes - all_shape_ids.push(star.id.clone()); - all_shape_ids.push(rect.id.clone()); - all_shape_ids.push(text.id.clone()); - all_shape_ids.push(bool_node.id.clone()); - - // Insert all nodes - repository.insert(Node::RegularStarPolygon(star)); - repository.insert(Node::Rectangle(rect)); - repository.insert(Node::TextSpan(text)); - repository.insert(Node::BooleanOperation(bool_node)); + // Add boolean operation to root, then add operands to it + let bool_id = graph.append_child( + Node::BooleanOperation(bool_node), + Parent::NodeId(root_container_id.clone()), + ); + graph.append_children( + vec![Node::RegularStarPolygon(star), Node::Rectangle(rect)], + Parent::NodeId(bool_id), + ); + graph.append_child( + Node::TextSpan(text), + Parent::NodeId(root_container_id.clone()), + ); } - // Example 4: Two Squares XOR + // Example 4): Two Squares XOR { let y_offset = 900.0; // Increased from 700.0 @@ -257,7 +264,6 @@ async fn demo_booleans() -> Scene { transform: Some(AffineTransform::new(start_x + spacing * 2.0, y_offset, 0.0)), op: BooleanPathOperation::Xor, corner_radius: None, - children: vec![square1.id.clone(), square2.id.clone()], fills: Paints::new([Paint::from(CGColor(100, 100, 200, 255))]), strokes: Paints::new([Paint::from(CGColor(0, 0, 0, 255))]), stroke_width: 2.0, @@ -266,29 +272,25 @@ async fn demo_booleans() -> Scene { }; // Collect IDs before moving nodes - all_shape_ids.push(square1.id.clone()); - all_shape_ids.push(square2.id.clone()); - all_shape_ids.push(text.id.clone()); - all_shape_ids.push(bool_node.id.clone()); - - // Insert all nodes - repository.insert(Node::Rectangle(square1)); - repository.insert(Node::Rectangle(square2)); - repository.insert(Node::TextSpan(text)); - repository.insert(Node::BooleanOperation(bool_node)); + // Add boolean operation to root, then add operands to it + let bool_id = graph.append_child( + Node::BooleanOperation(bool_node), + Parent::NodeId(root_container_id.clone()), + ); + graph.append_children( + vec![Node::Rectangle(square1), Node::Rectangle(square2)], + Parent::NodeId(bool_id), + ); + graph.append_child( + Node::TextSpan(text), + Parent::NodeId(root_container_id.clone()), + ); } - // Set up the root container - root_container_node.children.extend(all_shape_ids); - let root_container_id = root_container_node.id.clone(); - repository.insert(Node::Container(root_container_node)); - Scene { - id: "scene".to_string(), name: "Boolean Operations Demo".to_string(), - children: vec![root_container_id], - nodes: repository, background_color: Some(CGColor(250, 250, 250, 255)), + graph, } } diff --git a/crates/grida-canvas/examples/grida_container.rs b/crates/grida-canvas/examples/grida_container.rs index afeed3cfc4..3a27106076 100644 --- a/crates/grida-canvas/examples/grida_container.rs +++ b/crates/grida-canvas/examples/grida_container.rs @@ -1,13 +1,12 @@ use cg::cg::types::*; use cg::node::factory::NodeFactory; -use cg::node::repository::NodeRepository; +use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::*; use cg::window; use math2::transform::AffineTransform; async fn demo_clip() -> Scene { let nf = NodeFactory::new(); - let mut repository = NodeRepository::new(); // Create a single container with solid fill let mut container = nf.create_container_node(); @@ -42,22 +41,15 @@ async fn demo_clip() -> Scene { ellipse.strokes = Paints::new([Paint::from(CGColor(50, 150, 50, 255))]); ellipse.stroke_width = 2.0; - // Add nodes to repository and collect their IDs - let ellipse_id = ellipse.id.clone(); - repository.insert(Node::Ellipse(ellipse)); - - // Add ellipse as child of container - container.children = vec![ellipse_id]; - - let container_id = container.id.clone(); - repository.insert(Node::Container(container)); + // Build scene graph + let mut graph = SceneGraph::new(); + let container_id = graph.append_child(Node::Container(container), Parent::Root); + graph.append_child(Node::Ellipse(ellipse), Parent::NodeId(container_id)); Scene { - id: "scene".to_string(), name: "Simple Container Demo".to_string(), - children: vec![container_id], - nodes: repository, background_color: None, + graph, } } diff --git a/crates/grida-canvas/examples/grida_effects.rs b/crates/grida-canvas/examples/grida_effects.rs index d9236a3b1d..b39dfebcdf 100644 --- a/crates/grida-canvas/examples/grida_effects.rs +++ b/crates/grida-canvas/examples/grida_effects.rs @@ -1,13 +1,13 @@ use cg::cg::types::*; use cg::node::factory::NodeFactory; -use cg::node::repository::NodeRepository; +use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::*; use cg::window; use math2::transform::AffineTransform; async fn demo_effects() -> Scene { let nf = NodeFactory::new(); - let mut repository = NodeRepository::new(); + let mut graph = SceneGraph::new(); // Create a root container node let mut root_container_node = nf.create_container_node(); @@ -17,7 +17,7 @@ async fn demo_effects() -> Scene { }; root_container_node.name = Some("Root Container".to_string()); - let mut all_effect_ids = Vec::new(); + let root_container_id = graph.append_child(Node::Container(root_container_node), Parent::Root); let spacing = 200.0; let start_x = 50.0; let base_size = 150.0; @@ -42,8 +42,10 @@ async fn demo_effects() -> Scene { spread: 0.0, color: CGColor(0, 0, 0, 128), })]); - all_effect_ids.push(rect.id.clone()); - repository.insert(Node::Rectangle(rect)); + graph.append_child( + Node::Rectangle(rect), + Parent::NodeId(root_container_id.clone()), + ); } else { // Last two shapes as regular polygons let mut polygon = nf.create_regular_polygon_node(); @@ -62,8 +64,10 @@ async fn demo_effects() -> Scene { spread: 2.0 * (i + 1) as f32, color: CGColor(0, 0, 0, 128), })]); - all_effect_ids.push(polygon.id.clone()); - repository.insert(Node::RegularPolygon(polygon)); + graph.append_child( + Node::RegularPolygon(polygon), + Parent::NodeId(root_container_id.clone()), + ); } } @@ -84,8 +88,10 @@ async fn demo_effects() -> Scene { LayerEffects::from_array(vec![FilterEffect::LayerBlur(FeGaussianBlur { radius: 4.0 * (i + 1) as f32, })]); - all_effect_ids.push(rect.id.clone()); - repository.insert(Node::Rectangle(rect)); + graph.append_child( + Node::Rectangle(rect), + Parent::NodeId(root_container_id.clone()), + ); } else { // Last two shapes as regular polygons let mut polygon = nf.create_regular_polygon_node(); @@ -101,8 +107,10 @@ async fn demo_effects() -> Scene { LayerEffects::from_array(vec![FilterEffect::LayerBlur(FeGaussianBlur { radius: 4.0 * (i + 1) as f32, })]); - all_effect_ids.push(polygon.id.clone()); - repository.insert(Node::RegularPolygon(polygon)); + graph.append_child( + Node::RegularPolygon(polygon), + Parent::NodeId(root_container_id.clone()), + ); } } @@ -135,8 +143,10 @@ async fn demo_effects() -> Scene { blend_mode: BlendMode::Normal, active: true, })); - let vivid_gradient_rect_id = vivid_gradient_rect.id.clone(); - repository.insert(Node::Rectangle(vivid_gradient_rect)); + graph.append_child( + Node::Rectangle(vivid_gradient_rect), + Parent::NodeId(root_container_id.clone()), + ); for i in 0..6 { if i < 3 { @@ -154,8 +164,10 @@ async fn demo_effects() -> Scene { LayerEffects::from_array(vec![FilterEffect::BackdropBlur(FeGaussianBlur { radius: 8.0 * (i + 1) as f32, })]); - all_effect_ids.push(blur_rect.id.clone()); - repository.insert(Node::Rectangle(blur_rect)); + graph.append_child( + Node::Rectangle(blur_rect), + Parent::NodeId(root_container_id.clone()), + ); } else { // Last two shapes as regular polygons let mut blur_polygon = nf.create_regular_polygon_node(); @@ -171,8 +183,10 @@ async fn demo_effects() -> Scene { LayerEffects::from_array(vec![FilterEffect::BackdropBlur(FeGaussianBlur { radius: 8.0 * (i + 1) as f32, })]); - all_effect_ids.push(blur_polygon.id.clone()); - repository.insert(Node::RegularPolygon(blur_polygon)); + graph.append_child( + Node::RegularPolygon(blur_polygon), + Parent::NodeId(root_container_id.clone()), + ); } } @@ -196,8 +210,10 @@ async fn demo_effects() -> Scene { spread: 0.0, color: CGColor(0, 0, 0, 100), })]); - all_effect_ids.push(rect.id.clone()); - repository.insert(Node::Rectangle(rect)); + graph.append_child( + Node::Rectangle(rect), + Parent::NodeId(root_container_id.clone()), + ); } else { // Last three shapes as regular polygons let mut polygon = nf.create_regular_polygon_node(); @@ -216,8 +232,10 @@ async fn demo_effects() -> Scene { spread: 2.0 * (i + 1) as f32, color: CGColor(0, 0, 0, 100), })]); - all_effect_ids.push(polygon.id.clone()); - repository.insert(Node::RegularPolygon(polygon)); + graph.append_child( + Node::RegularPolygon(polygon), + Parent::NodeId(root_container_id.clone()), + ); } } @@ -284,8 +302,10 @@ async fn demo_effects() -> Scene { }; rect.effects = LayerEffects::from_array(effects); - all_effect_ids.push(rect.id.clone()); - repository.insert(Node::Rectangle(rect)); + graph.append_child( + Node::Rectangle(rect), + Parent::NodeId(root_container_id.clone()), + ); } else { // Last three shapes as regular polygons with multiple effects let mut polygon = nf.create_regular_polygon_node(); @@ -341,23 +361,17 @@ async fn demo_effects() -> Scene { }; polygon.effects = LayerEffects::from_array(effects); - all_effect_ids.push(polygon.id.clone()); - repository.insert(Node::RegularPolygon(polygon)); + graph.append_child( + Node::RegularPolygon(polygon), + Parent::NodeId(root_container_id.clone()), + ); } } - // Set up the root container - root_container_node.children = vec![vivid_gradient_rect_id]; - root_container_node.children.extend(all_effect_ids); - let root_container_id = root_container_node.id.clone(); - repository.insert(Node::Container(root_container_node)); - Scene { - id: "scene".to_string(), name: "Effects Demo".to_string(), - children: vec![root_container_id], - nodes: repository, background_color: Some(CGColor(250, 250, 250, 255)), + graph, } } diff --git a/crates/grida-canvas/examples/grida_fills.rs b/crates/grida-canvas/examples/grida_fills.rs index 3da39a7987..01c167f76b 100644 --- a/crates/grida-canvas/examples/grida_fills.rs +++ b/crates/grida-canvas/examples/grida_fills.rs @@ -1,13 +1,13 @@ use cg::cg::types::*; use cg::node::factory::NodeFactory; -use cg::node::repository::NodeRepository; +use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::*; use cg::window; use math2::transform::AffineTransform; async fn demo_fills() -> Scene { let nf = NodeFactory::new(); - let mut repository = NodeRepository::new(); + let mut graph = SceneGraph::new(); // Root container let mut root = nf.create_container_node(); @@ -17,7 +17,8 @@ async fn demo_fills() -> Scene { height: 800.0, }; - let mut ids = Vec::new(); + let root_id = graph.append_child(Node::Container(root), Parent::Root); + let spacing = 200.0; let start_x = 100.0; let base_y = 100.0; @@ -37,8 +38,10 @@ async fn demo_fills() -> Scene { Paint::from(CGColor(0, 0, 255, 255)), ]); multi_solid_rect.stroke_width = 3.0; - ids.push(multi_solid_rect.id.clone()); - repository.insert(Node::Rectangle(multi_solid_rect)); + graph.append_child( + Node::Rectangle(multi_solid_rect), + Parent::NodeId(root_id.clone()), + ); // 2. Rectangle with solid + linear gradient fills let mut solid_gradient_rect = nf.create_rectangle_node(); @@ -69,8 +72,10 @@ async fn demo_fills() -> Scene { }), ]); solid_gradient_rect.stroke_width = 3.0; - ids.push(solid_gradient_rect.id.clone()); - repository.insert(Node::Rectangle(solid_gradient_rect)); + graph.append_child( + Node::Rectangle(solid_gradient_rect), + Parent::NodeId(root_id.clone()), + ); // 3. Rectangle with solid + radial gradient fills let mut solid_radial_rect = nf.create_rectangle_node(); @@ -105,8 +110,10 @@ async fn demo_fills() -> Scene { }), ]); solid_radial_rect.stroke_width = 3.0; - ids.push(solid_radial_rect.id.clone()); - repository.insert(Node::Rectangle(solid_radial_rect)); + graph.append_child( + Node::Rectangle(solid_radial_rect), + Parent::NodeId(root_id.clone()), + ); // 4. Rectangle with linear + radial gradient fills let mut gradient_gradient_rect = nf.create_rectangle_node(); @@ -154,8 +161,10 @@ async fn demo_fills() -> Scene { }), ]); gradient_gradient_rect.stroke_width = 3.0; - ids.push(gradient_gradient_rect.id.clone()); - repository.insert(Node::Rectangle(gradient_gradient_rect)); + graph.append_child( + Node::Rectangle(gradient_gradient_rect), + Parent::NodeId(root_id.clone()), + ); // 5. Ellipse with multiple radial gradients (concentric circles effect) let mut multi_radial_ellipse = nf.create_ellipse_node(); @@ -216,8 +225,10 @@ async fn demo_fills() -> Scene { }), ]); multi_radial_ellipse.stroke_width = 3.0; - ids.push(multi_radial_ellipse.id.clone()); - repository.insert(Node::Ellipse(multi_radial_ellipse)); + graph.append_child( + Node::Ellipse(multi_radial_ellipse), + Parent::NodeId(root_id.clone()), + ); // 6. Polygon with solid + linear gradient + radial gradient let pentagon_points = (0..5) @@ -272,8 +283,10 @@ async fn demo_fills() -> Scene { }), ]); complex_fill_polygon.stroke_width = 4.0; - ids.push(complex_fill_polygon.id.clone()); - repository.insert(Node::Polygon(complex_fill_polygon)); + graph.append_child( + Node::Polygon(complex_fill_polygon), + Parent::NodeId(root_id.clone()), + ); // 7. Regular polygon with multiple linear gradients at different angles let mut multi_linear_polygon = nf.create_regular_polygon_node(); @@ -336,8 +349,10 @@ async fn demo_fills() -> Scene { }), ]); multi_linear_polygon.stroke_width = 3.0; - ids.push(multi_linear_polygon.id.clone()); - repository.insert(Node::RegularPolygon(multi_linear_polygon)); + graph.append_child( + Node::RegularPolygon(multi_linear_polygon), + Parent::NodeId(root_id.clone()), + ); // 8. Container with multiple fills (demonstrating container fill capability) let mut multi_fill_container = nf.create_container_node(); @@ -372,19 +387,14 @@ async fn demo_fills() -> Scene { }), ]); multi_fill_container.stroke_width = 3.0; - ids.push(multi_fill_container.id.clone()); - repository.insert(Node::Container(multi_fill_container)); - - // Add all nodes to root - root.children = ids.clone(); - let root_id = root.id.clone(); - repository.insert(Node::Container(root)); + graph.append_child( + Node::Container(multi_fill_container), + Parent::NodeId(root_id.clone()), + ); Scene { - id: "scene".to_string(), name: "Fills Demo".to_string(), - children: vec![root_id], - nodes: repository, + graph, background_color: Some(CGColor(240, 240, 240, 255)), } } diff --git a/crates/grida-canvas/examples/grida_gradients.rs b/crates/grida-canvas/examples/grida_gradients.rs index 90cb93e0d8..6d6f9000ed 100644 --- a/crates/grida-canvas/examples/grida_gradients.rs +++ b/crates/grida-canvas/examples/grida_gradients.rs @@ -1,13 +1,13 @@ use cg::cg::types::*; use cg::node::factory::NodeFactory; -use cg::node::repository::NodeRepository; +use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::*; use cg::window; use math2::transform::AffineTransform; async fn demo_gradients() -> Scene { let nf = NodeFactory::new(); - let mut repository = NodeRepository::new(); + let mut graph = SceneGraph::new(); // root container let mut root = nf.create_container_node(); @@ -17,7 +17,8 @@ async fn demo_gradients() -> Scene { height: 800.0, }; - let mut ids = Vec::new(); + let root_id = graph.append_child(Node::Container(root), Parent::Root); + let spacing = 160.0; let start_x = 60.0; let base = 120.0; @@ -49,8 +50,7 @@ async fn demo_gradients() -> Scene { blend_mode: BlendMode::Normal, active: true, })); - ids.push(rect.id.clone()); - repository.insert(Node::Rectangle(rect)); + graph.append_child(Node::Rectangle(rect), Parent::NodeId(root_id.clone())); } // Radial gradient fills @@ -82,8 +82,7 @@ async fn demo_gradients() -> Scene { blend_mode: BlendMode::Normal, active: true, })); - ids.push(rect.id.clone()); - repository.insert(Node::Rectangle(rect)); + graph.append_child(Node::Rectangle(rect), Parent::NodeId(root_id.clone())); } // Linear gradient strokes @@ -115,8 +114,7 @@ async fn demo_gradients() -> Scene { active: true, })]); rect.stroke_width = 8.0; - ids.push(rect.id.clone()); - repository.insert(Node::Rectangle(rect)); + graph.append_child(Node::Rectangle(rect), Parent::NodeId(root_id.clone())); } // Radial gradient strokes @@ -150,20 +148,13 @@ async fn demo_gradients() -> Scene { active: true, })]); rect.stroke_width = 8.0; - ids.push(rect.id.clone()); - repository.insert(Node::Rectangle(rect)); + graph.append_child(Node::Rectangle(rect), Parent::NodeId(root_id.clone())); } - root.children = ids.clone(); - let root_id = root.id.clone(); - repository.insert(Node::Container(root)); - Scene { - id: "scene".to_string(), name: "Gradients Demo".to_string(), - children: vec![root_id], - nodes: repository, background_color: Some(CGColor(250, 250, 250, 255)), + graph, } } diff --git a/crates/grida-canvas/examples/grida_image.rs b/crates/grida-canvas/examples/grida_image.rs index 61e1763783..8a2203f3b0 100644 --- a/crates/grida-canvas/examples/grida_image.rs +++ b/crates/grida-canvas/examples/grida_image.rs @@ -1,6 +1,6 @@ use cg::cg::{types::*, *}; use cg::node::factory::NodeFactory; -use cg::node::repository::NodeRepository; +use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::*; use cg::resources::{hash_bytes, load_image}; use cg::window; @@ -46,21 +46,14 @@ async fn demo_image() -> (Scene, Vec) { rect1.strokes = Paints::new([Paint::from(CGColor(255, 0, 0, 255))]); rect1.stroke_width = 2.0; - let mut repository = NodeRepository::new(); + let mut graph = SceneGraph::new(); - let rect1_id = rect1.id.clone(); - - repository.insert(Node::Rectangle(rect1)); - - root.children = vec![rect1_id]; - let root_id = root.id.clone(); - repository.insert(Node::Container(root)); + let root_id = graph.append_child(Node::Container(root), Parent::Root); + graph.append_child(Node::Rectangle(rect1), Parent::NodeId(root_id)); let scene = Scene { - id: "scene".to_string(), name: "Images Demo".to_string(), - children: vec![root_id], - nodes: repository, + graph, background_color: Some(CGColor(250, 250, 250, 255)), }; diff --git a/crates/grida-canvas/examples/grida_images.rs b/crates/grida-canvas/examples/grida_images.rs index 43927106f8..ca1d5e476c 100644 --- a/crates/grida-canvas/examples/grida_images.rs +++ b/crates/grida-canvas/examples/grida_images.rs @@ -1,6 +1,6 @@ use cg::cg::{types::*, *}; use cg::node::factory::NodeFactory; -use cg::node::repository::NodeRepository; +use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::*; use cg::resources::{hash_bytes, load_image}; use cg::window; @@ -234,40 +234,31 @@ async fn demo_images() -> (Scene, Vec) { rect9.strokes = Paints::new([Paint::from(CGColor(128, 0, 128, 255))]); rect9.stroke_width = 2.0; - let mut repository = NodeRepository::new(); + let mut graph = SceneGraph::new(); - let rect1_id = rect1.id.clone(); - let rect2_id = rect2.id.clone(); - let rect3_id = rect3.id.clone(); - let rect4_id = rect4.id.clone(); - let rect5_id = rect5.id.clone(); - let rect6_id = rect6.id.clone(); - let rect7_id = rect7.id.clone(); - let rect8_id = rect8.id.clone(); - let rect9_id = rect9.id.clone(); + // Add root container first + let root_id = graph.append_child(Node::Container(root), Parent::Root); - repository.insert(Node::Rectangle(rect1)); - repository.insert(Node::Rectangle(rect2)); - repository.insert(Node::Rectangle(rect3)); - repository.insert(Node::Rectangle(rect4)); - repository.insert(Node::Rectangle(rect5)); - repository.insert(Node::Rectangle(rect6)); - repository.insert(Node::Rectangle(rect7)); - repository.insert(Node::Rectangle(rect8)); - repository.insert(Node::Rectangle(rect9)); - - root.children = vec![ - rect1_id, rect2_id, rect3_id, rect4_id, rect5_id, rect6_id, rect7_id, rect8_id, rect9_id, - ]; - let root_id = root.id.clone(); - repository.insert(Node::Container(root)); + // Add all rectangles to root container + graph.append_children( + vec![ + Node::Rectangle(rect1), + Node::Rectangle(rect2), + Node::Rectangle(rect3), + Node::Rectangle(rect4), + Node::Rectangle(rect5), + Node::Rectangle(rect6), + Node::Rectangle(rect7), + Node::Rectangle(rect8), + Node::Rectangle(rect9), + ], + Parent::NodeId(root_id), + ); let scene = Scene { - id: "scene".to_string(), name: "Images Demo".to_string(), - children: vec![root_id], - nodes: repository, background_color: Some(CGColor(250, 250, 250, 255)), + graph, }; (scene, bytes) diff --git a/crates/grida-canvas/examples/grida_lines.rs b/crates/grida-canvas/examples/grida_lines.rs index cde2a3b566..ae44a83dd8 100644 --- a/crates/grida-canvas/examples/grida_lines.rs +++ b/crates/grida-canvas/examples/grida_lines.rs @@ -1,6 +1,6 @@ use cg::cg::types::*; use cg::node::factory::NodeFactory; -use cg::node::repository::NodeRepository; +use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::*; use cg::window; use math2::transform::AffineTransform; @@ -15,8 +15,7 @@ async fn demo_lines() -> Scene { height: 600.0, }; - let mut repo = NodeRepository::new(); - let mut ids = Vec::new(); + let mut graph = SceneGraph::new(); let start_x = 100.0; let start_y = 100.0; @@ -33,8 +32,6 @@ async fn demo_lines() -> Scene { }; line_basic.strokes = Paints::new([Paint::from(CGColor(0, 0, 0, 255))]); line_basic.stroke_width = 2.0; - ids.push(line_basic.id.clone()); - repo.insert(Node::Line(line_basic)); // Outside aligned thick line let mut line_outside = nf.create_line_node(); @@ -47,8 +44,6 @@ async fn demo_lines() -> Scene { line_outside.strokes = Paints::new([Paint::from(CGColor(255, 0, 0, 255))]); line_outside.stroke_width = 8.0; line_outside._data_stroke_align = StrokeAlign::Outside; - ids.push(line_outside.id.clone()); - repo.insert(Node::Line(line_outside)); // Dashed line let mut line_dashed = nf.create_line_node(); @@ -61,8 +56,6 @@ async fn demo_lines() -> Scene { line_dashed.strokes = Paints::new([Paint::from(CGColor(0, 0, 255, 255))]); line_dashed.stroke_width = 4.0; line_dashed.stroke_dash_array = Some(vec![10.0, 5.0]); - ids.push(line_dashed.id.clone()); - repo.insert(Node::Line(line_dashed)); // Gradient stroke line let mut line_gradient = nf.create_line_node(); @@ -89,8 +82,6 @@ async fn demo_lines() -> Scene { active: true, })]); line_gradient.stroke_width = 6.0; - ids.push(line_gradient.id.clone()); - repo.insert(Node::Line(line_gradient)); // Rotated diagonal line let mut line_rotated = nf.create_line_node(); @@ -103,20 +94,24 @@ async fn demo_lines() -> Scene { }; line_rotated.strokes = Paints::new([Paint::from(CGColor(0, 128, 128, 255))]); line_rotated.stroke_width = 4.0; - ids.push(line_rotated.id.clone()); - repo.insert(Node::Line(line_rotated)); - // Set up root container - root.children = ids; - let root_id = root.id.clone(); - repo.insert(Node::Container(root)); + // Set up root container and add all lines + let root_id = graph.append_child(Node::Container(root), Parent::Root); + graph.append_children( + vec![ + Node::Line(line_basic), + Node::Line(line_outside), + Node::Line(line_dashed), + Node::Line(line_gradient), + Node::Line(line_rotated), + ], + Parent::NodeId(root_id), + ); Scene { - id: "scene".to_string(), name: "LineNode Demo".to_string(), - children: vec![root_id], - nodes: repo, background_color: Some(CGColor(250, 250, 250, 255)), + graph, } } diff --git a/crates/grida-canvas/examples/grida_mask.rs b/crates/grida-canvas/examples/grida_mask.rs index 464511fac8..7dc3c2fa64 100644 --- a/crates/grida-canvas/examples/grida_mask.rs +++ b/crates/grida-canvas/examples/grida_mask.rs @@ -1,15 +1,16 @@ use cg::cg::types::*; // import style per repo convention [[memory:8559399]] use cg::node::factory::NodeFactory; -use cg::node::repository::NodeRepository; +use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::*; use cg::window; use math2::transform::AffineTransform; fn build_demo_content( nf: &NodeFactory, - repository: &mut NodeRepository, + graph: &mut SceneGraph, origin: (f32, f32), size: (f32, f32), + parent: Parent, ) -> Vec { let (ox, oy) = origin; let (w, h) = size; @@ -24,8 +25,7 @@ fn build_demo_content( }; a.corner_radius = RectangularCornerRadius::circular(12.0); a.set_fill(CGColor(255, 99, 71, 255).into()); - let a_id = a.id.clone(); - repository.insert(Node::Rectangle(a)); + let a_id = graph.append_child(Node::Rectangle(a), parent.clone()); // Content B let mut b = nf.create_rectangle_node(); @@ -37,8 +37,7 @@ fn build_demo_content( }; b.corner_radius = RectangularCornerRadius::circular(12.0); b.set_fill(CGColor(65, 105, 225, 255).into()); - let b_id = b.id.clone(); - repository.insert(Node::Rectangle(b)); + let b_id = graph.append_child(Node::Rectangle(b), parent.clone()); // Diagonal band (thin rotated rectangle) let mut band = nf.create_rectangle_node(); @@ -54,17 +53,17 @@ fn build_demo_content( }; band.corner_radius = RectangularCornerRadius::circular(8.0); band.set_fill(CGColor(60, 179, 113, 200).into()); - let band_id = band.id.clone(); - repository.insert(Node::Rectangle(band)); + let band_id = graph.append_child(Node::Rectangle(band), parent.clone()); vec![a_id, b_id, band_id] } fn build_geometry_mask( nf: &NodeFactory, - repository: &mut NodeRepository, + graph: &mut SceneGraph, origin: (f32, f32), size: (f32, f32), + parent: Parent, ) -> NodeId { let (ox, oy) = origin; let (w, h) = size; @@ -78,16 +77,15 @@ fn build_geometry_mask( }; mask.set_fill(CGColor(0, 0, 0, 255).into()); mask.mask = Some(LayerMaskType::Geometry); - let id = mask.id.clone(); - repository.insert(Node::Ellipse(mask)); - id + graph.append_child(Node::Ellipse(mask), parent) } fn build_alpha_mask( nf: &NodeFactory, - repository: &mut NodeRepository, + graph: &mut SceneGraph, origin: (f32, f32), size: (f32, f32), + parent: Parent, ) -> NodeId { let (ox, oy) = origin; let (w, h) = size; @@ -116,16 +114,15 @@ fn build_alpha_mask( active: true, })]); mask.mask = Some(LayerMaskType::Image(ImageMaskType::Alpha)); - let id = mask.id.clone(); - repository.insert(Node::Rectangle(mask)); - id + graph.append_child(Node::Rectangle(mask), parent) } fn build_luminance_mask( nf: &NodeFactory, - repository: &mut NodeRepository, + graph: &mut SceneGraph, origin: (f32, f32), size: (f32, f32), + parent: Parent, ) -> NodeId { let (ox, oy) = origin; let (w, h) = size; @@ -154,14 +151,12 @@ fn build_luminance_mask( active: true, })]); mask.mask = Some(LayerMaskType::Image(ImageMaskType::Luminance)); - let id = mask.id.clone(); - repository.insert(Node::Rectangle(mask)); - id + graph.append_child(Node::Rectangle(mask), parent) } async fn demo_mask_panels() -> Scene { let nf = NodeFactory::new(); - let mut repository = NodeRepository::new(); + let mut graph = SceneGraph::new(); // Root container let mut root = nf.create_container_node(); @@ -172,6 +167,8 @@ async fn demo_mask_panels() -> Scene { root.clip = false; root.set_fill(CGColor(255, 255, 255, 255).into()); + let root_id = graph.append_child(Node::Container(root), Parent::Root); + // Panel layout let margin = 20.0; let panel_w = (width - 5.0 * margin) / 4.0; @@ -186,8 +183,6 @@ async fn demo_mask_panels() -> Scene { Some(LayerMaskType::Image(ImageMaskType::Luminance)), ]; - let mut root_children = Vec::new(); - for kind in kinds { // Panel container per kind let mut panel = nf.create_container_node(); @@ -200,48 +195,52 @@ async fn demo_mask_panels() -> Scene { panel.corner_radius = RectangularCornerRadius::circular(6.0); panel.set_fill(CGColor(245, 245, 245, 255).into()); - // Build children inside panel - let mut children: Vec = Vec::new(); + // Add panel to root first + let panel_id = graph.append_child(Node::Container(panel), Parent::NodeId(root_id.clone())); - // Content first - let mut content_ids = - build_demo_content(&nf, &mut repository, (0.0, 0.0), (panel_w, panel_h)); - children.append(&mut content_ids); + // Build content inside panel + let _content_ids: Vec = build_demo_content( + &nf, + &mut graph, + (0.0, 0.0), + (panel_w, panel_h), + Parent::NodeId(panel_id.clone()), + ); // Mask last (topmost) — flat list model: mask consumes preceding siblings if let Some(k) = kind { - let mask_id = match k { - LayerMaskType::Geometry => { - build_geometry_mask(&nf, &mut repository, (0.0, 0.0), (panel_w, panel_h)) - } - LayerMaskType::Image(ImageMaskType::Alpha) => { - build_alpha_mask(&nf, &mut repository, (0.0, 0.0), (panel_w, panel_h)) - } - LayerMaskType::Image(ImageMaskType::Luminance) => { - build_luminance_mask(&nf, &mut repository, (0.0, 0.0), (panel_w, panel_h)) - } + let _mask_id = match k { + LayerMaskType::Geometry => build_geometry_mask( + &nf, + &mut graph, + (0.0, 0.0), + (panel_w, panel_h), + Parent::NodeId(panel_id.clone()), + ), + LayerMaskType::Image(ImageMaskType::Alpha) => build_alpha_mask( + &nf, + &mut graph, + (0.0, 0.0), + (panel_w, panel_h), + Parent::NodeId(panel_id.clone()), + ), + LayerMaskType::Image(ImageMaskType::Luminance) => build_luminance_mask( + &nf, + &mut graph, + (0.0, 0.0), + (panel_w, panel_h), + Parent::NodeId(panel_id.clone()), + ), }; - children.push(mask_id); } - panel.children = children; - let panel_id = panel.id.clone(); - repository.insert(Node::Container(panel)); - root_children.push(panel_id); - left += panel_w + margin; } - root.children = root_children; - let root_id = root.id.clone(); - repository.insert(Node::Container(root)); - Scene { - id: "scene".to_string(), name: "Mask Modes Demo".to_string(), - children: vec![root_id], - nodes: repository, background_color: None, + graph, } } diff --git a/crates/grida-canvas/examples/grida_nested.rs b/crates/grida-canvas/examples/grida_nested.rs index f931cb29c7..d022995fa4 100644 --- a/crates/grida-canvas/examples/grida_nested.rs +++ b/crates/grida-canvas/examples/grida_nested.rs @@ -1,85 +1,92 @@ use cg::cg::types::*; use cg::node::factory::NodeFactory; -use cg::node::repository::NodeRepository; +use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::*; use cg::window; use math2::transform::AffineTransform; async fn demo_nested() -> Scene { let nf = NodeFactory::new(); - let mut repository = NodeRepository::new(); - let n = 5; // number of nesting levels - - // Create innermost rectangle - let mut rect = nf.create_rectangle_node(); - rect.name = Some("Inner Rect".to_string()); - rect.size = Size { - width: 100.0, - height: 100.0, - }; - rect.set_fill(Paint::from(CGColor(255, 0, 0, 255))); - let mut current_id = rect.id.clone(); - repository.insert(Node::Rectangle(rect)); - - // Create nested structure - for i in 0..n { - if i % 2 == 0 { - // Create group with rotation transform - let mut group = nf.create_group_node(); - group.name = Some(format!("Group {}", i)); - group.transform = Some(AffineTransform::new( - 50.0 * (i as f32 + 1.0), // x offset - 50.0 * (i as f32 + 1.0), // y offset - 0.0, - )); - - // Add a rectangle to the group - let mut group_rect = nf.create_rectangle_node(); - group_rect.name = Some(format!("Group {} Rect", i)); - group_rect.size = Size { - width: 100.0, - height: 100.0, - }; - group_rect.set_fill(Paint::from(CGColor(0, 255, 0, 255))); - let group_rect_id = group_rect.id.clone(); - repository.insert(Node::Rectangle(group_rect)); - - group.children = vec![current_id, group_rect_id]; - current_id = group.id.clone(); - repository.insert(Node::Group(group)); - } else { - // Create container with scale transform - let mut container = nf.create_container_node(); - container.name = Some(format!("Container {}", i)); - container.transform = AffineTransform::new( - -30.0 * (i as f32 + 1.0), // x offset - -30.0 * (i as f32 + 1.0), // y offset - 0.0, - ); - - // Add a rectangle to the container - let mut container_rect = nf.create_rectangle_node(); - container_rect.name = Some(format!("Container {} Rect", i)); - container_rect.size = Size { - width: 100.0, - height: 100.0, - }; - container_rect.set_fill(Paint::from(CGColor(0, 0, 255, 255))); - let container_rect_id = container_rect.id.clone(); - repository.insert(Node::Rectangle(container_rect)); - - container.children = vec![current_id, container_rect_id]; - current_id = container.id.clone(); - repository.insert(Node::Container(container)); - } + let mut graph = SceneGraph::new(); + + // Demonstrate nested transformations and hierarchy + // Each level applies cumulative transformations: translation + rotation + scale + // Visual: concentric rotating squares that get progressively smaller and rotated + + let levels: i32 = 6; // Number of nesting levels + let base_size = 400.0; + + // Build from outermost to innermost + let mut current_parent = Parent::Root; + + for i in 0..levels { + let depth_ratio = (i as f32) / (levels as f32); + let size_reduction = 0.85_f32; // Each level is 85% of parent + let current_size = base_size * size_reduction.powi(i as i32); + let rotation = 15.0_f32.to_radians() * (i as f32); // Rotate 15 degrees per level + + // Create a container for this level + let mut container = nf.create_container_node(); + container.name = Some(format!("Level {} Container", i)); + + // Each level is centered in its parent with rotation + container.transform = AffineTransform::new( + current_size * 0.075, // Small offset for visual clarity + current_size * 0.075, + rotation, + ); + + container.size = Size { + width: current_size, + height: current_size, + }; + container.corner_radius = RectangularCornerRadius::circular(8.0); + + // Color gradient from blue (outer) to red (inner) + let r = (255.0 * depth_ratio) as u8; + let g = (100.0 * (1.0 - depth_ratio)) as u8; + let b = (255.0 * (1.0 - depth_ratio)) as u8; + container.set_fill(Paint::from(CGColor(r, g, b, 200))); + + // Add stroke to show boundaries + container.strokes = Paints::new([Paint::from(CGColor(255, 255, 255, 255))]); + container.stroke_width = 2.0; + + let container_id = graph.append_child(Node::Container(container), current_parent); + + // Add a label at each level + let mut label = nf.create_text_span_node(); + label.name = Some(format!("Level {} Label", i)); + label.transform = AffineTransform::new(10.0, 10.0, 0.0); + label.text = format!("Level {}", i); + label.text_style = TextStyleRec::from_font("", 14.0); + label.fills = Paints::new([Paint::from(CGColor(255, 255, 255, 255))]); + graph.append_child(Node::TextSpan(label), Parent::NodeId(container_id.clone())); + + // Move to next level (this container becomes the parent for the next iteration) + current_parent = Parent::NodeId(container_id); } + // Add final innermost content - a star + let mut star = nf.create_regular_star_polygon_node(); + star.name = Some("Center Star".to_string()); + let final_size = base_size * 0.85_f32.powi(levels); + star.transform = AffineTransform::new(final_size * 0.25, final_size * 0.25, 0.0); + star.size = Size { + width: final_size * 0.5, + height: final_size * 0.5, + }; + star.point_count = 5; + star.inner_radius = 0.4; + star.set_fill(Paint::from(CGColor(255, 255, 0, 255))); + star.strokes = Paints::new([Paint::from(CGColor(255, 200, 0, 255))]); + star.stroke_width = 3.0; + graph.append_child(Node::RegularStarPolygon(star), current_parent); + Scene { - id: "nested".to_string(), name: "Nested Demo".to_string(), - children: vec![current_id], - nodes: repository, background_color: Some(CGColor(250, 250, 250, 255)), + graph, } } diff --git a/crates/grida-canvas/examples/grida_paint.rs b/crates/grida-canvas/examples/grida_paint.rs index 8ad0d02d4d..8b6a622ae7 100644 --- a/crates/grida-canvas/examples/grida_paint.rs +++ b/crates/grida-canvas/examples/grida_paint.rs @@ -1,13 +1,13 @@ use cg::cg::types::*; use cg::node::factory::NodeFactory; -use cg::node::repository::NodeRepository; +use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::*; use cg::window; use math2::transform::AffineTransform; async fn demo_paints() -> Scene { let nf = NodeFactory::new(); - let mut repository = NodeRepository::new(); + let mut graph = SceneGraph::new(); // Create a root container node let mut root_container_node = nf.create_container_node(); @@ -17,7 +17,7 @@ async fn demo_paints() -> Scene { height: 1080.0, }; - let mut all_shape_ids = Vec::new(); + let root_container_id = graph.append_child(Node::Container(root_container_node), Parent::Root); let spacing = 100.0; let start_x = 50.0; let base_size = 80.0; @@ -39,8 +39,10 @@ async fn demo_paints() -> Scene { 50 + (i * 20) as u8, 255, ))); - all_shape_ids.push(rect.id.clone()); - repository.insert(Node::Rectangle(rect)); + graph.append_child( + Node::Rectangle(rect), + Parent::NodeId(root_container_id.clone()), + ); } // Linear Gradient Row @@ -74,8 +76,10 @@ async fn demo_paints() -> Scene { blend_mode: BlendMode::Normal, active: true, })); - all_shape_ids.push(rect.id.clone()); - repository.insert(Node::Rectangle(rect)); + graph.append_child( + Node::Rectangle(rect), + Parent::NodeId(root_container_id.clone()), + ); } // Radial Gradient Row @@ -110,8 +114,10 @@ async fn demo_paints() -> Scene { blend_mode: BlendMode::Normal, active: true, })); - all_shape_ids.push(rect.id.clone()); - repository.insert(Node::Rectangle(rect)); + graph.append_child( + Node::Rectangle(rect), + Parent::NodeId(root_container_id.clone()), + ); } // Stroke Solid Colors Row @@ -137,8 +143,10 @@ async fn demo_paints() -> Scene { ))]); rect.stroke_width = 4.0; // Consistent stroke width - all_shape_ids.push(rect.id.clone()); - repository.insert(Node::Rectangle(rect)); + graph.append_child( + Node::Rectangle(rect), + Parent::NodeId(root_container_id.clone()), + ); } // Stroke Linear Gradient Row @@ -177,8 +185,10 @@ async fn demo_paints() -> Scene { })]); rect.stroke_width = 4.0; // Consistent stroke width - all_shape_ids.push(rect.id.clone()); - repository.insert(Node::Rectangle(rect)); + graph.append_child( + Node::Rectangle(rect), + Parent::NodeId(root_container_id.clone()), + ); } // Stroke Radial Gradient Row @@ -218,21 +228,16 @@ async fn demo_paints() -> Scene { })]); rect.stroke_width = 4.0; // Consistent stroke width - all_shape_ids.push(rect.id.clone()); - repository.insert(Node::Rectangle(rect)); + graph.append_child( + Node::Rectangle(rect), + Parent::NodeId(root_container_id.clone()), + ); } - // Set up the root container - root_container_node.children.extend(all_shape_ids); - let root_container_id = root_container_node.id.clone(); - repository.insert(Node::Container(root_container_node)); - Scene { - id: "scene".to_string(), name: "Paints Demo".to_string(), - children: vec![root_container_id], - nodes: repository, background_color: Some(CGColor(250, 250, 250, 255)), + graph, } } diff --git a/crates/grida-canvas/examples/grida_shapes.rs b/crates/grida-canvas/examples/grida_shapes.rs index 92c2e154a3..cb46943cb5 100644 --- a/crates/grida-canvas/examples/grida_shapes.rs +++ b/crates/grida-canvas/examples/grida_shapes.rs @@ -1,6 +1,6 @@ use cg::cg::types::*; use cg::node::factory::NodeFactory; -use cg::node::repository::NodeRepository; +use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::*; use cg::window; use math2::transform::AffineTransform; @@ -16,14 +16,16 @@ async fn demo_shapes() -> Scene { height: 1200.0, }; - let mut repository = NodeRepository::new(); + let mut graph = SceneGraph::new(); - let mut all_shape_ids = Vec::new(); let spacing = 100.0; let start_x = 50.0; let base_size = 80.0; let items_per_row = 10; + // Set up the root container first + let root_container_id = graph.append_child(Node::Container(root_container_node), Parent::Root); + // Rectangle Row - demonstrating corner radius variations for i in 0..items_per_row { let mut rect = nf.create_rectangle_node(); @@ -40,8 +42,10 @@ async fn demo_shapes() -> Scene { 200 - (i * 20) as u8, 255, ))); // Fading gray - all_shape_ids.push(rect.id.clone()); - repository.insert(Node::Rectangle(rect)); + graph.append_child( + Node::Rectangle(rect), + Parent::NodeId(root_container_id.clone()), + ); } // Ellipse Row - demonstrating width/height ratio variations @@ -59,8 +63,10 @@ async fn demo_shapes() -> Scene { 200 - (i * 20) as u8, 255, ))]); // Fading gray - all_shape_ids.push(ellipse.id.clone()); - repository.insert(Node::Ellipse(ellipse)); + graph.append_child( + Node::Ellipse(ellipse), + Parent::NodeId(root_container_id.clone()), + ); } // Polygon Row - demonstrating point count variations @@ -88,8 +94,10 @@ async fn demo_shapes() -> Scene { 200 - (i * 20) as u8, 255, ))]); // Fading gray - all_shape_ids.push(polygon.id.clone()); - repository.insert(Node::Polygon(polygon)); + graph.append_child( + Node::Polygon(polygon), + Parent::NodeId(root_container_id.clone()), + ); } // Regular Polygon Row - demonstrating point count variations @@ -109,8 +117,10 @@ async fn demo_shapes() -> Scene { 255, ))]); // Fading gray regular_polygon.corner_radius = 8.0; - all_shape_ids.push(regular_polygon.id.clone()); - repository.insert(Node::RegularPolygon(regular_polygon)); + graph.append_child( + Node::RegularPolygon(regular_polygon), + Parent::NodeId(root_container_id.clone()), + ); } // Path Row - demonstrating different path patterns @@ -137,8 +147,10 @@ async fn demo_shapes() -> Scene { 200 - (i * 20) as u8, 255, ))]); // Fading gray - all_shape_ids.push(path.id.clone()); - repository.insert(Node::SVGPath(path)); + graph.append_child( + Node::SVGPath(path), + Parent::NodeId(root_container_id.clone()), + ); } // Star Polygon Row - demonstrating different point counts and inner radius variations @@ -159,8 +171,10 @@ async fn demo_shapes() -> Scene { 255, ))]); // Fading gray star.corner_radius = 8.0; - all_shape_ids.push(star.id.clone()); - repository.insert(Node::RegularStarPolygon(star)); + graph.append_child( + Node::RegularStarPolygon(star), + Parent::NodeId(root_container_id.clone()), + ); } // Arc Row - demonstrating different angle variations @@ -182,20 +196,15 @@ async fn demo_shapes() -> Scene { 255, ))]); // Fading gray arc.corner_radius = Some(8.0); - all_shape_ids.push(arc.id.clone()); - repository.insert(Node::Ellipse(arc)); + graph.append_child( + Node::Ellipse(arc), + Parent::NodeId(root_container_id.clone()), + ); } - // Set up the root container - root_container_node.children.extend(all_shape_ids); - let root_container_id = root_container_node.id.clone(); - repository.insert(Node::Container(root_container_node)); - Scene { - id: "scene".to_string(), name: "Shapes Demo".to_string(), - children: vec![root_container_id], - nodes: repository, + graph, background_color: Some(CGColor(250, 250, 250, 255)), } } diff --git a/crates/grida-canvas/examples/grida_shapes_ellipse.rs b/crates/grida-canvas/examples/grida_shapes_ellipse.rs index 50e1e38db7..9573c65814 100644 --- a/crates/grida-canvas/examples/grida_shapes_ellipse.rs +++ b/crates/grida-canvas/examples/grida_shapes_ellipse.rs @@ -1,6 +1,6 @@ use cg::cg::types::*; use cg::node::factory::NodeFactory; -use cg::node::repository::NodeRepository; +use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::*; use cg::window; use math2::transform::AffineTransform; @@ -16,9 +16,9 @@ async fn demo_ellipses() -> Scene { height: 800.0, }; - let mut repository = NodeRepository::new(); + let mut graph = SceneGraph::new(); - let mut all_ellipse_ids = Vec::new(); + let root_container_id = graph.append_child(Node::Container(root_container_node), Parent::Root); let spacing = 120.0; let start_x = 60.0; let base_size = 100.0; @@ -39,8 +39,10 @@ async fn demo_ellipses() -> Scene { 200 + (i * 5) as u8, 255, ))]); // Blue gradient - all_ellipse_ids.push(ellipse.id.clone()); - repository.insert(Node::Ellipse(ellipse)); + graph.append_child( + Node::Ellipse(ellipse), + Parent::NodeId(root_container_id.clone()), + ); } // Row 2: Ellipses with different inner radius (rings) @@ -59,8 +61,10 @@ async fn demo_ellipses() -> Scene { 50 + (i * 20) as u8, 255, ))]); // Orange gradient - all_ellipse_ids.push(ring.id.clone()); - repository.insert(Node::Ellipse(ring)); + graph.append_child( + Node::Ellipse(ring), + Parent::NodeId(root_container_id.clone()), + ); } // Row 3: Arcs with different angles @@ -80,8 +84,10 @@ async fn demo_ellipses() -> Scene { 100 + (i * 15) as u8, 255, ))]); // Green gradient - all_ellipse_ids.push(arc.id.clone()); - repository.insert(Node::Ellipse(arc)); + graph.append_child( + Node::Ellipse(arc), + Parent::NodeId(root_container_id.clone()), + ); } // Row 4: Arcs with inner radius (donut arcs) @@ -102,8 +108,10 @@ async fn demo_ellipses() -> Scene { 150 + (i * 12) as u8, 255, ))]); // Purple gradient - all_ellipse_ids.push(donut_arc.id.clone()); - repository.insert(Node::Ellipse(donut_arc)); + graph.append_child( + Node::Ellipse(donut_arc), + Parent::NodeId(root_container_id.clone()), + ); } // Row 5: Ellipses with strokes @@ -123,20 +131,15 @@ async fn demo_ellipses() -> Scene { 255, ))]); // Red gradient stroke stroke_ellipse.stroke_width = 3.0 + (i as f32 * 2.0); // 3 to 17 stroke weight - all_ellipse_ids.push(stroke_ellipse.id.clone()); - repository.insert(Node::Ellipse(stroke_ellipse)); + graph.append_child( + Node::Ellipse(stroke_ellipse), + Parent::NodeId(root_container_id.clone()), + ); } - // Set up the root container - root_container_node.children.extend(all_ellipse_ids); - let root_container_id = root_container_node.id.clone(); - repository.insert(Node::Container(root_container_node)); - Scene { - id: "scene".to_string(), name: "Ellipse Demo".to_string(), - children: vec![root_container_id], - nodes: repository, + graph, background_color: Some(CGColor(245, 245, 245, 255)), } } diff --git a/crates/grida-canvas/examples/grida_strokes.rs b/crates/grida-canvas/examples/grida_strokes.rs index fa0555c7f4..6098381954 100644 --- a/crates/grida-canvas/examples/grida_strokes.rs +++ b/crates/grida-canvas/examples/grida_strokes.rs @@ -1,6 +1,6 @@ use cg::cg::types::*; use cg::node::factory::NodeFactory; -use cg::node::repository::NodeRepository; +use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::*; use cg::window; use math2::transform::AffineTransform; @@ -16,9 +16,9 @@ async fn demo_strokes() -> Scene { height: 1200.0, }; - let mut repository = NodeRepository::new(); + let mut graph = SceneGraph::new(); - let mut all_shape_ids = Vec::new(); + let root_container_id = graph.append_child(Node::Container(root_container_node), Parent::Root); let spacing = 120.0; let start_x = 50.0; let base_size = 100.0; @@ -50,8 +50,10 @@ async fn demo_strokes() -> Scene { _ => unreachable!(), }; - all_shape_ids.push(rect.id.clone()); - repository.insert(Node::Rectangle(rect)); + graph.append_child( + Node::Rectangle(rect), + Parent::NodeId(root_container_id.clone()), + ); } // Stroke Width Demo Row @@ -72,8 +74,10 @@ async fn demo_strokes() -> Scene { rect.stroke_width = (i + 1) as f32 * 2.0; // Increasing stroke width rect.stroke_align = StrokeAlign::Center; - all_shape_ids.push(rect.id.clone()); - repository.insert(Node::Rectangle(rect)); + graph.append_child( + Node::Rectangle(rect), + Parent::NodeId(root_container_id.clone()), + ); } // Stroke with Different Shapes Row @@ -90,8 +94,10 @@ async fn demo_strokes() -> Scene { rect.set_fill(Paint::from(CGColor(0, 0, 0, 0))); rect.strokes = Paints::new([Paint::from(CGColor(0, 0, 0, 255))]); rect.stroke_width = 4.0; - all_shape_ids.push(rect.id.clone()); - repository.insert(Node::Rectangle(rect)); + graph.append_child( + Node::Rectangle(rect), + Parent::NodeId(root_container_id.clone()), + ); // Ellipse let mut ellipse = nf.create_ellipse_node(); @@ -104,8 +110,10 @@ async fn demo_strokes() -> Scene { ellipse.fills = Paints::new([Paint::from(CGColor(0, 0, 0, 0))]); ellipse.strokes = Paints::new([Paint::from(CGColor(0, 0, 0, 255))]); ellipse.stroke_width = 4.0; - all_shape_ids.push(ellipse.id.clone()); - repository.insert(Node::Ellipse(ellipse)); + graph.append_child( + Node::Ellipse(ellipse), + Parent::NodeId(root_container_id.clone()), + ); // Regular Polygon (Hexagon) let mut polygon = nf.create_regular_polygon_node(); @@ -119,8 +127,10 @@ async fn demo_strokes() -> Scene { polygon.fills = Paints::new([Paint::from(CGColor(0, 0, 0, 0))]); polygon.strokes = Paints::new([Paint::from(CGColor(0, 0, 0, 255))]); polygon.stroke_width = 4.0; - all_shape_ids.push(polygon.id.clone()); - repository.insert(Node::RegularPolygon(polygon)); + graph.append_child( + Node::RegularPolygon(polygon), + Parent::NodeId(root_container_id.clone()), + ); // Star let mut star = nf.create_regular_star_polygon_node(); @@ -135,8 +145,10 @@ async fn demo_strokes() -> Scene { star.fills = Paints::new([Paint::from(CGColor(0, 0, 0, 0))]); star.strokes = Paints::new([Paint::from(CGColor(0, 0, 0, 255))]); star.stroke_width = 4.0; - all_shape_ids.push(star.id.clone()); - repository.insert(Node::RegularStarPolygon(star)); + graph.append_child( + Node::RegularStarPolygon(star), + Parent::NodeId(root_container_id.clone()), + ); } // Stroke with Effects Row @@ -175,8 +187,10 @@ async fn demo_strokes() -> Scene { _ => unreachable!(), }; - all_shape_ids.push(rect.id.clone()); - repository.insert(Node::Rectangle(rect)); + graph.append_child( + Node::Rectangle(rect), + Parent::NodeId(root_container_id.clone()), + ); } // Stroke Dash Array Demo Row @@ -206,8 +220,10 @@ async fn demo_strokes() -> Scene { _ => unreachable!(), }; - all_shape_ids.push(rect.id.clone()); - repository.insert(Node::Rectangle(rect)); + graph.append_child( + Node::Rectangle(rect), + Parent::NodeId(root_container_id.clone()), + ); } // Stroke Paint Types Demo Row @@ -239,8 +255,10 @@ async fn demo_strokes() -> Scene { active: true, })]); rect.stroke_width = 8.0; - all_shape_ids.push(rect.id.clone()); - repository.insert(Node::Rectangle(rect)); + graph.append_child( + Node::Rectangle(rect), + Parent::NodeId(root_container_id.clone()), + ); // Radial Gradient Stroke let mut rect = nf.create_rectangle_node(); @@ -269,8 +287,10 @@ async fn demo_strokes() -> Scene { active: true, })]); rect.stroke_width = 8.0; - all_shape_ids.push(rect.id.clone()); - repository.insert(Node::Rectangle(rect)); + graph.append_child( + Node::Rectangle(rect), + Parent::NodeId(root_container_id.clone()), + ); // Conic Gradient Stroke let mut rect = nf.create_rectangle_node(); @@ -303,8 +323,10 @@ async fn demo_strokes() -> Scene { active: true, })]); rect.stroke_width = 8.0; - all_shape_ids.push(rect.id.clone()); - repository.insert(Node::Rectangle(rect)); + graph.append_child( + Node::Rectangle(rect), + Parent::NodeId(root_container_id.clone()), + ); // Multi-color Solid Stroke let mut rect = nf.create_rectangle_node(); @@ -319,8 +341,10 @@ async fn demo_strokes() -> Scene { rect.strokes = Paints::new([Paint::from(CGColor(255, 128, 0, 255))]); rect.stroke_width = 8.0; rect.stroke_dash_array = Some(vec![20.0, 10.0, 5.0, 10.0]); // Complex dash pattern - all_shape_ids.push(rect.id.clone()); - repository.insert(Node::Rectangle(rect)); + graph.append_child( + Node::Rectangle(rect), + Parent::NodeId(root_container_id.clone()), + ); } // Multiple Strokes Demo Row @@ -341,8 +365,10 @@ async fn demo_strokes() -> Scene { Paint::from(CGColor(0, 0, 255, 255)), ]); rect.stroke_width = 12.0; // Thick stroke to show layering - all_shape_ids.push(rect.id.clone()); - repository.insert(Node::Rectangle(rect)); + graph.append_child( + Node::Rectangle(rect), + Parent::NodeId(root_container_id.clone()), + ); // Rectangle with solid + gradient strokes let mut rect = nf.create_rectangle_node(); @@ -374,8 +400,10 @@ async fn demo_strokes() -> Scene { }), ]); rect.stroke_width = 10.0; - all_shape_ids.push(rect.id.clone()); - repository.insert(Node::Rectangle(rect)); + graph.append_child( + Node::Rectangle(rect), + Parent::NodeId(root_container_id.clone()), + ); // Ellipse with multiple gradient strokes let mut ellipse = nf.create_ellipse_node(); @@ -421,8 +449,10 @@ async fn demo_strokes() -> Scene { }), ]); ellipse.stroke_width = 12.0; - all_shape_ids.push(ellipse.id.clone()); - repository.insert(Node::Ellipse(ellipse)); + graph.append_child( + Node::Ellipse(ellipse), + Parent::NodeId(root_container_id.clone()), + ); // Polygon with complex multi-stroke pattern let mut polygon = nf.create_regular_polygon_node(); @@ -473,20 +503,15 @@ async fn demo_strokes() -> Scene { ]); polygon.stroke_width = 15.0; // Very thick to show all layers polygon.stroke_dash_array = Some(vec![8.0, 4.0]); // Dashed pattern - all_shape_ids.push(polygon.id.clone()); - repository.insert(Node::RegularPolygon(polygon)); + graph.append_child( + Node::RegularPolygon(polygon), + Parent::NodeId(root_container_id.clone()), + ); } - // Set up the root container - root_container_node.children.extend(all_shape_ids); - let root_container_id = root_container_node.id.clone(); - repository.insert(Node::Container(root_container_node)); - Scene { - id: "scene".to_string(), name: "Strokes Demo".to_string(), - children: vec![root_container_id], - nodes: repository, + graph, background_color: Some(CGColor(250, 250, 250, 255)), } } diff --git a/crates/grida-canvas/examples/grida_texts.rs b/crates/grida-canvas/examples/grida_texts.rs index 2610d5a871..5cf1b0a403 100644 --- a/crates/grida-canvas/examples/grida_texts.rs +++ b/crates/grida-canvas/examples/grida_texts.rs @@ -1,6 +1,6 @@ use cg::cg::types::*; use cg::node::factory::NodeFactory; -use cg::node::repository::NodeRepository; +use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::*; use cg::resources::{load_font, FontMessage}; use cg::window; @@ -112,39 +112,27 @@ async fn demo_texts() -> Scene { }; // Create a node repository and add all nodes - let mut repository = NodeRepository::new(); - - // Collect all the IDs - let word_text_id = word_text_node.id.clone(); - let sentence_text_id = sentence_text_node.id.clone(); - let paragraph_text_id = paragraph_text_node.id.clone(); - let second_paragraph_text_id = second_paragraph_text_node.id.clone(); - let blurry_text_id = blurry_text_node.id.clone(); - - // Add all nodes to the repository - repository.insert(Node::TextSpan(word_text_node)); - repository.insert(Node::TextSpan(sentence_text_node)); - repository.insert(Node::TextSpan(paragraph_text_node)); - repository.insert(Node::TextSpan(second_paragraph_text_node)); - repository.insert(Node::TextSpan(blurry_text_node)); - - // Set up the root container with all IDs - root_container_node.children = vec![ - word_text_id, - sentence_text_id, - paragraph_text_id, - second_paragraph_text_id, - blurry_text_id, - ]; - let root_container_id = root_container_node.id.clone(); - repository.insert(Node::Container(root_container_node)); + let mut graph = SceneGraph::new(); + + // Add root container first + let root_container_id = graph.append_child(Node::Container(root_container_node), Parent::Root); + + // Add all text nodes to the root container + graph.append_children( + vec![ + Node::TextSpan(word_text_node), + Node::TextSpan(sentence_text_node), + Node::TextSpan(paragraph_text_node), + Node::TextSpan(second_paragraph_text_node), + Node::TextSpan(blurry_text_node), + ], + Parent::NodeId(root_container_id), + ); Scene { - id: "scene".to_string(), name: "Text Demo".to_string(), - children: vec![root_container_id], - nodes: repository, background_color: Some(CGColor(250, 250, 250, 255)), + graph, } } diff --git a/crates/grida-canvas/examples/grida_vector.rs b/crates/grida-canvas/examples/grida_vector.rs index 778e04045e..bca91ddbdd 100644 --- a/crates/grida-canvas/examples/grida_vector.rs +++ b/crates/grida-canvas/examples/grida_vector.rs @@ -1,6 +1,6 @@ use cg::cg::types::*; use cg::node::factory::NodeFactory; -use cg::node::repository::NodeRepository; +use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::*; use cg::vectornetwork::*; use cg::window; @@ -8,7 +8,7 @@ use math2::transform::AffineTransform; async fn demo_vectors() -> Scene { let nf = NodeFactory::new(); - let mut repository = NodeRepository::new(); + let mut graph = SceneGraph::new(); // Root container let mut root = nf.create_container_node(); @@ -18,7 +18,7 @@ async fn demo_vectors() -> Scene { height: 800.0, }; - let mut ids = Vec::new(); + let root_id = graph.append_child(Node::Container(root), Parent::Root); let spacing = 200.0; let start_x = 100.0; let base_y = 100.0; @@ -51,8 +51,10 @@ async fn demo_vectors() -> Scene { stroke_dash_array: None, }; - ids.push(vector_node_1_tri_open.id.clone()); - repository.insert(Node::Vector(vector_node_1_tri_open)); + graph.append_child( + Node::Vector(vector_node_1_tri_open), + Parent::NodeId(root_id.clone()), + ); } { @@ -83,8 +85,10 @@ async fn demo_vectors() -> Scene { stroke_dash_array: None, }; - ids.push(vector_node_2_tri_closed.id.clone()); - repository.insert(Node::Vector(vector_node_2_tri_closed)); + graph.append_child( + Node::Vector(vector_node_2_tri_closed), + Parent::NodeId(root_id.clone()), + ); } // @@ -116,8 +120,7 @@ async fn demo_vectors() -> Scene { stroke_dash_array: None, }; - ids.push(vector_node_3.id.clone()); - repository.insert(Node::Vector(vector_node_3)); + graph.append_child(Node::Vector(vector_node_3), Parent::NodeId(root_id.clone())); } { @@ -148,8 +151,7 @@ async fn demo_vectors() -> Scene { stroke_dash_array: None, }; - ids.push(vector_node_4.id.clone()); - repository.insert(Node::Vector(vector_node_4)); + graph.append_child(Node::Vector(vector_node_4), Parent::NodeId(root_id.clone())); } // FIXME: not working @@ -183,8 +185,10 @@ async fn demo_vectors() -> Scene { stroke_dash_array: None, }; - ids.push(vector_node_1_5.id.clone()); - repository.insert(Node::Vector(vector_node_1_5)); + graph.append_child( + Node::Vector(vector_node_1_5), + Parent::NodeId(root_id.clone()), + ); } } @@ -224,8 +228,7 @@ async fn demo_vectors() -> Scene { stroke_dash_array: None, }; - ids.push(vector_node_5.id.clone()); - repository.insert(Node::Vector(vector_node_5)); + graph.append_child(Node::Vector(vector_node_5), Parent::NodeId(root_id.clone())); } // Single-segment 90-degree straight line @@ -257,8 +260,10 @@ async fn demo_vectors() -> Scene { stroke_dash_array: None, }; - ids.push(vector_node_5_5.id.clone()); - repository.insert(Node::Vector(vector_node_5_5)); + graph.append_child( + Node::Vector(vector_node_5_5), + Parent::NodeId(root_id.clone()), + ); } } @@ -297,8 +302,7 @@ async fn demo_vectors() -> Scene { stroke_dash_array: None, }; - ids.push(vector_node_6.id.clone()); - repository.insert(Node::Vector(vector_node_6)); + graph.append_child(Node::Vector(vector_node_6), Parent::NodeId(root_id.clone())); } // Filled rectangle @@ -335,22 +339,14 @@ async fn demo_vectors() -> Scene { stroke_dash_array: None, }; - ids.push(vector_node_7.id.clone()); - repository.insert(Node::Vector(vector_node_7)); + graph.append_child(Node::Vector(vector_node_7), Parent::NodeId(root_id.clone())); } } - // Add all nodes to root - root.children = ids.clone(); - let root_id = root.id.clone(); - repository.insert(Node::Container(root)); - Scene { - id: "scene".to_string(), name: "Vector Network Demo".to_string(), - children: vec![root_id], - nodes: repository, background_color: Some(CGColor(240, 240, 240, 255)), + graph, } } diff --git a/crates/grida-canvas/examples/grida_webfonts.rs b/crates/grida-canvas/examples/grida_webfonts.rs index 99db7fca76..4757946632 100644 --- a/crates/grida-canvas/examples/grida_webfonts.rs +++ b/crates/grida-canvas/examples/grida_webfonts.rs @@ -1,7 +1,7 @@ use cg::cg::types::*; use cg::helpers::webfont_helper::{find_font_files_by_family, load_webfonts_metadata}; use cg::node::factory::NodeFactory; -use cg::node::repository::NodeRepository; +use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::*; use cg::resources::{load_font, FontMessage}; use cg::window; @@ -60,7 +60,7 @@ async fn demo_webfonts() -> Scene { letter_spacing: Default::default(), word_spacing: Default::default(), font_style_italic: false, - line_height: TextLineHeight::Fixed(1.5), + line_height: TextLineHeight::Factor(1.5), text_transform: TextTransform::None, }; description_node.text_align = TextAlign::Left; @@ -124,33 +124,31 @@ async fn demo_webfonts() -> Scene { }; // Create a node repository and add all nodes - let mut repository = NodeRepository::new(); - - // Collect all the IDs - let heading_id = heading_node.id.clone(); - let description_id = description_node.id.clone(); - let albert_text_ids: Vec<_> = albert_text_nodes.iter().map(|n| n.id.clone()).collect(); - - // Add all nodes to the repository - repository.insert(Node::TextSpan(heading_node)); - repository.insert(Node::TextSpan(description_node)); + let mut graph = SceneGraph::new(); + + // Add root container first + let root_container_id = graph.append_child(Node::Container(root_container_node), Parent::Root); + + // Add all text nodes to root container + graph.append_child( + Node::TextSpan(heading_node), + Parent::NodeId(root_container_id.clone()), + ); + graph.append_child( + Node::TextSpan(description_node), + Parent::NodeId(root_container_id.clone()), + ); for text_node in albert_text_nodes { - repository.insert(Node::TextSpan(text_node)); + graph.append_child( + Node::TextSpan(text_node), + Parent::NodeId(root_container_id.clone()), + ); } - // Set up the root container with all IDs - let mut children = vec![heading_id, description_id]; - children.extend(albert_text_ids); - root_container_node.children = children; - let root_container_id = root_container_node.id.clone(); - repository.insert(Node::Container(root_container_node)); - Scene { - id: "scene".to_string(), name: "Webfonts Demo".to_string(), - children: vec![root_container_id], - nodes: repository, background_color: Some(CGColor(250, 250, 250, 255)), + graph, } } diff --git a/crates/grida-canvas/examples/sys_camera.rs b/crates/grida-canvas/examples/sys_camera.rs index 55465990f8..231ea707a4 100644 --- a/crates/grida-canvas/examples/sys_camera.rs +++ b/crates/grida-canvas/examples/sys_camera.rs @@ -1,6 +1,7 @@ use cg::cg::types::*; use cg::node::factory::NodeFactory; use cg::{ + node::scene_graph::{Parent, SceneGraph}, node::schema::*, runtime::camera::Camera2D, runtime::scene::{Backend, Renderer, RendererOptions}, @@ -25,46 +26,40 @@ use winit::{ }; fn create_static_scene() -> Scene { - let mut repository = cg::node::repository::NodeRepository::new(); + let mut graph = SceneGraph::new(); let nf = NodeFactory::new(); + // Create a root group containing all rectangles + let root_group = GroupNodeRec { + id: "root".to_string(), + name: Some("Root Group".to_string()), + active: true, + transform: None, + opacity: 1.0, + blend_mode: LayerBlendMode::default(), + mask: None, + }; + + let root_id = graph.append_child(Node::Group(root_group), Parent::Root); + // Create a grid of rectangles - let mut ids = Vec::new(); for i in 0..10 { for j in 0..10 { let mut rect = nf.create_rectangle_node(); - let id = rect.id.clone(); rect.name = Some(format!("Rectangle {}-{}", i, j)); rect.transform = AffineTransform::new(i as f32 * 100.0, j as f32 * 100.0, 0.0); rect.size = Size { width: 50.0, height: 50.0, }; - repository.insert(Node::Rectangle(rect)); - ids.push(id); + graph.append_child(Node::Rectangle(rect), Parent::NodeId(root_id.clone())); } } - // Create a root group containing all rectangles - let root_group = GroupNodeRec { - id: "root".to_string(), - name: Some("Root Group".to_string()), - active: true, - transform: None, - children: ids, - opacity: 1.0, - blend_mode: LayerBlendMode::default(), - mask: None, - }; - - repository.insert(Node::Group(root_group)); - Scene { - id: "scene".to_string(), name: "Test Scene".to_string(), - children: vec!["root".to_string()], - nodes: repository, background_color: Some(CGColor(255, 255, 255, 255)), + graph, } } diff --git a/crates/grida-canvas/examples/wd_animation.rs b/crates/grida-canvas/examples/wd_animation.rs index 3c6e1ac077..2023e4f81d 100644 --- a/crates/grida-canvas/examples/wd_animation.rs +++ b/crates/grida-canvas/examples/wd_animation.rs @@ -1,6 +1,6 @@ use cg::cg::types::*; use cg::node::factory::NodeFactory; -use cg::node::repository::NodeRepository; +use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::*; use cg::window::{self, application::HostEvent}; use math2::transform::AffineTransform; @@ -8,7 +8,7 @@ use std::time::Instant; fn create_scene(t: f32) -> Scene { let nf = NodeFactory::new(); - let mut repo = NodeRepository::new(); + let mut graph = SceneGraph::new(); let mut rect = nf.create_rectangle_node(); rect.size = Size { @@ -21,15 +21,12 @@ fn create_scene(t: f32) -> Scene { let r = ((t.sin() * 0.5 + 0.5) * 255.0) as u8; let g = ((t.cos() * 0.5 + 0.5) * 255.0) as u8; rect.set_fill(Paint::from(CGColor(r, g, 200, 255))); - let rect_id = rect.id.clone(); - repo.insert(Node::Rectangle(rect)); + graph.append_child(Node::Rectangle(rect), Parent::Root); Scene { - id: "scene".to_string(), name: "Animated".to_string(), - children: vec![rect_id], - nodes: repo, background_color: Some(CGColor(255, 255, 255, 255)), + graph, } } diff --git a/crates/grida-canvas/goldens/pdf.pdf b/crates/grida-canvas/goldens/pdf.pdf index 41f7a49cf7..520f08797d 100644 Binary files a/crates/grida-canvas/goldens/pdf.pdf and b/crates/grida-canvas/goldens/pdf.pdf differ diff --git a/crates/grida-canvas/goldens/svg.svg b/crates/grida-canvas/goldens/svg.svg index 86b5aea56b..cf8883a168 100644 --- a/crates/grida-canvas/goldens/svg.svg +++ b/crates/grida-canvas/goldens/svg.svg @@ -2,79 +2,99 @@ + + - + + + Grida Canvas SVG Demo + - + - + + + + Rich content demonstration with shapes, gradients, and eects + + + + + + - + + - + - + - + + - + - + + + - + - - - + + + + + - + - + + + - - - - - - - - + + + + - - + + + This PDF demonstrates various rendering capabilities including gradients, shapes, text, and eects. + diff --git a/crates/grida-canvas/goldens/type_line_height.png b/crates/grida-canvas/goldens/type_line_height.png index d26f06f9e7..41f6cf4b1a 100644 Binary files a/crates/grida-canvas/goldens/type_line_height.png and b/crates/grida-canvas/goldens/type_line_height.png differ diff --git a/crates/grida-canvas/goldens/type_stroke.png b/crates/grida-canvas/goldens/type_stroke.png index 11636262c4..cb8010146d 100644 Binary files a/crates/grida-canvas/goldens/type_stroke.png and b/crates/grida-canvas/goldens/type_stroke.png differ diff --git a/crates/grida-canvas/src/cache/geometry.rs b/crates/grida-canvas/src/cache/geometry.rs index a3318a5f90..f635e5d63c 100644 --- a/crates/grida-canvas/src/cache/geometry.rs +++ b/crates/grida-canvas/src/cache/geometry.rs @@ -1,6 +1,6 @@ use crate::cache::paragraph::ParagraphCache; use crate::cg::types::*; -use crate::node::repository::NodeRepository; +use crate::node::scene_graph::SceneGraph; use crate::node::schema::{ IntrinsicSizeNode, LayerEffects, Node, NodeGeometryMixin, NodeId, Scene, }; @@ -58,10 +58,10 @@ impl GeometryCache { ) -> Self { let mut cache = Self::new(); let root_world = AffineTransform::identity(); - for child in &scene.children { + for child in scene.graph.roots() { Self::build_recursive( - child, - &scene.nodes, + &child, + &scene.graph, &root_world, None, &mut cache, @@ -74,15 +74,15 @@ impl GeometryCache { fn build_recursive( id: &NodeId, - repo: &NodeRepository, + graph: &SceneGraph, parent_world: &AffineTransform, parent_id: Option, cache: &mut GeometryCache, paragraph_cache: &mut ParagraphCache, fonts: &FontRepository, ) -> Rectangle { - let node = repo - .get(id) + let node = graph + .get_node(id) .expect(&format!("node not found in geometry cache {id:?}")); match node { @@ -90,25 +90,27 @@ impl GeometryCache { let world_transform = parent_world.compose(&n.transform.unwrap_or_default()); let mut union_bounds: Option = None; let mut union_render_bounds: Option = None; - for child_id in &n.children { - let child_bounds = Self::build_recursive( - child_id, - repo, - &world_transform, - Some(id.clone()), - cache, - paragraph_cache, - fonts, - ); - union_bounds = match union_bounds { - Some(b) => Some(rect::union(&[b, child_bounds])), - None => Some(child_bounds), - }; - if let Some(rb) = cache.get_render_bounds(child_id) { - union_render_bounds = match union_render_bounds { - Some(b) => Some(rect::union(&[b, rb])), - None => Some(rb), + if let Some(children) = graph.get_children(id) { + for child_id in children { + let child_bounds = Self::build_recursive( + child_id, + graph, + &world_transform, + Some(id.clone()), + cache, + paragraph_cache, + fonts, + ); + union_bounds = match union_bounds { + Some(b) => Some(rect::union(&[b, child_bounds])), + None => Some(child_bounds), }; + if let Some(rb) = cache.get_render_bounds(child_id) { + union_render_bounds = match union_render_bounds { + Some(b) => Some(rect::union(&[b, rb])), + None => Some(rb), + }; + } } } @@ -149,20 +151,22 @@ impl GeometryCache { Node::BooleanOperation(n) => { let world_transform = parent_world.compose(&n.transform.unwrap_or_default()); let mut union_bounds: Option = None; - for child_id in &n.children { - let child_bounds = Self::build_recursive( - child_id, - repo, - &world_transform, - Some(id.clone()), - cache, - paragraph_cache, - fonts, - ); - union_bounds = match union_bounds { - Some(b) => Some(rect::union(&[b, child_bounds])), - None => Some(child_bounds), - }; + if let Some(children) = graph.get_children(id) { + for child_id in children { + let child_bounds = Self::build_recursive( + child_id, + graph, + &world_transform, + Some(id.clone()), + cache, + paragraph_cache, + fonts, + ); + union_bounds = match union_bounds { + Some(b) => Some(rect::union(&[b, child_bounds])), + None => Some(child_bounds), + }; + } } let world_bounds = union_bounds.unwrap_or_else(|| Rectangle { @@ -225,17 +229,19 @@ impl GeometryCache { &n.effects, ); - for child_id in &n.children { - let child_bounds = Self::build_recursive( - child_id, - repo, - &world_transform, - Some(id.clone()), - cache, - paragraph_cache, - fonts, - ); - union_world_bounds = rect::union(&[union_world_bounds, child_bounds]); + if let Some(children) = graph.get_children(id) { + for child_id in children { + let child_bounds = Self::build_recursive( + child_id, + graph, + &world_transform, + Some(id.clone()), + cache, + paragraph_cache, + fonts, + ); + union_world_bounds = rect::union(&[union_world_bounds, child_bounds]); + } } let entry = GeometryEntry { diff --git a/crates/grida-canvas/src/dummy/mod.rs b/crates/grida-canvas/src/dummy/mod.rs index 33ca87ab97..9635c75112 100644 --- a/crates/grida-canvas/src/dummy/mod.rs +++ b/crates/grida-canvas/src/dummy/mod.rs @@ -1,10 +1,13 @@ use crate::cg::types::*; -use crate::node::{factory::NodeFactory, repository::NodeRepository, schema::*}; +use crate::node::{ + factory::NodeFactory, + scene_graph::{Parent, SceneGraph}, + schema::*, +}; /// Load a simple demo scene with a few colored rectangles. pub(crate) fn create_dummy_scene() -> Scene { let nf = NodeFactory::new(); - let mut nodes = NodeRepository::new(); let mut rect1 = nf.create_rectangle_node(); rect1.name = Some("Red Rectangle".to_string()); @@ -14,8 +17,6 @@ pub(crate) fn create_dummy_scene() -> Scene { height: 100.0, }; rect1.set_fill(Paint::Solid(SolidPaint::RED)); - let rect1_id = rect1.id.clone(); - nodes.insert(Node::Rectangle(rect1)); let mut rect2 = nf.create_rectangle_node(); rect2.name = Some("Blue Rectangle".to_string()); @@ -25,8 +26,6 @@ pub(crate) fn create_dummy_scene() -> Scene { height: 80.0, }; rect2.set_fill(Paint::Solid(SolidPaint::BLUE)); - let rect2_id = rect2.id.clone(); - nodes.insert(Node::Rectangle(rect2)); let mut rect3 = nf.create_rectangle_node(); rect3.name = Some("Green Rectangle".to_string()); @@ -36,23 +35,28 @@ pub(crate) fn create_dummy_scene() -> Scene { height: 120.0, }; rect3.set_fill(Paint::Solid(SolidPaint::GREEN)); - let rect3_id = rect3.id.clone(); - nodes.insert(Node::Rectangle(rect3)); + + let mut graph = SceneGraph::new(); + graph.append_children( + vec![ + Node::Rectangle(rect1), + Node::Rectangle(rect2), + Node::Rectangle(rect3), + ], + Parent::Root, + ); Scene { - id: "dummy".to_string(), name: "Dummy Scene".to_string(), - children: vec![rect1_id, rect2_id, rect3_id], - nodes, - background_color: Some(CGColor(240, 240, 240, 255)), + graph, + background_color: Some(CGColor::WHITE), } } /// Load a heavy scene useful for performance benchmarking. pub(crate) fn create_benchmark_scene(cols: u32, rows: u32) -> Scene { let nf = NodeFactory::new(); - let mut nodes = NodeRepository::new(); - let mut children = Vec::new(); + let mut graph = SceneGraph::new(); let size = 20.0f32; let spacing = 5.0f32; @@ -74,17 +78,13 @@ pub(crate) fn create_benchmark_scene(cols: u32, rows: u32) -> Scene { blend_mode: BlendMode::default(), active: true, })); - let id = rect.id.clone(); - nodes.insert(Node::Rectangle(rect)); - children.push(id); + graph.append_child(Node::Rectangle(rect), Parent::Root); } } Scene { - id: "benchmark".to_string(), name: "Benchmark Scene".to_string(), - children, - nodes, - background_color: Some(CGColor(255, 255, 255, 255)), + graph, + background_color: Some(CGColor::WHITE), } } diff --git a/crates/grida-canvas/src/io/io_figma.rs b/crates/grida-canvas/src/io/io_figma.rs index 8add1f4fc2..411f8692e2 100644 --- a/crates/grida-canvas/src/io/io_figma.rs +++ b/crates/grida-canvas/src/io/io_figma.rs @@ -1,6 +1,7 @@ use crate::cg::{types::*, Alignment}; use crate::helpers::webfont_helper; use crate::node::repository::NodeRepository; +use crate::node::scene_graph::SceneGraph; use crate::node::schema::*; use figma_api::models::minimal_strokes_trait::StrokeAlign as FigmaStrokeAlign; use figma_api::models::type_style::{ @@ -20,8 +21,6 @@ use math2::transform::AffineTransform; const TRANSPARENT: Paint = Paint::Solid(SolidPaint::TRANSPARENT); -const BLACK: Paint = Paint::Solid(SolidPaint::BLACK); - // Map implementations impl From<&Rgba> for CGColor { fn from(color: &Rgba) -> Self { @@ -84,7 +83,7 @@ impl From<&FigmaPaint> for Paint { } }; - let repeat = match image.scale_mode { + let _repeat = match image.scale_mode { figma_api::models::image_paint::ScaleMode::Tile => ImageRepeat::Repeat, _ => ImageRepeat::default(), }; @@ -250,6 +249,7 @@ fn convert_gradient_transform(handles: &Vec) -> AffineTransform { /// Converts Figma nodes to Grida schema pub struct FigmaConverter { repository: NodeRepository, + links: std::collections::HashMap>, image_urls: std::collections::HashMap, font_store: webfont_helper::FontUsageStore, } @@ -258,6 +258,7 @@ impl FigmaConverter { pub fn new() -> Self { Self { repository: NodeRepository::new(), + links: std::collections::HashMap::new(), image_urls: std::collections::HashMap::new(), font_store: webfont_helper::FontUsageStore::new(), } @@ -348,7 +349,7 @@ impl FigmaConverter { } }; - let repeat = match image.scale_mode { + let _repeat = match image.scale_mode { figma_api::models::image_paint::ScaleMode::Tile => ImageRepeat::Repeat, _ => ImageRepeat::default(), }; @@ -604,8 +605,15 @@ impl FigmaConverter { let size = Self::convert_size(component.size.as_ref()); let transform = Self::convert_transform(component.relative_transform.as_ref()); + let node_id = component.id.clone(); + + // Store children relationship + if !children.is_empty() { + self.links.insert(node_id.clone(), children); + } + Ok(Node::Container(ContainerNodeRec { - id: component.id.clone(), + id: node_id, name: Some(component.name.clone()), active: component.visible.unwrap_or(true), opacity: Self::convert_opacity(component.visible), @@ -632,7 +640,6 @@ impl FigmaConverter { .clone() .map(|v| v.into_iter().map(|x| x as f32).collect()), effects: Self::convert_effects(&component.effects), - children, clip: component.clips_content, })) } @@ -693,8 +700,15 @@ impl FigmaConverter { let size = Self::convert_size(instance.size.as_ref()); let transform = Self::convert_transform(instance.relative_transform.as_ref()); + let node_id = instance.id.clone(); + + // Store children relationship + if !children.is_empty() { + self.links.insert(node_id.clone(), children); + } + Ok(Node::Container(ContainerNodeRec { - id: instance.id.clone(), + id: node_id, name: Some(instance.name.clone()), active: instance.visible.unwrap_or(true), opacity: Self::convert_opacity(instance.visible), @@ -721,7 +735,6 @@ impl FigmaConverter { .clone() .map(|v| v.into_iter().map(|x| x as f32).collect()), effects: Self::convert_effects(&instance.effects), - children, clip: instance.clips_content, })) } @@ -734,8 +747,15 @@ impl FigmaConverter { .map(|child| self.convert_sub_canvas_node(child)) .collect::, _>>()?; + let node_id = section.id.clone(); + + // Store children relationship + if !children.is_empty() { + self.links.insert(node_id.clone(), children); + } + Ok(Node::Container(ContainerNodeRec { - id: section.id.clone(), + id: node_id, name: Some(format!("[Section] {}", section.name)), active: section.visible.unwrap_or(true), opacity: Self::convert_opacity(section.visible), @@ -744,7 +764,6 @@ impl FigmaConverter { transform: Self::convert_transform(section.relative_transform.as_ref()), size: Self::convert_size(section.size.as_ref()), corner_radius: RectangularCornerRadius::zero(), - children, fills: self.convert_fills(Some(§ion.fills.as_ref())), strokes: Paints::default(), stroke_width: 0.0, @@ -827,12 +846,14 @@ impl FigmaConverter { .iter() .map(|child| self.convert_sub_canvas_node(child)) .collect::, _>>()?; - // canvas.background_color + + // Build scene graph from snapshot + let nodes = self.repository.iter().map(|(_, node)| node.clone()); + let graph = SceneGraph::new_from_snapshot(nodes, self.links.clone(), children); + Ok(Scene { - id: canvas.id.clone(), name: canvas.name.clone(), - children, - nodes: self.repository.clone(), + graph, background_color: Some(CGColor::from(&canvas.background_color)), }) } @@ -847,8 +868,15 @@ impl FigmaConverter { let size = Self::convert_size(origin.size.as_ref()); let transform = Self::convert_transform(origin.relative_transform.as_ref()); + let node_id = origin.id.clone(); + + // Store children relationship + if !children.is_empty() { + self.links.insert(node_id.clone(), children); + } + Ok(Node::Container(ContainerNodeRec { - id: origin.id.clone(), + id: node_id, name: Some(origin.name.clone()), active: origin.visible.unwrap_or(true), opacity: Self::convert_opacity(origin.visible), @@ -875,7 +903,6 @@ impl FigmaConverter { .clone() .map(|v| v.into_iter().map(|x| x as f32).collect()), effects: Self::convert_effects(&origin.effects), - children, clip: origin.clips_content, })) } @@ -1080,7 +1107,6 @@ impl FigmaConverter { stroke_align: StrokeAlign::Inside, stroke_dash_array: None, effects: LayerEffects::default(), - children, clip: false, })) } @@ -1112,8 +1138,15 @@ impl FigmaConverter { } }; + let node_id = origin.id.clone(); + + // Store children relationship + if !children.is_empty() { + self.links.insert(node_id.clone(), children); + } + Ok(Node::BooleanOperation(BooleanPathOperationNodeRec { - id: origin.id.clone(), + id: node_id, name: Some(origin.name.clone()), active: origin.visible.unwrap_or(true), opacity: Self::convert_opacity(origin.visible), @@ -1122,7 +1155,6 @@ impl FigmaConverter { effects: Self::convert_effects(&origin.effects), transform: Some(transform), op: op, - children, // map this corner_radius: None, fills: self.convert_fills(Some(&origin.fills)), @@ -1332,13 +1364,19 @@ impl FigmaConverter { .map(|child| self.convert_sub_canvas_node(child)) .collect::, _>>()?; + let node_id = origin.id.clone(); + + // Store children relationship + if !children.is_empty() { + self.links.insert(node_id.clone(), children); + } + Ok(Node::Group(GroupNodeRec { - id: origin.id.clone(), + id: node_id, name: Some(origin.name.clone()), active: origin.visible.unwrap_or(true), // the figma's relativeTransform for group is a no-op on our model. transform: None, - children, opacity: Self::convert_opacity(origin.visible), blend_mode: Self::convert_blend_mode(origin.blend_mode), mask: None, diff --git a/crates/grida-canvas/src/io/io_grida.rs b/crates/grida-canvas/src/io/io_grida.rs index 09935d483a..09f6a20d7a 100644 --- a/crates/grida-canvas/src/io/io_grida.rs +++ b/crates/grida-canvas/src/io/io_grida.rs @@ -954,7 +954,6 @@ impl From for GroupNodeRec { // TODO: group's transform should be handled differently transform: Some(transform), // Children populated from links after conversion - children: vec![], opacity: node.base.opacity, blend_mode: node.base.blend_mode.into(), mask: node.base.mask.map(|m| m.into()), @@ -999,7 +998,6 @@ impl From for ContainerNodeRec { node.base.fe_backdrop_blur, ), // Children populated from links after conversion - children: vec![], clip: true, mask: node.base.mask.map(|m| m.into()), } @@ -1499,7 +1497,6 @@ impl From for Node { .corner_radius .and_then(JSONCornerRadius::into_uniform), // Children populated from links after conversion - children: vec![], fills: merge_paints(node.base.fill, node.base.fills), strokes: merge_paints(node.base.stroke, node.base.strokes), stroke_width: node.base.stroke_width, diff --git a/crates/grida-canvas/src/node/factory.rs b/crates/grida-canvas/src/node/factory.rs index d58e635fdc..f7a16a7722 100644 --- a/crates/grida-canvas/src/node/factory.rs +++ b/crates/grida-canvas/src/node/factory.rs @@ -153,7 +153,6 @@ impl NodeFactory { name: None, active: true, transform: None, - children: Vec::new(), opacity: Self::DEFAULT_OPACITY, blend_mode: LayerBlendMode::default(), mask: None, @@ -172,7 +171,6 @@ impl NodeFactory { transform: AffineTransform::identity(), size: Self::DEFAULT_SIZE, corner_radius: RectangularCornerRadius::zero(), - children: Vec::new(), fills: Paints::new([Self::default_solid_paint(Self::DEFAULT_COLOR)]), strokes: Paints::default(), stroke_width: Self::DEFAULT_STROKE_WIDTH, diff --git a/crates/grida-canvas/src/node/mod.rs b/crates/grida-canvas/src/node/mod.rs index 7cdc81ad06..f4d945604d 100644 --- a/crates/grida-canvas/src/node/mod.rs +++ b/crates/grida-canvas/src/node/mod.rs @@ -1,3 +1,4 @@ pub mod factory; pub mod repository; +pub mod scene_graph; pub mod schema; diff --git a/crates/grida-canvas/src/node/scene_graph.rs b/crates/grida-canvas/src/node/scene_graph.rs new file mode 100644 index 0000000000..baa8448ff8 --- /dev/null +++ b/crates/grida-canvas/src/node/scene_graph.rs @@ -0,0 +1,673 @@ +use super::repository::NodeRepository; +use super::schema::{Node, NodeId}; +use std::collections::HashMap; + +/// Parent reference in the scene graph +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Parent { + /// Root-level node (direct child of the scene) + Root, + /// Child of another node + NodeId(NodeId), +} + +/// Error type for SceneGraph operations +#[derive(Debug, Clone)] +pub enum SceneGraphError { + NodeNotFound(NodeId), + ParentNotFound(NodeId), + ChildNotFound(NodeId), + IndexOutOfBounds { + parent: NodeId, + index: usize, + len: usize, + }, +} + +impl std::fmt::Display for SceneGraphError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SceneGraphError::NodeNotFound(id) => write!(f, "Node not found: {}", id), + SceneGraphError::ParentNotFound(id) => write!(f, "Parent not found: {}", id), + SceneGraphError::ChildNotFound(id) => write!(f, "Child not found: {}", id), + SceneGraphError::IndexOutOfBounds { parent, index, len } => { + write!( + f, + "Index out of bounds for parent {}: index {} but length is {}", + parent, index, len + ) + } + } + } +} + +impl std::error::Error for SceneGraphError {} + +pub type SceneGraphResult = Result; + +/// A scene graph that manages both the tree structure and node data. +/// +/// The SceneGraph maintains: +/// - Root node IDs (direct children of the scene) +/// - An adjacency list (parent->children) for the tree structure +/// - A node repository for storing actual node data +/// +/// This provides a centralized, efficient way to manage scene hierarchy +/// separate from node attributes. +#[derive(Debug, Clone)] +pub struct SceneGraph { + /// Root node IDs - direct children of the scene + roots: Vec, + /// Parent to children adjacency list + links: HashMap>, + /// Node data repository + nodes: NodeRepository, +} + +impl SceneGraph { + /// Creates a new empty scene graph + pub fn new() -> Self { + Self { + roots: Vec::new(), + links: HashMap::new(), + nodes: NodeRepository::new(), + } + } + + /// Create a SceneGraph from a complete snapshot (typical IO loader use case). + /// + /// This is optimized for deserializing complete scene data where nodes and links + /// are provided as separate collections. + /// + /// # Arguments + /// * `nodes` - Iterator of nodes to add to the repository + /// * `links` - HashMap of parent->children relationships + /// * `roots` - Root node IDs (direct children of the scene) + pub fn new_from_snapshot( + nodes: impl IntoIterator, + links: HashMap>, + roots: Vec, + ) -> Self { + let mut graph = Self::new(); + + // Add all nodes to the repository + for node in nodes { + graph.nodes.insert(node); + } + + // Set up all links + graph.links = links; + + // Set roots + graph.roots = roots; + + graph + } + + // ------------------------------------------------------------------------- + // Graph Structure Methods + // ------------------------------------------------------------------------- + + /// Add a node to the graph and link it to a parent in one operation. + /// + /// Returns the node's ID. + pub fn append_child(&mut self, node: Node, parent: Parent) -> NodeId { + let id = self.nodes.insert(node); + + match parent { + Parent::Root => { + self.roots.push(id.clone()); + } + Parent::NodeId(parent_id) => { + self.links + .entry(parent_id) + .or_insert_with(Vec::new) + .push(id.clone()); + } + } + + id + } + + /// Add multiple nodes to the graph and link them all to a parent in one operation. + /// This is a bulk convenience method for adding multiple children to the same parent. + /// + /// Returns the node IDs in the same order as the input nodes. + pub fn append_children(&mut self, nodes: Vec, parent: Parent) -> Vec { + let mut ids = Vec::new(); + for node in nodes { + let id = self.append_child(node, parent.clone()); + ids.push(id); + } + ids + } + + /// Get children of a node, if any exist + pub fn get_children(&self, id: &NodeId) -> Option<&Vec> { + self.links.get(id) + } + + /// Add a child to a parent's children list + pub fn add_child(&mut self, parent: &NodeId, child: NodeId) -> SceneGraphResult<()> { + let children = self + .links + .get_mut(parent) + .ok_or_else(|| SceneGraphError::ParentNotFound(parent.clone()))?; + children.push(child); + Ok(()) + } + + /// Insert a child at a specific index in the parent's children list + pub fn add_child_at( + &mut self, + parent: &NodeId, + child: NodeId, + index: usize, + ) -> SceneGraphResult<()> { + let children = self + .links + .get_mut(parent) + .ok_or_else(|| SceneGraphError::ParentNotFound(parent.clone()))?; + + if index > children.len() { + return Err(SceneGraphError::IndexOutOfBounds { + parent: parent.clone(), + index, + len: children.len(), + }); + } + + children.insert(index, child); + Ok(()) + } + + /// Remove a child from a parent's children list + pub fn remove_child(&mut self, parent: &NodeId, child: &NodeId) -> SceneGraphResult<()> { + let children = self + .links + .get_mut(parent) + .ok_or_else(|| SceneGraphError::ParentNotFound(parent.clone()))?; + + let pos = children + .iter() + .position(|id| id == child) + .ok_or_else(|| SceneGraphError::ChildNotFound(child.clone()))?; + + children.remove(pos); + Ok(()) + } + + /// Iterate over all parent->children pairs + pub fn iter(&self) -> impl Iterator)> { + self.links.iter() + } + + /// Get the root nodes (direct children of the scene) + pub fn roots(&self) -> &[NodeId] { + &self.roots + } + + // ------------------------------------------------------------------------- + // Node Data Methods + // ------------------------------------------------------------------------- + + /// Get a reference to a node by ID + pub fn get_node(&self, id: &NodeId) -> SceneGraphResult<&Node> { + self.nodes + .get(id) + .ok_or_else(|| SceneGraphError::NodeNotFound(id.clone())) + } + + /// Get a mutable reference to a node by ID + pub fn get_node_mut(&mut self, id: &NodeId) -> SceneGraphResult<&mut Node> { + self.nodes + .get_mut(id) + .ok_or_else(|| SceneGraphError::NodeNotFound(id.clone())) + } + + /// Remove a node from the repository and return it + pub fn remove_node(&mut self, id: &NodeId) -> SceneGraphResult { + self.nodes + .remove(id) + .ok_or_else(|| SceneGraphError::NodeNotFound(id.clone())) + } + + /// Check if a node exists in the repository + pub fn has_node(&self, id: &NodeId) -> bool { + self.nodes.get(id).is_some() + } + + /// Get the number of nodes in the graph + pub fn node_count(&self) -> usize { + self.nodes.len() + } + + /// Check if the graph is empty + pub fn is_empty(&self) -> bool { + self.nodes.is_empty() + } + + // ------------------------------------------------------------------------- + // Tree Traversal Methods + // ------------------------------------------------------------------------- + + /// Walk the tree in pre-order (parent before children) + pub fn walk_preorder( + &self, + root: &NodeId, + visitor: &mut impl FnMut(&NodeId), + ) -> SceneGraphResult<()> { + if !self.has_node(root) { + return Err(SceneGraphError::NodeNotFound(root.clone())); + } + + visitor(root); + + if let Some(children) = self.get_children(root) { + for child in children { + self.walk_preorder(child, visitor)?; + } + } + + Ok(()) + } + + /// Walk the tree in post-order (children before parent) + pub fn walk_postorder( + &self, + root: &NodeId, + visitor: &mut impl FnMut(&NodeId), + ) -> SceneGraphResult<()> { + if !self.has_node(root) { + return Err(SceneGraphError::NodeNotFound(root.clone())); + } + + if let Some(children) = self.get_children(root) { + for child in children { + self.walk_postorder(child, visitor)?; + } + } + + visitor(root); + + Ok(()) + } + + /// Get all ancestors of a node (path to root) + pub fn ancestors(&self, id: &NodeId) -> SceneGraphResult> { + if !self.has_node(id) { + return Err(SceneGraphError::NodeNotFound(id.clone())); + } + + let mut result = Vec::new(); + let mut current = id.clone(); + + // Find parent by searching all links + loop { + let mut found_parent = false; + for (parent_id, children) in &self.links { + if children.contains(¤t) { + result.push(parent_id.clone()); + current = parent_id.clone(); + found_parent = true; + break; + } + } + + if !found_parent { + break; + } + } + + Ok(result) + } + + /// Get all descendants of a node (all children recursively) + pub fn descendants(&self, id: &NodeId) -> SceneGraphResult> { + if !self.has_node(id) { + return Err(SceneGraphError::NodeNotFound(id.clone())); + } + + let mut result = Vec::new(); + + self.walk_preorder(id, &mut |node_id| { + if node_id != id { + result.push(node_id.clone()); + } + })?; + + Ok(result) + } +} + +impl Default for SceneGraph { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::node::schema::{ErrorNodeRec, NodeTrait, Size}; + use math2::transform::AffineTransform; + + fn create_test_node(id: &str) -> Node { + Node::Error(ErrorNodeRec { + id: id.to_string(), + name: Some(format!("node_{}", id)), + active: true, + transform: AffineTransform::identity(), + size: Size { + width: 10.0, + height: 10.0, + }, + error: "test".to_string(), + opacity: 1.0, + }) + } + + #[test] + fn test_scene_graph_basic() { + let mut graph = SceneGraph::new(); + + let node_a = create_test_node("a"); + let node_b = create_test_node("b"); + let node_c = create_test_node("c"); + + let id_a = graph.append_child(node_a, Parent::Root); + let id_b = graph.append_child(node_b, Parent::NodeId(id_a.clone())); + let id_c = graph.append_child(node_c, Parent::NodeId(id_a.clone())); + + assert_eq!(graph.node_count(), 3); + assert_eq!(graph.get_children(&id_a).unwrap().len(), 2); + assert_eq!(graph.get_children(&id_a).unwrap(), &vec![id_b, id_c]); + } + + #[test] + fn test_add_child() { + let mut graph = SceneGraph::new(); + + let node_a = create_test_node("a"); + let node_b = create_test_node("b"); + let node_c = create_test_node("c"); + + // Create parent with one child first + let id_a = graph.append_child(node_a, Parent::Root); + let id_b = graph.append_child(node_b, Parent::NodeId(id_a.clone())); + + // Now add another child dynamically using add_child + let id_c = graph.append_child(node_c, Parent::Root); + graph.add_child(&id_a, id_c.clone()).unwrap(); + + assert_eq!(graph.get_children(&id_a).unwrap().len(), 2); + assert_eq!(graph.get_children(&id_a).unwrap()[0], id_b); + assert_eq!(graph.get_children(&id_a).unwrap()[1], id_c); + } + + #[test] + fn test_add_child_at() { + let mut graph = SceneGraph::new(); + + let id_a = graph.append_child(create_test_node("a"), Parent::Root); + let id_b = graph.append_child(create_test_node("b"), Parent::NodeId(id_a.clone())); + let id_c = graph.append_child(create_test_node("c"), Parent::NodeId(id_a.clone())); + let id_d = graph.append_child(create_test_node("d"), Parent::Root); + + // Insert id_d at index 1 in id_a's children (between id_b and id_c) + graph.add_child_at(&id_a, id_d.clone(), 1).unwrap(); + + let children = graph.get_children(&id_a).unwrap(); + assert_eq!(children.len(), 3); + assert_eq!(children[0], id_b); + assert_eq!(children[1], id_d); + assert_eq!(children[2], id_c); + } + + #[test] + fn test_remove_child() { + let mut graph = SceneGraph::new(); + + let id_a = graph.append_child(create_test_node("a"), Parent::Root); + let id_b = graph.append_child(create_test_node("b"), Parent::NodeId(id_a.clone())); + let id_c = graph.append_child(create_test_node("c"), Parent::NodeId(id_a.clone())); + + graph.remove_child(&id_a, &id_b).unwrap(); + + let children = graph.get_children(&id_a).unwrap(); + assert_eq!(children.len(), 1); + assert_eq!(children[0], id_c); + } + + #[test] + fn test_roots() { + let mut graph = SceneGraph::new(); + + let id_a = graph.append_child(create_test_node("a"), Parent::Root); + let id_b = graph.append_child(create_test_node("b"), Parent::NodeId(id_a.clone())); + let _id_c = graph.append_child(create_test_node("c"), Parent::NodeId(id_b.clone())); + + let roots = graph.roots(); + assert_eq!(roots.len(), 1); + assert!(roots.contains(&id_a)); + } + + #[test] + fn test_walk_preorder() { + let mut graph = SceneGraph::new(); + + let id_a = graph.append_child(create_test_node("a"), Parent::Root); + let id_b = graph.append_child(create_test_node("b"), Parent::NodeId(id_a.clone())); + let id_c = graph.append_child(create_test_node("c"), Parent::NodeId(id_a.clone())); + + let mut visited = Vec::new(); + graph + .walk_preorder(&id_a, &mut |id| visited.push(id.clone())) + .unwrap(); + + assert_eq!(visited, vec![id_a.clone(), id_b, id_c]); + } + + #[test] + fn test_walk_postorder() { + let mut graph = SceneGraph::new(); + + let id_a = graph.append_child(create_test_node("a"), Parent::Root); + let id_b = graph.append_child(create_test_node("b"), Parent::NodeId(id_a.clone())); + let id_c = graph.append_child(create_test_node("c"), Parent::NodeId(id_a.clone())); + + let mut visited = Vec::new(); + graph + .walk_postorder(&id_a, &mut |id| visited.push(id.clone())) + .unwrap(); + + assert_eq!(visited, vec![id_b, id_c, id_a]); + } + + #[test] + fn test_ancestors() { + let mut graph = SceneGraph::new(); + + let id_a = graph.append_child(create_test_node("a"), Parent::Root); + let id_b = graph.append_child(create_test_node("b"), Parent::NodeId(id_a.clone())); + let id_c = graph.append_child(create_test_node("c"), Parent::NodeId(id_b.clone())); + + let ancestors = graph.ancestors(&id_c).unwrap(); + assert_eq!(ancestors, vec![id_b, id_a]); + } + + #[test] + fn test_descendants() { + let mut graph = SceneGraph::new(); + + let id_a = graph.append_child(create_test_node("a"), Parent::Root); + let id_b = graph.append_child(create_test_node("b"), Parent::NodeId(id_a.clone())); + let id_c = graph.append_child(create_test_node("c"), Parent::NodeId(id_b.clone())); + + let descendants = graph.descendants(&id_a).unwrap(); + assert_eq!(descendants.len(), 2); + assert!(descendants.contains(&id_b)); + assert!(descendants.contains(&id_c)); + } + + #[test] + fn test_error_node_not_found() { + let graph = SceneGraph::new(); + let result = graph.get_node(&"missing".to_string()); + assert!(matches!(result, Err(SceneGraphError::NodeNotFound(_)))); + } + + #[test] + fn test_error_parent_not_found() { + let mut graph = SceneGraph::new(); + let id_b = graph.append_child(create_test_node("b"), Parent::Root); + let result = graph.add_child(&"missing".to_string(), id_b); + assert!(matches!(result, Err(SceneGraphError::ParentNotFound(_)))); + } + + #[test] + fn test_append_child_to_root() { + let mut graph = SceneGraph::new(); + let node_a = create_test_node("a"); + let id_a = graph.append_child(node_a, Parent::Root); + + assert_eq!(graph.roots().len(), 1); + assert!(graph.roots().contains(&id_a)); + assert!(graph.has_node(&id_a)); + } + + #[test] + fn test_append_child_to_parent() { + let mut graph = SceneGraph::new(); + let parent = create_test_node("parent"); + let child = create_test_node("child"); + + let parent_id = graph.append_child(parent, Parent::Root); + let child_id = graph.append_child(child, Parent::NodeId(parent_id.clone())); + + assert_eq!(graph.get_children(&parent_id).unwrap().len(), 1); + assert_eq!(graph.get_children(&parent_id).unwrap()[0], child_id); + } + + #[test] + fn test_append_multiple_children() { + let mut graph = SceneGraph::new(); + let parent = create_test_node("parent"); + let child1 = create_test_node("child1"); + let child2 = create_test_node("child2"); + + let parent_id = graph.append_child(parent, Parent::Root); + let child1_id = graph.append_child(child1, Parent::NodeId(parent_id.clone())); + let child2_id = graph.append_child(child2, Parent::NodeId(parent_id.clone())); + + let children = graph.get_children(&parent_id).unwrap(); + assert_eq!(children.len(), 2); + assert_eq!(children[0], child1_id); + assert_eq!(children[1], child2_id); + } + + #[test] + fn test_append_children_to_root() { + let mut graph = SceneGraph::new(); + let nodes = vec![ + create_test_node("a"), + create_test_node("b"), + create_test_node("c"), + ]; + let ids = graph.append_children(nodes, Parent::Root); + + assert_eq!(graph.roots().len(), 3); + assert_eq!(ids.len(), 3); + assert!(graph.roots().contains(&ids[0])); + assert!(graph.roots().contains(&ids[1])); + assert!(graph.roots().contains(&ids[2])); + } + + #[test] + fn test_append_children_to_parent() { + let mut graph = SceneGraph::new(); + let parent = create_test_node("parent"); + let parent_id = graph.append_child(parent, Parent::Root); + + let children_nodes = vec![ + create_test_node("child1"), + create_test_node("child2"), + create_test_node("child3"), + ]; + let child_ids = graph.append_children(children_nodes, Parent::NodeId(parent_id.clone())); + + assert_eq!(child_ids.len(), 3); + let children = graph.get_children(&parent_id).unwrap(); + assert_eq!(children.len(), 3); + assert_eq!(children[0], child_ids[0]); + assert_eq!(children[1], child_ids[1]); + assert_eq!(children[2], child_ids[2]); + } + + #[test] + fn test_append_children_empty() { + let mut graph = SceneGraph::new(); + let ids = graph.append_children(vec![], Parent::Root); + + assert_eq!(ids.len(), 0); + assert_eq!(graph.roots().len(), 0); + } + + #[test] + fn test_new_from_snapshot() { + let node_a = create_test_node("a"); + let node_b = create_test_node("b"); + let node_c = create_test_node("c"); + + let id_a = node_a.id().clone(); + let id_b = node_b.id().clone(); + let id_c = node_c.id().clone(); + + let nodes = vec![node_a, node_b, node_c]; + let mut links = HashMap::new(); + links.insert(id_a.clone(), vec![id_b.clone(), id_c.clone()]); + let roots = vec![id_a.clone()]; + + let graph = SceneGraph::new_from_snapshot(nodes, links, roots); + + assert_eq!(graph.node_count(), 3); + assert_eq!(graph.roots().len(), 1); + assert_eq!(graph.get_children(&id_a).unwrap().len(), 2); + } + + #[test] + fn test_new_from_snapshot_empty() { + let graph = SceneGraph::new_from_snapshot(vec![], HashMap::new(), vec![]); + + assert_eq!(graph.node_count(), 0); + assert_eq!(graph.roots().len(), 0); + assert!(graph.is_empty()); + } + + #[test] + fn test_new_from_snapshot_complex_hierarchy() { + let node_root = create_test_node("root"); + let node_a = create_test_node("a"); + let node_b = create_test_node("b"); + let node_c = create_test_node("c"); + + let id_root = node_root.id().clone(); + let id_a = node_a.id().clone(); + let id_b = node_b.id().clone(); + let id_c = node_c.id().clone(); + + let nodes = vec![node_root, node_a, node_b, node_c]; + let mut links = HashMap::new(); + links.insert(id_root.clone(), vec![id_a.clone(), id_b.clone()]); + links.insert(id_b.clone(), vec![id_c.clone()]); + let roots = vec![id_root.clone()]; + + let graph = SceneGraph::new_from_snapshot(nodes, links, roots); + + assert_eq!(graph.node_count(), 4); + assert_eq!(graph.roots().len(), 1); + assert_eq!(graph.roots()[0], id_root); + assert_eq!(graph.get_children(&id_root).unwrap().len(), 2); + assert_eq!(graph.get_children(&id_b).unwrap().len(), 1); + } +} diff --git a/crates/grida-canvas/src/node/schema.rs b/crates/grida-canvas/src/node/schema.rs index 013c3cb6f5..33c55738f4 100644 --- a/crates/grida-canvas/src/node/schema.rs +++ b/crates/grida-canvas/src/node/schema.rs @@ -1,7 +1,7 @@ use crate::cg; use crate::cg::types::*; pub use crate::cg::types::{FontFeature, FontVariation}; -use crate::node::repository::NodeRepository; +use crate::node::scene_graph::SceneGraph; use crate::shape::*; use crate::vectornetwork::*; use math2::rect::Rectangle; @@ -80,15 +80,13 @@ pub struct Size { // region: Scene /// Runtime scene representation. /// -/// The children field is populated from the document's `links` map during deserialization. -/// In the new format, scenes are stored in `document.nodes` and their children are in `document.links`. +/// The scene graph contains both the tree structure (links) and node data (nodes). +/// This provides a centralized, efficient way to manage scene hierarchy. #[derive(Debug, Clone)] pub struct Scene { - pub id: String, pub name: String, - /// Children node IDs - populated from document.links during load - pub children: Vec, - pub nodes: NodeRepository, + /// Scene graph containing tree structure and node data + pub graph: SceneGraph, pub background_color: Option, } @@ -242,6 +240,27 @@ impl NodeTrait for Node { } } +impl Node { + pub fn mask(&self) -> Option { + match self { + Node::Group(n) => n.mask, + Node::Container(n) => n.mask, + Node::Rectangle(n) => n.mask, + Node::Ellipse(n) => n.mask, + Node::Polygon(n) => n.mask, + Node::RegularPolygon(n) => n.mask, + Node::RegularStarPolygon(n) => n.mask, + Node::Line(n) => n.mask, + Node::TextSpan(n) => n.mask, + Node::SVGPath(n) => n.mask, + Node::Vector(n) => n.mask, + Node::BooleanOperation(n) => n.mask, + Node::Image(n) => n.mask, + Node::Error(_) => None, + } + } +} + pub trait NodeFillsMixin { fn set_fill(&mut self, fill: Paint); fn set_fills(&mut self, fills: Paints); @@ -396,7 +415,6 @@ pub struct GroupNodeRec { pub mask: Option, pub transform: Option, - pub children: Vec, } #[derive(Debug, Clone)] @@ -412,7 +430,6 @@ pub struct ContainerNodeRec { pub transform: AffineTransform, pub size: Size, pub corner_radius: RectangularCornerRadius, - pub children: Vec, pub fills: Paints, pub strokes: Paints, pub stroke_width: f32, @@ -848,7 +865,6 @@ pub struct BooleanPathOperationNodeRec { pub transform: Option, pub op: BooleanPathOperation, pub corner_radius: Option, - pub children: Vec, pub fills: Paints, pub strokes: Paints, pub stroke_width: f32, diff --git a/crates/grida-canvas/src/painter/geometry.rs b/crates/grida-canvas/src/painter/geometry.rs index d1b5ab4793..5655a5efc7 100644 --- a/crates/grida-canvas/src/painter/geometry.rs +++ b/crates/grida-canvas/src/painter/geometry.rs @@ -1,5 +1,5 @@ use crate::cg::types::*; -use crate::node::repository::NodeRepository; +use crate::node::scene_graph::SceneGraph; use crate::node::schema::*; use crate::shape::*; use crate::{cache::geometry::GeometryCache, sk}; @@ -243,7 +243,7 @@ pub fn build_shape_from_node(node: &Node) -> Option { /// Compute the resulting path for a [`BooleanPathOperationNode`] in its local coordinate space. pub fn boolean_operation_path( node: &BooleanPathOperationNodeRec, - repo: &NodeRepository, + graph: &SceneGraph, cache: &GeometryCache, ) -> Option { let world = cache @@ -253,11 +253,12 @@ pub fn boolean_operation_path( let mut shapes_with_ops = Vec::new(); - for (i, child_id) in node.children.iter().enumerate() { - if let Some(child_node) = repo.get(child_id) { + let children = graph.get_children(&node.id)?; + for (i, child_id) in children.iter().enumerate() { + if let Ok(child_node) = graph.get_node(child_id) { let mut path = match child_node { Node::BooleanOperation(child_bool) => { - boolean_operation_path(child_bool, repo, cache)? + boolean_operation_path(child_bool, graph, cache)? } _ => build_shape_from_node(child_node)?.to_path(), }; @@ -298,8 +299,8 @@ pub fn boolean_operation_path( /// Convenience wrapper around [`boolean_operation_path`] returning a [`PainterShape`]. pub fn boolean_operation_shape( node: &BooleanPathOperationNodeRec, - repo: &NodeRepository, + graph: &SceneGraph, cache: &GeometryCache, ) -> Option { - boolean_operation_path(node, repo, cache).map(PainterShape::from_path) + boolean_operation_path(node, graph, cache).map(PainterShape::from_path) } diff --git a/crates/grida-canvas/src/painter/layer.rs b/crates/grida-canvas/src/painter/layer.rs index 418776b437..57e50d0107 100644 --- a/crates/grida-canvas/src/painter/layer.rs +++ b/crates/grida-canvas/src/painter/layer.rs @@ -3,7 +3,7 @@ use super::geometry::{ }; use crate::cache::scene::SceneCache; use crate::cg::types::*; -use crate::node::repository::NodeRepository; +use crate::node::scene_graph::SceneGraph; use crate::node::schema::*; use crate::shape::*; use crate::sk; @@ -216,8 +216,8 @@ impl LayerList { /// Flatten an entire scene into a layer list using the provided scene cache. pub fn from_scene(scene: &Scene, scene_cache: &SceneCache) -> Self { let mut list = LayerList::default(); - for id in &scene.children { - let result = Self::flatten_node(id, &scene.nodes, scene_cache, 1.0, &mut list.layers); + for id in scene.graph.roots() { + let result = Self::flatten_node(&id, &scene.graph, scene_cache, 1.0, &mut list.layers); list.commands.extend(result.commands); } // Build a LUT (id -> index) for picture caching and quick lookup @@ -228,12 +228,12 @@ impl LayerList { /// Build a layer list starting from a node subtree using a scene cache. pub fn from_node( id: &NodeId, - repo: &NodeRepository, + graph: &SceneGraph, scene_cache: &SceneCache, opacity: f32, ) -> Self { let mut list = LayerList::default(); - let result = Self::flatten_node(id, repo, scene_cache, opacity, &mut list.layers); + let result = Self::flatten_node(id, graph, scene_cache, opacity, &mut list.layers); list.commands = result.commands; list } @@ -244,12 +244,12 @@ impl LayerList { fn flatten_node( id: &NodeId, - repo: &NodeRepository, + graph: &SceneGraph, scene_cache: &SceneCache, parent_opacity: f32, out: &mut Vec, ) -> FlattenResult { - let Some(node) = repo.get(id) else { + let Ok(node) = graph.get_node(id) else { return FlattenResult::default(); }; @@ -265,10 +265,11 @@ impl LayerList { match node { Node::Group(n) => { let opacity = parent_opacity * n.opacity; + let children = graph.get_children(id).map(|c| c.as_slice()).unwrap_or(&[]); FlattenResult { commands: Self::build_render_commands( - &n.children, - repo, + children, + graph, scene_cache, opacity, out, @@ -296,7 +297,7 @@ impl LayerList { opacity, blend_mode: n.blend_mode, transform, - clip_path: Self::compute_clip_path(&n.id, repo, scene_cache), + clip_path: Self::compute_clip_path(&n.id, graph, scene_cache), }, shape, effects: n.effects.clone(), @@ -306,8 +307,9 @@ impl LayerList { }); out.push(layer.clone()); let mut commands = vec![PainterRenderCommand::Draw(layer)]; + let children = graph.get_children(id).map(|c| c.as_slice()).unwrap_or(&[]); let child_commands = - Self::build_render_commands(&n.children, repo, scene_cache, opacity, out); + Self::build_render_commands(children, graph, scene_cache, opacity, out); commands.extend(child_commands); FlattenResult { commands, @@ -316,7 +318,7 @@ impl LayerList { } Node::BooleanOperation(n) => { let opacity = parent_opacity * n.opacity; - if let Some(shape) = boolean_operation_shape(n, repo, scene_cache.geometry()) { + if let Some(shape) = boolean_operation_shape(n, graph, scene_cache.geometry()) { let stroke_path = if !n.strokes.is_empty() && n.stroke_width > 0.0 { Some(stroke_geometry( &shape.to_path(), @@ -334,7 +336,7 @@ impl LayerList { opacity, blend_mode: n.blend_mode, transform, - clip_path: Self::compute_clip_path(&n.id, repo, scene_cache), + clip_path: Self::compute_clip_path(&n.id, graph, scene_cache), }, shape, effects: n.effects.clone(), @@ -348,10 +350,11 @@ impl LayerList { mask: n.mask, } } else { + let children = graph.get_children(id).map(|c| c.as_slice()).unwrap_or(&[]); FlattenResult { commands: Self::build_render_commands( - &n.children, - repo, + children, + graph, scene_cache, opacity, out, @@ -379,7 +382,7 @@ impl LayerList { opacity: parent_opacity * n.opacity, blend_mode: n.blend_mode, transform, - clip_path: Self::compute_clip_path(&n.id, repo, scene_cache), + clip_path: Self::compute_clip_path(&n.id, graph, scene_cache), }, shape, effects: n.effects.clone(), @@ -412,7 +415,7 @@ impl LayerList { opacity: parent_opacity * n.opacity, blend_mode: n.blend_mode, transform, - clip_path: Self::compute_clip_path(&n.id, repo, scene_cache), + clip_path: Self::compute_clip_path(&n.id, graph, scene_cache), }, shape, effects: n.effects.clone(), @@ -445,7 +448,7 @@ impl LayerList { opacity: parent_opacity * n.opacity, blend_mode: n.blend_mode, transform, - clip_path: Self::compute_clip_path(&n.id, repo, scene_cache), + clip_path: Self::compute_clip_path(&n.id, graph, scene_cache), }, shape, effects: n.effects.clone(), @@ -478,7 +481,7 @@ impl LayerList { opacity: parent_opacity * n.opacity, blend_mode: n.blend_mode, transform, - clip_path: Self::compute_clip_path(&n.id, repo, scene_cache), + clip_path: Self::compute_clip_path(&n.id, graph, scene_cache), }, shape, effects: n.effects.clone(), @@ -511,7 +514,7 @@ impl LayerList { opacity: parent_opacity * n.opacity, blend_mode: n.blend_mode, transform, - clip_path: Self::compute_clip_path(&n.id, repo, scene_cache), + clip_path: Self::compute_clip_path(&n.id, graph, scene_cache), }, shape, effects: n.effects.clone(), @@ -544,7 +547,7 @@ impl LayerList { opacity: parent_opacity * n.opacity, blend_mode: n.blend_mode, transform, - clip_path: Self::compute_clip_path(&n.id, repo, scene_cache), + clip_path: Self::compute_clip_path(&n.id, graph, scene_cache), }, shape, effects: n.effects.clone(), @@ -591,7 +594,7 @@ impl LayerList { opacity: parent_opacity * n.opacity, blend_mode: n.blend_mode, transform, - clip_path: Self::compute_clip_path(&n.id, repo, scene_cache), + clip_path: Self::compute_clip_path(&n.id, graph, scene_cache), }, width: n.width, height: n.height, @@ -635,7 +638,7 @@ impl LayerList { opacity: parent_opacity * n.opacity, blend_mode: n.blend_mode, transform, - clip_path: Self::compute_clip_path(&n.id, repo, scene_cache), + clip_path: Self::compute_clip_path(&n.id, graph, scene_cache), }, shape, effects: n.effects.clone(), @@ -658,7 +661,7 @@ impl LayerList { opacity: parent_opacity * n.opacity, blend_mode: n.blend_mode, transform, - clip_path: Self::compute_clip_path(&n.id, repo, scene_cache), + clip_path: Self::compute_clip_path(&n.id, graph, scene_cache), }, shape, effects: n.effects.clone(), @@ -695,7 +698,7 @@ impl LayerList { opacity: parent_opacity * n.opacity, blend_mode: n.blend_mode, transform, - clip_path: Self::compute_clip_path(&n.id, repo, scene_cache), + clip_path: Self::compute_clip_path(&n.id, graph, scene_cache), }, shape, effects: n.effects.clone(), @@ -720,7 +723,7 @@ impl LayerList { opacity: parent_opacity * n.opacity, blend_mode: LayerBlendMode::PassThrough, transform, - clip_path: Self::compute_clip_path(&n.id, repo, scene_cache), + clip_path: Self::compute_clip_path(&n.id, graph, scene_cache), }, shape, effects: LayerEffects::default(), @@ -739,7 +742,7 @@ impl LayerList { fn build_render_commands( children: &[NodeId], - repo: &NodeRepository, + graph: &SceneGraph, scene_cache: &SceneCache, parent_opacity: f32, out: &mut Vec, @@ -750,7 +753,7 @@ impl LayerList { let mut out_commands = Vec::new(); let mut run: Vec = Vec::new(); for child_id in children { - let result = Self::flatten_node(child_id, repo, scene_cache, parent_opacity, out); + let result = Self::flatten_node(child_id, graph, scene_cache, parent_opacity, out); if let Some(mask_type) = result.mask { let mask_commands = result.commands; // Emit a scope with the accumulated run as content under this mask @@ -769,25 +772,6 @@ impl LayerList { out_commands } - fn node_mask(node: &Node) -> Option { - match node { - Node::Group(n) => n.mask, - Node::Container(n) => n.mask, - Node::Rectangle(n) => n.mask, - Node::Ellipse(n) => n.mask, - Node::Polygon(n) => n.mask, - Node::RegularPolygon(n) => n.mask, - Node::RegularStarPolygon(n) => n.mask, - Node::Line(n) => n.mask, - Node::TextSpan(n) => n.mask, - Node::SVGPath(n) => n.mask, - Node::Vector(n) => n.mask, - Node::BooleanOperation(n) => n.mask, - Node::Image(n) => n.mask, - Node::Error(_) => None, - } - } - pub fn filter(&self, filter: impl Fn(&PainterPictureLayer) -> bool) -> Self { let mut list = LayerList::default(); for layer in &self.layers { @@ -816,7 +800,7 @@ impl LayerList { /// An `Option` representing the merged clip path, or `None` if no clipping is needed. pub fn compute_clip_path( node_id: &NodeId, - repo: &NodeRepository, + graph: &SceneGraph, scene_cache: &SceneCache, ) -> Option { let mut clip_shapes = Vec::new(); @@ -834,7 +818,7 @@ impl LayerList { // Walk up the hierarchy to collect clip shapes while let Some(id) = current_id { - if let Some(node) = repo.get(&id) { + if let Ok(node) = graph.get_node(&id) { match node { Node::Container(n) => { if n.clip { @@ -858,7 +842,7 @@ impl LayerList { } Node::BooleanOperation(n) => { if let Some(mut path) = - boolean_operation_path(n, repo, scene_cache.geometry()) + boolean_operation_path(n, graph, scene_cache.geometry()) { let world_transform = scene_cache .geometry() diff --git a/crates/grida-canvas/src/painter/painter_debug_node.rs b/crates/grida-canvas/src/painter/painter_debug_node.rs index fe029f6819..abe4cb47e3 100644 --- a/crates/grida-canvas/src/painter/painter_debug_node.rs +++ b/crates/grida-canvas/src/painter/painter_debug_node.rs @@ -1,8 +1,7 @@ use super::geometry::*; -use super::layer::{LayerList, PainterPictureLayer}; use crate::cache::geometry::GeometryCache; use crate::cg::types::*; -use crate::node::repository::NodeRepository; +use crate::node::scene_graph::SceneGraph; use crate::node::schema::*; /// A painter specifically for drawing nodes, using the main Painter for operations. @@ -269,7 +268,7 @@ impl<'a> NodePainter<'a> { pub fn draw_container_node_recursively( &self, node: &ContainerNodeRec, - repository: &NodeRepository, + graph: &SceneGraph, cache: &GeometryCache, ) { self.painter.with_transform(&node.transform.matrix, || { @@ -287,18 +286,20 @@ impl<'a> NodePainter<'a> { // a clip region for the container's shape so that // descendants are clipped but the container's own stroke // remains unaffected. - if node.clip { - self.painter.with_clip(&shape, || { - for child_id in &node.children { - if let Some(child) = repository.get(child_id) { - self.draw_node_recursively(child, repository, cache); + if let Some(children) = graph.get_children(&node.id) { + if node.clip { + self.painter.with_clip(&shape, || { + for child_id in children { + if let Ok(child) = graph.get_node(child_id) { + self.draw_node_recursively(child, graph, cache); + } + } + }); + } else { + for child_id in children { + if let Ok(child) = graph.get_node(child_id) { + self.draw_node_recursively(child, graph, cache); } - } - }); - } else { - for child_id in &node.children { - if let Some(child) = repository.get(child_id) { - self.draw_node_recursively(child, repository, cache); } } } @@ -351,14 +352,16 @@ impl<'a> NodePainter<'a> { pub fn draw_group_node_recursively( &self, node: &GroupNodeRec, - repository: &NodeRepository, + graph: &SceneGraph, cache: &GeometryCache, ) { self.painter.with_transform_option(&node.transform, || { self.painter.with_opacity(node.opacity, || { - for child_id in &node.children { - if let Some(child) = repository.get(child_id) { - self.draw_node_recursively(child, repository, cache); + if let Some(children) = graph.get_children(&node.id) { + for child_id in children { + if let Ok(child) = graph.get_node(child_id) { + self.draw_node_recursively(child, graph, cache); + } } } }); @@ -368,11 +371,11 @@ impl<'a> NodePainter<'a> { pub fn draw_boolean_operation_node_recursively( &self, node: &BooleanPathOperationNodeRec, - repository: &NodeRepository, + graph: &SceneGraph, cache: &GeometryCache, ) { self.painter.with_transform_option(&node.transform, || { - if let Some(shape) = boolean_operation_shape(node, repository, cache) { + if let Some(shape) = boolean_operation_shape(node, graph, cache) { self.painter .draw_shape_with_effects(&node.effects, &shape, || { self.painter.with_opacity(node.opacity, || { @@ -393,9 +396,11 @@ impl<'a> NodePainter<'a> { }); }); } else { - for child_id in &node.children { - if let Some(child) = repository.get(child_id) { - self.draw_node_recursively(child, repository, cache); + if let Some(children) = graph.get_children(&node.id) { + for child_id in children { + if let Ok(child) = graph.get_node(child_id) { + self.draw_node_recursively(child, graph, cache); + } } } } @@ -424,19 +429,14 @@ impl<'a> NodePainter<'a> { } /// Dispatch to the correct node‐type draw method - pub fn draw_node_recursively( - &self, - node: &Node, - repository: &NodeRepository, - cache: &GeometryCache, - ) { + pub fn draw_node_recursively(&self, node: &Node, graph: &SceneGraph, cache: &GeometryCache) { if !node.active() { return; } match node { Node::Error(n) => self.draw_error_node(n), - Node::Group(n) => self.draw_group_node_recursively(n, repository, cache), - Node::Container(n) => self.draw_container_node_recursively(n, repository, cache), + Node::Group(n) => self.draw_group_node_recursively(n, graph, cache), + Node::Container(n) => self.draw_container_node_recursively(n, graph, cache), Node::Rectangle(n) => self.draw_rect_node(n), Node::Ellipse(n) => self.draw_ellipse_node(n), Node::Polygon(n) => self.draw_polygon_node(n), @@ -449,7 +449,7 @@ impl<'a> NodePainter<'a> { Node::Vector(n) => self.draw_vector_node(n), Node::SVGPath(n) => self.draw_path_node(n), Node::BooleanOperation(n) => { - self.draw_boolean_operation_node_recursively(n, repository, cache) + self.draw_boolean_operation_node_recursively(n, graph, cache) } Node::RegularStarPolygon(n) => self.draw_regular_star_polygon_node(n), } diff --git a/crates/grida-canvas/src/resources/mod.rs b/crates/grida-canvas/src/resources/mod.rs index d6a03c4488..143fef8ef5 100644 --- a/crates/grida-canvas/src/resources/mod.rs +++ b/crates/grida-canvas/src/resources/mod.rs @@ -122,20 +122,23 @@ pub struct FontMessage { /// Extract all image URLs from a scene. pub fn extract_image_urls(scene: &Scene) -> Vec { + // FIXME: this should either iterate the fills / strokes (all paints) rather then iterating the nodes. - the below implementation is legacy. let mut urls = Vec::new(); - for (_, n) in scene.nodes.iter() { - if let crate::node::schema::Node::Rectangle(rect) = n { - for fill in &rect.fills { - if let Paint::Image(img) = fill { - match &img.image { - ResourceRef::RID(r) | ResourceRef::HASH(r) => urls.push(r.clone()), + for (id, _) in scene.graph.iter() { + if let Ok(n) = scene.graph.get_node(id) { + if let crate::node::schema::Node::Rectangle(rect) = n { + for fill in &rect.fills { + if let Paint::Image(img) = fill { + match &img.image { + ResourceRef::RID(r) | ResourceRef::HASH(r) => urls.push(r.clone()), + } } } - } - for stroke in &rect.strokes { - if let Paint::Image(img) = stroke { - match &img.image { - ResourceRef::RID(r) | ResourceRef::HASH(r) => urls.push(r.clone()), + for stroke in &rect.strokes { + if let Paint::Image(img) = stroke { + match &img.image { + ResourceRef::RID(r) | ResourceRef::HASH(r) => urls.push(r.clone()), + } } } } diff --git a/crates/grida-canvas/src/runtime/scene.rs b/crates/grida-canvas/src/runtime/scene.rs index 089e61b46f..81c5767263 100644 --- a/crates/grida-canvas/src/runtime/scene.rs +++ b/crates/grida-canvas/src/runtime/scene.rs @@ -1,6 +1,6 @@ use crate::cache::tile::{ImageTileCacheResolutionStrategy, RegionTileInfo}; use crate::cg::types::*; -use crate::node::{repository::NodeRepository, schema::*}; +use crate::node::{scene_graph::SceneGraph, schema::*}; use crate::painter::layer::Layer; use crate::painter::Painter; use crate::runtime::counter::FrameCounter; @@ -63,35 +63,25 @@ impl Default for RendererOptions { } fn collect_scene_font_families(scene: &Scene) -> HashSet { - fn walk(id: &NodeId, repo: &NodeRepository, set: &mut HashSet) { - if let Some(node) = repo.get(id) { + fn walk(id: &NodeId, graph: &SceneGraph, set: &mut HashSet) { + if let Ok(node) = graph.get_node(id) { match node { Node::TextSpan(n) => { set.insert(n.text_style.font_family.clone()); } - Node::Group(n) => { - for child in &n.children { - walk(child, repo, set); - } - } - Node::Container(n) => { - for child in &n.children { - walk(child, repo, set); - } - } - Node::BooleanOperation(n) => { - for child in &n.children { - walk(child, repo, set); - } - } _ => {} } } + if let Some(children) = graph.get_children(id) { + for child in children { + walk(child, graph, set); + } + } } let mut set = HashSet::new(); - for id in &scene.children { - walk(id, &scene.nodes, &mut set); + for id in scene.graph.roots() { + walk(&id, &scene.graph, &mut set); } set } @@ -611,7 +601,7 @@ impl Renderer { fn draw_nocache( &self, canvas: &Canvas, - plan: &FramePlan, + _plan: &FramePlan, background_color: Option, width: f32, height: f32, @@ -679,26 +669,28 @@ impl Renderer { #[cfg(test)] mod tests { use super::*; - use crate::node::{factory::NodeFactory, repository::NodeRepository, schema::Size}; + use crate::node::{ + factory::NodeFactory, + scene_graph::{Parent, SceneGraph}, + schema::Size, + }; #[test] fn picture_recorded_with_layer_bounds() { let nf = NodeFactory::new(); - let mut repo = NodeRepository::new(); let mut rect = nf.create_rectangle_node(); rect.size = Size { width: 50.0, height: 40.0, }; - let rect_id = rect.id.clone(); - repo.insert(Node::Rectangle(rect)); + + let mut graph = SceneGraph::new(); + let rect_id = graph.append_child(Node::Rectangle(rect), Parent::Root); let scene = Scene { - id: "scene".into(), name: "test".into(), - children: vec![rect_id.clone()], - nodes: repo, + graph, background_color: None, }; @@ -755,17 +747,16 @@ mod tests { #[test] fn renderer_tracks_missing_fonts_from_scene() { let nf = NodeFactory::new(); - let mut repo = NodeRepository::new(); let mut text = nf.create_text_span_node(); text.text_style.font_family = "MissingFont".into(); - let text_id = repo.insert(Node::TextSpan(text)); + + let mut graph = SceneGraph::new(); + graph.append_child(Node::TextSpan(text), Parent::Root); let scene = Scene { - id: "scene".into(), name: "test".into(), - children: vec![text_id], - nodes: repo, + graph, background_color: None, }; diff --git a/crates/grida-canvas/src/vectornetwork/vn_painter.rs b/crates/grida-canvas/src/vectornetwork/vn_painter.rs index df1d6df10b..71fc9c4afe 100644 --- a/crates/grida-canvas/src/vectornetwork/vn_painter.rs +++ b/crates/grida-canvas/src/vectornetwork/vn_painter.rs @@ -256,8 +256,7 @@ impl<'a> VNPainter<'a> { mod tests { use super::*; use crate::cg::types::{ - BlendMode, CGColor, FillRule, ImagePaint, ImageRepeat, Paint, ResourceRef, SolidPaint, - StrokeAlign, + BlendMode, CGColor, FillRule, ImagePaint, Paint, ResourceRef, SolidPaint, StrokeAlign, }; use crate::cg::Alignment; use crate::resources::ByteStore; diff --git a/crates/grida-canvas/src/window/application.rs b/crates/grida-canvas/src/window/application.rs index 2f2994adfd..d929f8baa6 100644 --- a/crates/grida-canvas/src/window/application.rs +++ b/crates/grida-canvas/src/window/application.rs @@ -6,6 +6,7 @@ use crate::dummy; use crate::export::{export_node_as, ExportAs, Exported}; use crate::io::io_grida::{self, JSONVectorNetwork}; use crate::io::io_grida_patch::{self, TransactionApplyReport}; +use crate::node::scene_graph::SceneGraph; use crate::node::schema::*; use crate::resources::{FontMessage, ImageMessage}; use crate::runtime::camera::Camera2D; @@ -285,7 +286,7 @@ impl ApplicationApi for UnknownTargetApplication { fn to_vector_network(&mut self, id: &str) -> Option { if let Some(scene) = self.renderer.scene.as_ref() { - if let Some(node) = scene.nodes.get(&id.to_string()) { + if let Ok(node) = scene.graph.get_node(&id.to_string()) { let vn = match node { Node::Rectangle(n) => Some(n.to_vector_network()), Node::Ellipse(n) => Some(n.to_vector_network()), @@ -490,36 +491,29 @@ impl UnknownTargetApplication { .and_then(|c| c.clone()) .unwrap_or_default(); - // Convert nodes to repository, filtering out scene nodes - let mut node_repo = crate::node::repository::NodeRepository::new(); - for (node_id, json_node) in file.document.nodes { - // Skip scene nodes - they're handled separately - if matches!(json_node, io_grida::JSONNode::Scene(_)) { - continue; - } - - let mut node: Node = json_node.into(); - - // Populate children from links - if let Some(children_opt) = links.get(&node_id) { - if let Some(children) = children_opt { - match &mut node { - Node::Container(n) => n.children = children.clone(), - Node::Group(n) => n.children = children.clone(), - Node::BooleanOperation(n) => n.children = children.clone(), - _ => {} // Other nodes don't have children - } - } - } - - node_repo.insert(node); - } + // Convert all nodes (skip scene nodes) + let nodes: Vec = file + .document + .nodes + .into_iter() + .filter(|(_, json_node)| !matches!(json_node, io_grida::JSONNode::Scene(_))) + .map(|(_, json_node)| json_node.into()) + .collect(); + + // Filter links to remove None values + let filtered_links: std::collections::HashMap> = links + .into_iter() + .filter_map(|(parent_id, children_opt)| { + children_opt.map(|children| (parent_id, children)) + }) + .collect(); + + // Build scene graph from snapshot + let graph = SceneGraph::new_from_snapshot(nodes, filtered_links, scene_children); let scene = crate::node::schema::Scene { - id: scene_id, name: scene_name, - children: scene_children, - nodes: node_repo, + graph, background_color: bg_color, }; diff --git a/crates/grida-canvas/tests/export_as_pdf.rs b/crates/grida-canvas/tests/export_as_pdf.rs index 7779c48983..d02e561423 100644 --- a/crates/grida-canvas/tests/export_as_pdf.rs +++ b/crates/grida-canvas/tests/export_as_pdf.rs @@ -1,6 +1,10 @@ use cg::cg::types::*; use cg::export::{export_node_as, ExportAs}; -use cg::node::{factory::NodeFactory, repository::NodeRepository, schema::*}; +use cg::node::{ + factory::NodeFactory, + scene_graph::{Parent, SceneGraph}, + schema::*, +}; use cg::resources::ByteStore; use cg::runtime::{font_repository::FontRepository, image_repository::ImageRepository}; use math2::transform::AffineTransform; @@ -10,7 +14,7 @@ use std::sync::{Arc, Mutex}; fn test_pdf_export() { // Create a simple scene with a rectangle let nf = NodeFactory::new(); - let mut repo = NodeRepository::new(); + let mut graph = SceneGraph::new(); let mut rect = nf.create_rectangle_node(); rect.name = Some("Test Rectangle".to_string()); @@ -21,15 +25,12 @@ fn test_pdf_export() { rect.transform = AffineTransform::new(10.0, 10.0, 0.0); rect.fills = Paints::new([Paint::from(CGColor(255, 0, 0, 255))]); - let rect_id = rect.id.clone(); - repo.insert(Node::Rectangle(rect)); + let rect_id = graph.append_child(Node::Rectangle(rect), Parent::Root); let scene = Scene { - id: "test_scene".into(), name: "Test Scene".into(), - children: vec![rect_id.clone()], - nodes: repo, background_color: Some(CGColor(255, 255, 255, 255)), // White background + graph, }; let store = Arc::new(Mutex::new(ByteStore::new())); diff --git a/crates/grida-canvas/tests/geometry_cache.rs b/crates/grida-canvas/tests/geometry_cache.rs index b8b47ba5fe..b068e000bc 100644 --- a/crates/grida-canvas/tests/geometry_cache.rs +++ b/crates/grida-canvas/tests/geometry_cache.rs @@ -1,5 +1,9 @@ use cg::cache::geometry::GeometryCache; -use cg::node::{factory::NodeFactory, repository::NodeRepository, schema::*}; +use cg::node::{ + factory::NodeFactory, + scene_graph::{Parent, SceneGraph}, + schema::*, +}; use cg::resources::ByteStore; use cg::runtime::font_repository::FontRepository; use math2::transform::AffineTransform; @@ -8,43 +12,32 @@ use std::sync::{Arc, Mutex}; #[test] fn geometry_cache_builds_recursively() { let nf = NodeFactory::new(); - let mut repo = NodeRepository::new(); - - let mut rect = nf.create_rectangle_node(); - rect.transform = AffineTransform::new(4.0, 6.0, 0.0); - let rect_id = rect.id.clone(); - repo.insert(Node::Rectangle(rect)); + let mut graph = SceneGraph::new(); let mut group2 = nf.create_group_node(); group2.transform = Some(AffineTransform::new(2.0, 3.0, 0.0)); - group2.children.push(rect_id.clone()); - let group2_id = group2.id.clone(); - repo.insert(Node::Group(group2)); - let mut group1 = nf.create_group_node(); group1.transform = Some(AffineTransform::new(5.0, 5.0, 0.0)); - group1.children.push(group2_id.clone()); - let group1_id = group1.id.clone(); - repo.insert(Node::Group(group1)); - let mut container = nf.create_container_node(); container.transform = AffineTransform::new(10.0, 20.0, 0.0); - container.children.push(group1_id.clone()); - let container_id = container.id.clone(); - repo.insert(Node::Container(container)); + let mut rect = nf.create_rectangle_node(); + rect.transform = AffineTransform::new(4.0, 6.0, 0.0); + + let container_id = graph.append_child(Node::Container(container), Parent::Root); + let group1_id = graph.append_child(Node::Group(group1), Parent::NodeId(container_id.clone())); + let group2_id = graph.append_child(Node::Group(group2), Parent::NodeId(group1_id.clone())); + let rect_id = graph.append_child(Node::Rectangle(rect), Parent::NodeId(group2_id.clone())); let scene = Scene { - id: "scene".into(), name: "test".into(), - children: vec![container_id.clone()], - nodes: repo.clone(), background_color: None, + graph: graph.clone(), }; let store = Arc::new(Mutex::new(ByteStore::new())); let fonts = FontRepository::new(store); let cache = GeometryCache::from_scene(&scene, &fonts); - assert_eq!(cache.len(), repo.len()); + assert_eq!(cache.len(), graph.node_count()); let expected = AffineTransform::new(21.0, 34.0, 0.0); assert_eq!( @@ -59,32 +52,27 @@ fn geometry_cache_builds_recursively() { #[test] fn container_world_bounds_include_children() { let nf = NodeFactory::new(); - let mut repo = NodeRepository::new(); + let mut graph = SceneGraph::new(); + let mut container = nf.create_container_node(); + container.size = Size { + width: 100.0, + height: 100.0, + }; let mut rect = nf.create_rectangle_node(); rect.transform = AffineTransform::new(50.0, 50.0, 0.0); rect.size = Size { width: 100.0, height: 100.0, }; - let rect_id = rect.id.clone(); - repo.insert(Node::Rectangle(rect)); - let mut container = nf.create_container_node(); - container.size = Size { - width: 100.0, - height: 100.0, - }; - container.children.push(rect_id.clone()); - let container_id = container.id.clone(); - repo.insert(Node::Container(container)); + let container_id = graph.append_child(Node::Container(container), Parent::Root); + let rect_id = graph.append_child(Node::Rectangle(rect), Parent::NodeId(container_id.clone())); let scene = Scene { - id: "scene".into(), name: "test".into(), - children: vec![container_id.clone()], - nodes: repo, background_color: None, + graph, }; let store = Arc::new(Mutex::new(ByteStore::new())); diff --git a/crates/grida-canvas/tests/hit_test.rs b/crates/grida-canvas/tests/hit_test.rs index 0fca051e8b..d518151ff8 100644 --- a/crates/grida-canvas/tests/hit_test.rs +++ b/crates/grida-canvas/tests/hit_test.rs @@ -1,6 +1,10 @@ use cg::cache::scene::SceneCache; use cg::hittest::HitTester; -use cg::node::{factory::NodeFactory, repository::NodeRepository, schema::*}; +use cg::node::{ + factory::NodeFactory, + scene_graph::{Parent, SceneGraph}, + schema::*, +}; use cg::resources::ByteStore; use cg::runtime::font_repository::FontRepository; use math2::{rect::Rectangle, transform::AffineTransform}; @@ -9,32 +13,27 @@ use std::sync::{Arc, Mutex}; #[test] fn hit_first_returns_topmost() { let nf = NodeFactory::new(); - let mut repo = NodeRepository::new(); + let mut container = nf.create_container_node(); + container.size = Size { + width: 40.0, + height: 40.0, + }; let mut rect = nf.create_rectangle_node(); rect.transform = AffineTransform::new(10.0, 10.0, 0.0); rect.size = Size { width: 20.0, height: 20.0, }; - let rect_id = rect.id.clone(); - repo.insert(Node::Rectangle(rect)); - let mut container = nf.create_container_node(); - container.size = Size { - width: 40.0, - height: 40.0, - }; - let container_id = container.id.clone(); - container.children.push(rect_id.clone()); - repo.insert(Node::Container(container)); + let mut graph = SceneGraph::new(); + let container_id = graph.append_child(Node::Container(container), Parent::Root); + let rect_id = graph.append_child(Node::Rectangle(rect), Parent::NodeId(container_id.clone())); let scene = Scene { - id: "scene".into(), name: "test".into(), - children: vec![container_id.clone()], - nodes: repo, background_color: None, + graph, }; let mut cache = SceneCache::new(); @@ -58,19 +57,16 @@ fn hit_first_returns_topmost() { #[test] fn path_hit_testing_uses_contains() { let nf = NodeFactory::new(); - let mut repo = NodeRepository::new(); + let mut graph = SceneGraph::new(); let mut path_node = nf.create_path_node(); path_node.data = "M0 0 L10 0 L10 10 Z".into(); - let path_id = path_node.id.clone(); - repo.insert(Node::SVGPath(path_node.clone())); + let path_id = graph.append_child(Node::SVGPath(path_node.clone()), Parent::Root); let scene = Scene { - id: "scene".into(), name: "test".into(), - children: vec![path_id.clone()], - nodes: repo, background_color: None, + graph, }; let mut cache = SceneCache::new(); @@ -94,32 +90,27 @@ fn path_hit_testing_uses_contains() { #[test] fn intersects_returns_all_nodes_in_rect() { let nf = NodeFactory::new(); - let mut repo = NodeRepository::new(); + let mut container = nf.create_container_node(); + container.size = Size { + width: 100.0, + height: 100.0, + }; let mut rect = nf.create_rectangle_node(); rect.transform = AffineTransform::new(50.0, 50.0, 0.0); rect.size = Size { width: 100.0, height: 100.0, }; - let rect_id = rect.id.clone(); - repo.insert(Node::Rectangle(rect)); - let mut container = nf.create_container_node(); - container.size = Size { - width: 100.0, - height: 100.0, - }; - let container_id = container.id.clone(); - container.children.push(rect_id.clone()); - repo.insert(Node::Container(container)); + let mut graph = SceneGraph::new(); + let container_id = graph.append_child(Node::Container(container), Parent::Root); + let rect_id = graph.append_child(Node::Rectangle(rect), Parent::NodeId(container_id.clone())); let scene = Scene { - id: "scene".into(), name: "test".into(), - children: vec![container_id.clone()], - nodes: repo, background_color: None, + graph, }; let mut cache = SceneCache::new(); diff --git a/crates/grida-canvas/tests/render_bounds.rs b/crates/grida-canvas/tests/render_bounds.rs index 4ac42e717d..8a529c895d 100644 --- a/crates/grida-canvas/tests/render_bounds.rs +++ b/crates/grida-canvas/tests/render_bounds.rs @@ -1,6 +1,10 @@ use cg::cache::geometry::GeometryCache; use cg::cg::types::*; -use cg::node::{factory::NodeFactory, repository::NodeRepository, schema::*}; +use cg::node::{ + factory::NodeFactory, + scene_graph::{Parent, SceneGraph}, + schema::*, +}; use cg::resources::ByteStore; use cg::runtime::font_repository::FontRepository; use std::sync::{Arc, Mutex}; @@ -8,20 +12,18 @@ use std::sync::{Arc, Mutex}; #[test] fn stroke_affects_render_bounds() { let nf = NodeFactory::new(); - let mut repo = NodeRepository::new(); + let mut graph = SceneGraph::new(); let mut rect = nf.create_rectangle_node(); rect.stroke_width = 10.0; rect.stroke_align = StrokeAlign::Outside; - let rect_id = rect.id.clone(); - repo.insert(Node::Rectangle(rect)); + + let rect_id = graph.append_child(Node::Rectangle(rect), Parent::Root); let scene = Scene { - id: "scene".into(), name: "test".into(), - children: vec![rect_id.clone()], - nodes: repo, background_color: None, + graph, }; let store = Arc::new(Mutex::new(ByteStore::new())); @@ -37,21 +39,19 @@ fn stroke_affects_render_bounds() { #[test] fn gaussian_blur_expands_render_bounds() { let nf = NodeFactory::new(); - let mut repo = NodeRepository::new(); + let mut graph = SceneGraph::new(); let mut rect = nf.create_rectangle_node(); rect.effects = LayerEffects::from_array(vec![FilterEffect::LayerBlur(FeGaussianBlur { radius: 5.0, })]); - let rect_id = rect.id.clone(); - repo.insert(Node::Rectangle(rect)); + + let rect_id = graph.append_child(Node::Rectangle(rect), Parent::Root); let scene = Scene { - id: "scene".into(), name: "test".into(), - children: vec![rect_id.clone()], - nodes: repo, background_color: None, + graph, }; let store = Arc::new(Mutex::new(ByteStore::new())); @@ -67,7 +67,7 @@ fn gaussian_blur_expands_render_bounds() { #[test] fn drop_shadow_expands_render_bounds() { let nf = NodeFactory::new(); - let mut repo = NodeRepository::new(); + let mut graph = SceneGraph::new(); let mut rect = nf.create_rectangle_node(); rect.effects = LayerEffects::from_array(vec![FilterEffect::DropShadow(FeShadow { @@ -77,15 +77,13 @@ fn drop_shadow_expands_render_bounds() { spread: 0.0, color: CGColor(0, 0, 0, 255), })]); - let rect_id = rect.id.clone(); - repo.insert(Node::Rectangle(rect)); + + let rect_id = graph.append_child(Node::Rectangle(rect), Parent::Root); let scene = Scene { - id: "scene".into(), name: "test".into(), - children: vec![rect_id.clone()], - nodes: repo, background_color: None, + graph, }; let store = Arc::new(Mutex::new(ByteStore::new())); @@ -101,7 +99,7 @@ fn drop_shadow_expands_render_bounds() { #[test] fn drop_shadow_spread_expands_render_bounds() { let nf = NodeFactory::new(); - let mut repo = NodeRepository::new(); + let mut graph = SceneGraph::new(); let mut rect = nf.create_rectangle_node(); rect.effects = LayerEffects::from_array(vec![FilterEffect::DropShadow(FeShadow { @@ -111,15 +109,13 @@ fn drop_shadow_spread_expands_render_bounds() { spread: 10.0, color: CGColor(0, 0, 0, 255), })]); - let rect_id = rect.id.clone(); - repo.insert(Node::Rectangle(rect)); + + let rect_id = graph.append_child(Node::Rectangle(rect), Parent::Root); let scene = Scene { - id: "scene".into(), name: "test".into(), - children: vec![rect_id.clone()], - nodes: repo, background_color: None, + graph, }; let store = Arc::new(Mutex::new(ByteStore::new())); diff --git a/crates/grida-canvas/tests/scene_cache.rs b/crates/grida-canvas/tests/scene_cache.rs index fb3faf492c..61ac5dcc5e 100644 --- a/crates/grida-canvas/tests/scene_cache.rs +++ b/crates/grida-canvas/tests/scene_cache.rs @@ -1,5 +1,9 @@ use cg::cache::scene::SceneCache; -use cg::node::{factory::NodeFactory, repository::NodeRepository, schema::*}; +use cg::node::{ + factory::NodeFactory, + scene_graph::{Parent, SceneGraph}, + schema::*, +}; use cg::painter::layer::Layer; use cg::resources::ByteStore; use cg::runtime::font_repository::FontRepository; @@ -10,32 +14,27 @@ use std::sync::{Arc, Mutex}; #[test] fn layers_in_rect_include_partially_visible_nested() { let nf = NodeFactory::new(); - let mut repo = NodeRepository::new(); + let mut graph = SceneGraph::new(); + let mut container = nf.create_container_node(); + container.size = Size { + width: 100.0, + height: 100.0, + }; let mut rect = nf.create_rectangle_node(); rect.transform = AffineTransform::new(50.0, 50.0, 0.0); rect.size = Size { width: 100.0, height: 100.0, }; - let rect_id = rect.id.clone(); - repo.insert(Node::Rectangle(rect)); - let mut container = nf.create_container_node(); - container.size = Size { - width: 100.0, - height: 100.0, - }; - let container_id = container.id.clone(); - container.children.push(rect_id.clone()); - repo.insert(Node::Container(container)); + let container_id = graph.append_child(Node::Container(container), Parent::Root); + let rect_id = graph.append_child(Node::Rectangle(rect), Parent::NodeId(container_id.clone())); let scene = Scene { - id: "scene".into(), name: "test".into(), - children: vec![container_id.clone()], - nodes: repo, background_color: None, + graph, }; let mut cache = SceneCache::new();