Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
310 changes: 285 additions & 25 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ petgraph = { version = "0.7", default-features = false, features = ["graphmap"]
half = { version = "2.4", default-features = false, features = ["bytemuck"] }
tinyvec = { version = "1", features = ["std"] }
criterion = { version = "0.7", features = ["html_reports"] }
vtracer = { version = "0.6.5" }
visioncortex = { version = "0.8.9" }
gungraun = { version = "0.18" }
ndarray = "0.16"
strum = { version = "0.27", features = ["derive"] }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
// Force chosen tool to be Select Tool after importing image.
responses.add(ToolMessage::ActivateTool { tool_type: ToolType::Select });
}

DocumentMessage::PasteSvg {
name,
svg,
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use super::transform_utils;
use crate::consts::{LAYER_INDENT_OFFSET, STACK_VERTICAL_GAP};
use crate::messages::portfolio::document::node_graph::document_node_definitions::{DefinitionIdentifier, resolve_document_node_type, resolve_network_node_type, resolve_proto_node_type};
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{self, FlowType, InputConnector, NodeNetworkInterface, OutputConnector};
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::{get_fill_input_node_id, get_upstream_gradient_value_node_id, gradient_chain_target_input};
use glam::{DAffine2, DVec2};
use glam::{DAffine2, DVec2, IVec2};
use graph_craft::application_io::resource::ResourceId;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput};
Expand All @@ -13,6 +14,8 @@ use graphene_std::brush::brush_stroke::BrushStroke;
use graphene_std::list::List;
use graphene_std::raster::BlendMode;
use graphene_std::raster_types::Image;
use graphene_std::renderer::Quad;
use graphene_std::renderer::usvg_utils::{convert_usvg_path, extract_usvg_fill, extract_usvg_stroke, usvg_transform};
use graphene_std::subpath::Subpath;
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{Fill, GradientSpreadMethod, GradientType, Stroke};
Expand Down Expand Up @@ -805,6 +808,218 @@ impl<'a> ModifyInputsContext<'a> {
}
}

/// Import a usvg node as the root of an SVG import operation.
///
/// The root layer uses the full `move_layer_to_stack` (with push/collision logic) to correctly
/// interact with any existing layers in the parent stack. All descendant layers use a lightweight
/// O(n) import path that skips collision detection and instead calculates positions directly from
/// the known tree structure.
pub fn import_usvg_node(
modify_inputs: &mut ModifyInputsContext,
node: &usvg::Node,
id: NodeId,
parent: LayerNodeIdentifier,
insert_index: usize,
graphite_gradient_stops: &HashMap<String, GradientStops>,
) {
let layer = modify_inputs.create_layer(id);

modify_inputs.network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
modify_inputs.layer_node = Some(layer);
if let Some(upstream_layer) = layer.next_sibling(modify_inputs.network_interface.document_metadata()) {
modify_inputs.network_interface.shift_node(&upstream_layer.to_node(), IVec2::new(0, STACK_VERTICAL_GAP), &[]);
}

match node {
usvg::Node::Group(group) => {
// Collect child extents for O(n) position calculation
let mut child_extents_svg_order: Vec<u32> = Vec::new();
let mut group_extents_map: HashMap<LayerNodeIdentifier, Vec<u32>> = HashMap::new();

// Enable import mode: skips expensive is_acyclic checks and per-node cache invalidation
// during wiring since we're building a known tree structure where cycles are impossible
modify_inputs.import = true;

for child in group.children() {
let extent = import_usvg_node_inner(modify_inputs, child, NodeId::new(), layer, 0, graphite_gradient_stops, &mut group_extents_map);
child_extents_svg_order.push(extent);
}

modify_inputs.import = false;
modify_inputs.layer_node = Some(layer);

// Rebuild the layer tree once now that all wiring is complete
modify_inputs.network_interface.load_structure();

// Set positions for all imported descendants in a single O(n) pass
let parent_pos = modify_inputs.network_interface.position(&layer.to_node(), &[]).unwrap_or(IVec2::ZERO);
set_import_child_positions(modify_inputs.network_interface, layer, parent_pos, &child_extents_svg_order, &group_extents_map);

// Invalidate caches once after all positions are set
modify_inputs.network_interface.unload_all_nodes_click_targets(&[]);
modify_inputs.network_interface.unload_all_nodes_bounding_box(&[]);
}
usvg::Node::Path(path) => {
import_usvg_path(modify_inputs, node, path, layer, graphite_gradient_stops);
}
usvg::Node::Image(_image) => {
warn!("Skip image");
}
usvg::Node::Text(text) => {
let font = Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.to_string(), graphene_std::consts::DEFAULT_FONT_STYLE.to_string());
modify_inputs.insert_text(text.chunks().iter().map(|chunk| chunk.text()).collect(), font, TypesettingConfig::default(), layer);
modify_inputs.fill_set(Fill::Solid(Color::BLACK));
}
}
}

