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
313 changes: 288 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 @@ -1548,6 +1548,7 @@ fn static_node_properties() -> NodeProperties {
"monitor_properties".to_string(),
Box::new(|_node_id, _context| node_properties::string_properties("Used internally by the editor to obtain a layer thumbnail.")),
);
map.insert("vectorize_properties".to_string(), Box::new(node_properties::vectorize_properties));
map
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ use graphene_std::transform::{Footprint, ReferencePoint, ScaleType, Transform};
use graphene_std::vector::misc::BooleanOperation;
use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType};
use graphene_std::vector::style::{Fill, FillChoice, FillChoiceUI, GradientSpreadMethod, GradientStops, GradientStopsUI, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::vector::vectorize::{
ColorModeInput, ColorPrecisionInput, CornerThresholdInput, FilterSpeckleInput, HierarchicalInput, LayerDifferenceInput, LengthThresholdInput, MaxIterationsInput, PathPrecisionInput,
PathSimplifyModeInput, SpliceThresholdInput,
};
use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification};
use graphene_std::vector_types::vectorize_config;

pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
let widget = TextLabel::new(text).widget_instance();
Expand Down Expand Up @@ -2765,6 +2770,39 @@ pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) ->
]
}

pub fn vectorize_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let color_mode = enum_choice::<vectorize_config::ColorMode>()
.for_socket(ParameterWidgetsInfo::new(node_id, ColorModeInput::INDEX, true, context))
.property_row();
let hierarchical = enum_choice::<vectorize_config::Hierarchical>()
.for_socket(ParameterWidgetsInfo::new(node_id, HierarchicalInput::INDEX, true, context))
.property_row();
let filter_speckle = number_widget(ParameterWidgetsInfo::new(node_id, FilterSpeckleInput::INDEX, true, context), NumberInput::default().int().min(0.));
let color_precision = number_widget(ParameterWidgetsInfo::new(node_id, ColorPrecisionInput::INDEX, true, context), NumberInput::default().int().min(0.));
let layer_difference = number_widget(ParameterWidgetsInfo::new(node_id, LayerDifferenceInput::INDEX, true, context), NumberInput::default().int().min(0.));
let path_simplify_mode = enum_choice::<vectorize_config::PathSimplifyMode>()
.for_socket(ParameterWidgetsInfo::new(node_id, PathSimplifyModeInput::INDEX, true, context))
.property_row();
let corner_threshold = number_widget(ParameterWidgetsInfo::new(node_id, CornerThresholdInput::INDEX, true, context), NumberInput::default().int().min(0.));
let length_threshold = number_widget(ParameterWidgetsInfo::new(node_id, LengthThresholdInput::INDEX, true, context), NumberInput::default().min(0.));
let max_iterations = number_widget(ParameterWidgetsInfo::new(node_id, MaxIterationsInput::INDEX, true, context), NumberInput::default().int().min(0.));
let splice_threshold = number_widget(ParameterWidgetsInfo::new(node_id, SpliceThresholdInput::INDEX, true, context), NumberInput::default().int().min(0.));
let path_precision = number_widget(ParameterWidgetsInfo::new(node_id, PathPrecisionInput::INDEX, true, context), NumberInput::default().int().min(0.));
vec![
color_mode,
hierarchical,
LayoutGroup::row(filter_speckle),
LayoutGroup::row(color_precision),
LayoutGroup::row(layer_difference),
path_simplify_mode,
LayoutGroup::row(corner_threshold),
LayoutGroup::row(length_threshold),
LayoutGroup::row(max_iterations),
LayoutGroup::row(splice_threshold),
LayoutGroup::row(path_precision),
]
}

pub struct ParameterWidgetsInfo<'a> {
network_interface: &'a NodeNetworkInterface,
resources: &'a ResourceMessageHandler,
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
1 change: 1 addition & 0 deletions node-graph/graph-craft/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ graphene-application-io = { workspace = true, features = ["serde"] }
rendering = { workspace = true, features = ["serde"] }
raster-nodes = { workspace = true, features = ["serde"] }
vector-nodes = { workspace = true, features = ["serde"] }
vector-types = { workspace = true, features = ["serde"] }
graphic-types = { workspace = true, features = ["serde"] }
text-nodes = { workspace = true, features = ["serde"] }

Expand Down
3 changes: 3 additions & 0 deletions node-graph/graph-craft/src/document/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,9 @@ tagged_value! {
BooleanOperation(vector::misc::BooleanOperation),
TextAlign(text_nodes::TextAlign),
ScaleType(core_types::transform::ScaleType),
ColorMode(vector_types::vectorize_config::ColorMode),
Hierarchical(vector_types::vectorize_config::Hierarchical),
PathSimplifyMode(vector_types::vectorize_config::PathSimplifyMode)
}

impl TaggedValue {
Expand Down
47 changes: 0 additions & 47 deletions node-graph/libraries/rendering/src/convert_usvg_path.rs

This file was deleted.

Loading
Loading