/// Recursively import a usvg node as a descendant of the root import layer.
/// Uses lightweight wiring (no push/collision) and returns the subtree extent for position calculation.
///
/// The subtree extent represents the additional vertical grid units that this node's descendants
/// occupy below the node's position. This is used to calculate correct y_offsets between siblings.
pub fn import_usvg_node_inner(
modify_inputs: &mut ModifyInputsContext,
node: &usvg::Node,
id: NodeId,
parent: LayerNodeIdentifier,
insert_index: usize,
graphite_gradient_stops: &HashMap<String, GradientStops>,
group_extents_map: &mut HashMap<LayerNodeIdentifier, Vec<u32>>,
) -> u32 {
let layer = modify_inputs.create_layer(id);
modify_inputs.network_interface.move_layer_to_stack_for_import(layer, parent, insert_index, &[]);
modify_inputs.layer_node = Some(layer);

match node {
usvg::Node::Group(group) => {
let mut child_extents: Vec<u32> = Vec::new();
for child in group.children() {
let extent = import_usvg_node_inner(modify_inputs, child, NodeId::new(), layer, 0, graphite_gradient_stops, group_extents_map);
child_extents.push(extent);
}
modify_inputs.layer_node = Some(layer);

let n = child_extents.len();
let total_extent = if n == 0 {
0
} else {
(2 * STACK_VERTICAL_GAP as u32) * n as u32 - STACK_VERTICAL_GAP as u32 + child_extents.iter().sum::<u32>()
};
group_extents_map.insert(layer, child_extents);
total_extent
}
usvg::Node::Path(path) => {
import_usvg_path(modify_inputs, node, path, layer, graphite_gradient_stops);
0
}
usvg::Node::Image(_image) => {
warn!("Skip image");
0
}
usvg::Node::Text(text) => {
let font = Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.to_string(), graphene_std::consts::DEFAULT_FONT_STYLE.to_string());
modify_inputs.insert_text(text.chunks().iter().map(|chunk| chunk.text()).collect(), font, TypesettingConfig::default(), layer);
modify_inputs.fill_set(Fill::Solid(Color::BLACK));
0
}
}
}

/// Set correct positions for all imported layers in a single top-down O(n) pass.
///
/// For each group's child stack:
/// - The top-of-stack child (last SVG child) gets an `Absolute` position at `(parent_x - LAYER_INDENT_OFFSET, parent_y + STACK_VERTICAL_GAP)`
/// - All other children get `Stack(y_offset)` where `y_offset` accounts for the subtree extent of the sibling above them in the stack, ensuring no overlap.
pub fn set_import_child_positions(
network_interface: &mut NodeNetworkInterface,
group: LayerNodeIdentifier,
group_pos: IVec2,
child_extents_svg_order: &[u32],
group_extents_map: &HashMap<LayerNodeIdentifier, Vec<u32>>,
) {
use crate::messages::portfolio::document::utility_types::network_interface::LayerPosition;

let layer_children: Vec<_> = group.children(network_interface.document_metadata()).collect();
let n = child_extents_svg_order.len();

if n == 0 || layer_children.is_empty() {
return;
}

// Children in the layer tree are in stack order (top to bottom), which is the REVERSE of SVG order.
// SVG order: [s_0, s_1, ..., s_{n-1}] with extents [e_0, e_1, ..., e_{n-1}]
// Stack order: [s_{n-1}, s_{n-2}, ..., s_0 ] (top to bottom)
//
// For stack child at index i:
// - SVG index = n - 1 - i
// - Previous stack sibling's SVG index = n - i
// - y_offset = extent_of_previous_sibling + STACK_VERTICAL_GAP

let child_x = group_pos.x - LAYER_INDENT_OFFSET;
let mut current_y = group_pos.y + STACK_VERTICAL_GAP;

for (i, child_layer) in layer_children.iter().enumerate() {
let child_pos = IVec2::new(child_x, current_y);

if i == 0 {
// Top of stack: set to `Absolute` position
network_interface.set_layer_position_for_import(&child_layer.to_node(), LayerPosition::Absolute(child_pos), &[]);
} else {
// Below top: set `Stack` with `y_offset` based on previous sibling's subtree extent
let prev_sibling_svg_index = n - i;
let y_offset = child_extents_svg_order[prev_sibling_svg_index] + STACK_VERTICAL_GAP as u32;
network_interface.set_layer_position_for_import(&child_layer.to_node(), LayerPosition::Stack(y_offset), &[]);
}

// Recurse into group children to set their descendants' positions
if let Some(grandchild_extents) = group_extents_map.get(child_layer) {
set_import_child_positions(network_interface, *child_layer, child_pos, grandchild_extents, group_extents_map);
}

// Advance `current_y` for the next child: node height (STACK_VERTICAL_GAP) + gap (STACK_VERTICAL_GAP) + subtree extent
let child_svg_index = n - 1 - i;
let child_extent = child_extents_svg_order[child_svg_index];
current_y += 2 * STACK_VERTICAL_GAP + child_extent as i32;
}
}

/// Helper to apply path data (vector geometry, fill, stroke, transform) to a layer.
///
pub fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, path: &usvg::Path, layer: LayerNodeIdentifier, graphite_gradient_stops: &HashMap<String, GradientStops>) {
let subpaths = convert_usvg_path(path);

let bounds = subpaths.iter().filter_map(|subpath| subpath.bounding_box()).reduce(Quad::combine_bounds).unwrap_or_default();

// Skip creating a Transform node entirely when the SVG-native transform is identity.
let node_transform = usvg_transform(node.abs_transform());
let has_transform = node_transform != DAffine2::IDENTITY;
modify_inputs.insert_vector(subpaths, layer, has_transform, path.fill().is_some(), path.stroke().is_some());
if has_transform && let Some(transform_node_id) = modify_inputs.existing_proto_node_id(graphene_std::transform_nodes::transform::IDENTIFIER, false) {
transform_utils::update_transform(modify_inputs.network_interface, &transform_node_id, node_transform);
}

if let Some(fill) = path.fill() {
let bounds_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
apply_usvg_fill(fill, modify_inputs, bounds_transform, graphite_gradient_stops);
}
if let Some(stroke) = path.stroke() {
apply_usvg_stroke(stroke, modify_inputs, node_transform);
}
}

pub fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, bounds_transform: DAffine2, graphite_gradient_stops: &HashMap<String, GradientStops>) {
if let Some(fill) = extract_usvg_fill(fill, bounds_transform, graphite_gradient_stops) {
modify_inputs.fill_set(fill);
}
}

pub fn apply_usvg_stroke(stroke: &usvg::Stroke, modify_inputs: &mut ModifyInputsContext, transform: DAffine2) {
if let Some(stroke) = extract_usvg_stroke(stroke, transform) {
modify_inputs.stroke_set(stroke)
}
}

/// Rebuild the y-axis so its (parallel, perpendicular) components in the x-axis-aligned frame stay constant, both
/// rescaled by `|new_x| / |old_x|`. This holds the (x, y) parallelogram's aspect ratio and skew fixed across an endpoint
/// drag, so a radial ellipse stays the same shape (just rotated and resized) instead of distorting as x grows or shrinks.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ pub fn new_image_layer(image: Image<Color>, id: NodeId, parent: LayerNodeIdentif
}

/// Create a new group layer from an SVG string.
///
pub fn new_svg_layer(svg: String, transform: glam::DAffine2, center: bool, id: NodeId, parent: LayerNodeIdentifier, responses: &mut VecDeque<Message>) -> LayerNodeIdentifier {
let insert_index = 0;
responses.add(GraphOperationMessage::NewSvg {
Expand Down
2 changes: 2 additions & 0 deletions node-graph/libraries/rendering/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ vello = { workspace = true }
vello_encoding = { workspace = true }
parley = { workspace = true }
skrifa = { workspace = true }
vtracer = { workspace = true }
visioncortex = { workspace = true }

# Optional workspace dependencies
serde = { workspace = true, optional = true }
47 changes: 0 additions & 47 deletions node-graph/libraries/rendering/src/convert_usvg_path.rs

This file was deleted.

3 changes: 2 additions & 1 deletion node-graph/libraries/rendering/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod convert_usvg_path;
pub mod render_ext;
mod renderer;
pub mod to_peniko;
pub mod usvg_utils;
pub mod vtracer_utils;

pub use renderer::*;
Loading
Loading