Skip to content

Commit a10092c

Browse files
authored
Fix abysmal O(n^2) SVG import performance (#3938)
* Remove O(n^2) import by disabling bumping * Add an import mode to avoid acyclic checks * Rebuild the layer tree at the end, not after each step * Incrementally update outward wires instead of repeatedly rebuilding them * Add import->export direct connection guard * Code review fixes * Replace magic number offsets with consts * Add consts for magic numbers * Improve code structuring
1 parent 9727e14 commit a10092c

File tree

5 files changed

+359
-56
lines changed

5 files changed

+359
-56
lines changed

editor/src/consts.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ pub const EXPORTS_TO_TOP_EDGE_PIXEL_GAP: u32 = 72;
44
pub const EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP: u32 = 120;
55
pub const IMPORTS_TO_TOP_EDGE_PIXEL_GAP: u32 = 72;
66
pub const IMPORTS_TO_LEFT_EDGE_PIXEL_GAP: u32 = 120;
7+
/// Vertical grid distance between adjacent stack siblings, or between a parent layer and its first stack child.
8+
pub const STACK_VERTICAL_GAP: i32 = 3;
9+
/// Horizontal grid indentation of a child layer relative to its parent layer.
10+
pub const LAYER_INDENT_OFFSET: i32 = 8;
711

812
// VIEWPORT
913
pub const VIEWPORT_ZOOM_WHEEL_RATE: f64 = (1. / 600.) * 3.;

editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs

Lines changed: 170 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use super::transform_utils;
22
use super::utility_types::ModifyInputsContext;
3+
use crate::consts::{LAYER_INDENT_OFFSET, STACK_VERTICAL_GAP};
34
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
45
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
56
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface, OutputConnector};
@@ -442,6 +443,12 @@ fn parse_hex_stop_color(hex: &str, opacity: f32) -> Option<Color> {
442443
Some(Color::from_rgbaf32_unchecked(r, g, b, opacity))
443444
}
444445

446+
/// Import a usvg node as the root of an SVG import operation.
447+
///
448+
/// The root layer uses the full `move_layer_to_stack` (with push/collision logic) to correctly
449+
/// interact with any existing layers in the parent stack. All descendant layers use a lightweight
450+
/// O(n) import path that skips collision detection and instead calculates positions directly from
451+
/// the known tree structure.
445452
fn import_usvg_node(
446453
modify_inputs: &mut ModifyInputsContext,
447454
node: &usvg::Node,
@@ -452,44 +459,192 @@ fn import_usvg_node(
452459
graphite_gradient_stops: &HashMap<String, GradientStops>,
453460
) {
454461
let layer = modify_inputs.create_layer(id);
462+
455463
modify_inputs.network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
456464
modify_inputs.layer_node = Some(layer);
457465
if let Some(upstream_layer) = layer.next_sibling(modify_inputs.network_interface.document_metadata()) {
458-
modify_inputs.network_interface.shift_node(&upstream_layer.to_node(), IVec2::new(0, 3), &[]);
466+
modify_inputs.network_interface.shift_node(&upstream_layer.to_node(), IVec2::new(0, STACK_VERTICAL_GAP), &[]);
459467
}
468+
460469
match node {
461470
usvg::Node::Group(group) => {
471+
// Collect child extents for O(n) position calculation
472+
let mut child_extents_svg_order: Vec<u32> = Vec::new();
473+
let mut group_extents_map: HashMap<LayerNodeIdentifier, Vec<u32>> = HashMap::new();
474+
475+
// Enable import mode: skips expensive is_acyclic checks and per-node cache invalidation
476+
// during wiring since we're building a known tree structure where cycles are impossible
477+
modify_inputs.import = true;
478+
462479
for child in group.children() {
463-
import_usvg_node(modify_inputs, child, transform, NodeId::new(), layer, 0, graphite_gradient_stops);
480+
let extent = import_usvg_node_inner(modify_inputs, child, transform, NodeId::new(), layer, 0, graphite_gradient_stops, &mut group_extents_map);
481+
child_extents_svg_order.push(extent);
464482
}
483+
484+
modify_inputs.import = false;
465485
modify_inputs.layer_node = Some(layer);
486+
487+
// Rebuild the layer tree once now that all wiring is complete
488+
modify_inputs.network_interface.load_structure();
489+
490+
// Set positions for all imported descendants in a single O(n) pass
491+
let parent_pos = modify_inputs.network_interface.position(&layer.to_node(), &[]).unwrap_or(IVec2::ZERO);
492+
set_import_child_positions(modify_inputs.network_interface, layer, parent_pos, &child_extents_svg_order, &group_extents_map);
493+
494+
// Invalidate caches once after all positions are set
495+
modify_inputs.network_interface.unload_all_nodes_click_targets(&[]);
496+
modify_inputs.network_interface.unload_all_nodes_bounding_box(&[]);
466497
}
467498
usvg::Node::Path(path) => {
468-
let subpaths = convert_usvg_path(path);
469-
let bounds = subpaths.iter().filter_map(|subpath| subpath.bounding_box()).reduce(Quad::combine_bounds).unwrap_or_default();
499+
import_usvg_path(modify_inputs, node, path, transform, layer, graphite_gradient_stops);
500+
}
501+
usvg::Node::Image(_image) => {
502+
warn!("Skip image");
503+
}
504+
usvg::Node::Text(text) => {
505+
let font = Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.to_string(), graphene_std::consts::DEFAULT_FONT_STYLE.to_string());
506+
modify_inputs.insert_text(text.chunks().iter().map(|chunk| chunk.text()).collect(), font, TypesettingConfig::default(), layer);
507+
modify_inputs.fill_set(Fill::Solid(Color::BLACK));
508+
}
509+
}
510+
}
470511

471-
modify_inputs.insert_vector(subpaths, layer, true, path.fill().is_some(), path.stroke().is_some());
512+
/// Recursively import a usvg node as a descendant of the root import layer.
513+
/// Uses lightweight wiring (no push/collision) and returns the subtree extent for position calculation.
514+
///
515+
/// The subtree extent represents the additional vertical grid units that this node's descendants
516+
/// occupy below the node's position. This is used to calculate correct y_offsets between siblings.
517+
fn import_usvg_node_inner(
518+
modify_inputs: &mut ModifyInputsContext,
519+
node: &usvg::Node,
520+
transform: DAffine2,
521+
id: NodeId,
522+
parent: LayerNodeIdentifier,
523+
insert_index: usize,
524+
graphite_gradient_stops: &HashMap<String, GradientStops>,
525+
group_extents_map: &mut HashMap<LayerNodeIdentifier, Vec<u32>>,
526+
) -> u32 {
527+
let layer = modify_inputs.create_layer(id);
528+
modify_inputs.network_interface.move_layer_to_stack_for_import(layer, parent, insert_index, &[]);
529+
modify_inputs.layer_node = Some(layer);
472530

473-
if let Some(transform_node_id) = modify_inputs.existing_network_node_id("Transform", true) {
474-
transform_utils::update_transform(modify_inputs.network_interface, &transform_node_id, transform * usvg_transform(node.abs_transform()));
531+
match node {
532+
usvg::Node::Group(group) => {
533+
let mut child_extents: Vec<u32> = Vec::new();
534+
for child in group.children() {
535+
let extent = import_usvg_node_inner(modify_inputs, child, transform, NodeId::new(), layer, 0, graphite_gradient_stops, group_extents_map);
536+
child_extents.push(extent);
475537
}
538+
modify_inputs.layer_node = Some(layer);
476539

477-
if let Some(fill) = path.fill() {
478-
let bounds_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
479-
apply_usvg_fill(fill, modify_inputs, bounds_transform, graphite_gradient_stops);
480-
}
481-
if let Some(stroke) = path.stroke() {
482-
apply_usvg_stroke(stroke, modify_inputs, transform * usvg_transform(node.abs_transform()));
483-
}
540+
let n = child_extents.len();
541+
let total_extent = if n == 0 {
542+
0
543+
} else {
544+
(2 * STACK_VERTICAL_GAP as u32) * n as u32 - STACK_VERTICAL_GAP as u32 + child_extents.iter().sum::<u32>()
545+
};
546+
group_extents_map.insert(layer, child_extents);
547+
total_extent
548+
}
549+
usvg::Node::Path(path) => {
550+
import_usvg_path(modify_inputs, node, path, transform, layer, graphite_gradient_stops);
551+
0
484552
}
485553
usvg::Node::Image(_image) => {
486-
warn!("Skip image")
554+
warn!("Skip image");
555+
0
487556
}
488557
usvg::Node::Text(text) => {
489558
let font = Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.to_string(), graphene_std::consts::DEFAULT_FONT_STYLE.to_string());
490559
modify_inputs.insert_text(text.chunks().iter().map(|chunk| chunk.text()).collect(), font, TypesettingConfig::default(), layer);
491560
modify_inputs.fill_set(Fill::Solid(Color::BLACK));
561+
0
562+
}
563+
}
564+
}
565+
566+
/// Helper to apply path data (vector geometry, fill, stroke, transform) to a layer.
567+
fn import_usvg_path(
568+
modify_inputs: &mut ModifyInputsContext,
569+
node: &usvg::Node,
570+
path: &usvg::Path,
571+
transform: DAffine2,
572+
layer: LayerNodeIdentifier,
573+
graphite_gradient_stops: &HashMap<String, GradientStops>,
574+
) {
575+
let subpaths = convert_usvg_path(path);
576+
let bounds = subpaths.iter().filter_map(|subpath| subpath.bounding_box()).reduce(Quad::combine_bounds).unwrap_or_default();
577+
578+
modify_inputs.insert_vector(subpaths, layer, true, path.fill().is_some(), path.stroke().is_some());
579+
580+
if let Some(transform_node_id) = modify_inputs.existing_network_node_id("Transform", true) {
581+
transform_utils::update_transform(modify_inputs.network_interface, &transform_node_id, transform * usvg_transform(node.abs_transform()));
582+
}
583+
584+
if let Some(fill) = path.fill() {
585+
let bounds_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
586+
apply_usvg_fill(fill, modify_inputs, bounds_transform, graphite_gradient_stops);
587+
}
588+
if let Some(stroke) = path.stroke() {
589+
apply_usvg_stroke(stroke, modify_inputs, transform * usvg_transform(node.abs_transform()));
590+
}
591+
}
592+
593+
/// Set correct positions for all imported layers in a single top-down O(n) pass.
594+
///
595+
/// For each group's child stack:
596+
/// - The top-of-stack child (last SVG child) gets an `Absolute` position at `(parent_x - LAYER_INDENT_OFFSET, parent_y + STACK_VERTICAL_GAP)`
597+
/// - 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.
598+
fn set_import_child_positions(
599+
network_interface: &mut NodeNetworkInterface,
600+
group: LayerNodeIdentifier,
601+
group_pos: IVec2,
602+
child_extents_svg_order: &[u32],
603+
group_extents_map: &HashMap<LayerNodeIdentifier, Vec<u32>>,
604+
) {
605+
use crate::messages::portfolio::document::utility_types::network_interface::LayerPosition;
606+
607+
let layer_children: Vec<_> = group.children(network_interface.document_metadata()).collect();
608+
let n = child_extents_svg_order.len();
609+
610+
if n == 0 || layer_children.is_empty() {
611+
return;
612+
}
613+
614+
// Children in the layer tree are in stack order (top to bottom), which is the REVERSE of SVG order.
615+
// SVG order: [s_0, s_1, ..., s_{n-1}] with extents [e_0, e_1, ..., e_{n-1}]
616+
// Stack order: [s_{n-1}, s_{n-2}, ..., s_0 ] (top to bottom)
617+
//
618+
// For stack child at index i:
619+
// - SVG index = n - 1 - i
620+
// - Previous stack sibling's SVG index = n - i
621+
// - y_offset = extent_of_previous_sibling + STACK_VERTICAL_GAP
622+
623+
let child_x = group_pos.x - LAYER_INDENT_OFFSET;
624+
let mut current_y = group_pos.y + STACK_VERTICAL_GAP;
625+
626+
for (i, child_layer) in layer_children.iter().enumerate() {
627+
let child_pos = IVec2::new(child_x, current_y);
628+
629+
if i == 0 {
630+
// Top of stack — set to `Absolute` position
631+
network_interface.set_layer_position_for_import(&child_layer.to_node(), LayerPosition::Absolute(child_pos), &[]);
632+
} else {
633+
// Below top — set `Stack` with `y_offset` based on previous sibling's subtree extent
634+
let prev_sibling_svg_index = n - i;
635+
let y_offset = child_extents_svg_order[prev_sibling_svg_index] + STACK_VERTICAL_GAP as u32;
636+
network_interface.set_layer_position_for_import(&child_layer.to_node(), LayerPosition::Stack(y_offset), &[]);
492637
}
638+
639+
// Recurse into group children to set their descendants' positions
640+
if let Some(grandchild_extents) = group_extents_map.get(child_layer) {
641+
set_import_child_positions(network_interface, *child_layer, child_pos, grandchild_extents, group_extents_map);
642+
}
643+
644+
// Advance `current_y` for the next child: node height (STACK_VERTICAL_GAP) + gap (STACK_VERTICAL_GAP) + subtree extent
645+
let child_svg_index = n - 1 - i;
646+
let child_extent = child_extents_svg_order[child_svg_index];
647+
current_y += 2 * STACK_VERTICAL_GAP + child_extent as i32;
493648
}
494649
}
495650

editor/src/messages/portfolio/document/graph_operation/utility_types.rs

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ pub struct ModifyInputsContext<'a> {
3333
pub responses: &'a mut VecDeque<Message>,
3434
// Cannot be LayerNodeIdentifier::ROOT_PARENT
3535
pub layer_node: Option<LayerNodeIdentifier>,
36+
/// When true, uses lightweight import paths that skip expensive checks during bulk import.
37+
pub import: bool,
3638
}
3739

3840
impl<'a> ModifyInputsContext<'a> {
@@ -42,6 +44,7 @@ impl<'a> ModifyInputsContext<'a> {
4244
network_interface,
4345
responses,
4446
layer_node: None,
47+
import: false,
4548
}
4649
}
4750

@@ -150,7 +153,7 @@ impl<'a> ModifyInputsContext<'a> {
150153

151154
let boolean_id = NodeId::new();
152155
self.network_interface.insert_node(boolean_id, boolean, &[]);
153-
self.network_interface.move_node_to_chain_start(&boolean_id, layer, &[]);
156+
self.network_interface.move_node_to_chain_start(&boolean_id, layer, &[], self.import);
154157
}
155158

156159
pub fn insert_vector(&mut self, subpaths: Vec<Subpath<PointId>>, layer: LayerNodeIdentifier, include_transform: bool, include_fill: bool, include_stroke: bool) {
@@ -161,13 +164,13 @@ impl<'a> ModifyInputsContext<'a> {
161164
.node_template_input_override([Some(NodeInput::value(TaggedValue::Vector(vector), false))]);
162165
let shape_id = NodeId::new();
163166
self.network_interface.insert_node(shape_id, shape, &[]);
164-
self.network_interface.move_node_to_chain_start(&shape_id, layer, &[]);
167+
self.network_interface.move_node_to_chain_start(&shape_id, layer, &[], self.import);
165168

166169
if include_transform {
167170
let transform = resolve_network_node_type("Transform").expect("Transform node does not exist").default_node_template();
168171
let transform_id = NodeId::new();
169172
self.network_interface.insert_node(transform_id, transform, &[]);
170-
self.network_interface.move_node_to_chain_start(&transform_id, layer, &[]);
173+
self.network_interface.move_node_to_chain_start(&transform_id, layer, &[], self.import);
171174
}
172175

173176
if include_stroke {
@@ -176,7 +179,7 @@ impl<'a> ModifyInputsContext<'a> {
176179
.default_node_template();
177180
let stroke_id = NodeId::new();
178181
self.network_interface.insert_node(stroke_id, stroke, &[]);
179-
self.network_interface.move_node_to_chain_start(&stroke_id, layer, &[]);
182+
self.network_interface.move_node_to_chain_start(&stroke_id, layer, &[], self.import);
180183
}
181184

182185
if include_fill {
@@ -185,7 +188,7 @@ impl<'a> ModifyInputsContext<'a> {
185188
.default_node_template();
186189
let fill_id = NodeId::new();
187190
self.network_interface.insert_node(fill_id, fill, &[]);
188-
self.network_interface.move_node_to_chain_start(&fill_id, layer, &[]);
191+
self.network_interface.move_node_to_chain_start(&fill_id, layer, &[], self.import);
189192
}
190193
}
191194

@@ -216,19 +219,19 @@ impl<'a> ModifyInputsContext<'a> {
216219

217220
let text_id = NodeId::new();
218221
self.network_interface.insert_node(text_id, text, &[]);
219-
self.network_interface.move_node_to_chain_start(&text_id, layer, &[]);
222+
self.network_interface.move_node_to_chain_start(&text_id, layer, &[], self.import);
220223

221224
let transform_id = NodeId::new();
222225
self.network_interface.insert_node(transform_id, transform, &[]);
223-
self.network_interface.move_node_to_chain_start(&transform_id, layer, &[]);
226+
self.network_interface.move_node_to_chain_start(&transform_id, layer, &[], self.import);
224227

225228
let stroke_id = NodeId::new();
226229
self.network_interface.insert_node(stroke_id, stroke, &[]);
227-
self.network_interface.move_node_to_chain_start(&stroke_id, layer, &[]);
230+
self.network_interface.move_node_to_chain_start(&stroke_id, layer, &[], self.import);
228231

229232
let fill_id = NodeId::new();
230233
self.network_interface.insert_node(fill_id, fill, &[]);
231-
self.network_interface.move_node_to_chain_start(&fill_id, layer, &[]);
234+
self.network_interface.move_node_to_chain_start(&fill_id, layer, &[], self.import);
232235
}
233236

234237
pub fn insert_image_data(&mut self, image_frame: Table<Raster<CPU>>, layer: LayerNodeIdentifier) {
@@ -239,11 +242,11 @@ impl<'a> ModifyInputsContext<'a> {
239242

240243
let image_id = NodeId::new();
241244
self.network_interface.insert_node(image_id, image, &[]);
242-
self.network_interface.move_node_to_chain_start(&image_id, layer, &[]);
245+
self.network_interface.move_node_to_chain_start(&image_id, layer, &[], self.import);
243246

244247
let transform_id = NodeId::new();
245248
self.network_interface.insert_node(transform_id, transform, &[]);
246-
self.network_interface.move_node_to_chain_start(&transform_id, layer, &[]);
249+
self.network_interface.move_node_to_chain_start(&transform_id, layer, &[], self.import);
247250
}
248251

249252
fn get_output_layer(&self) -> Option<LayerNodeIdentifier> {
@@ -328,12 +331,12 @@ impl<'a> ModifyInputsContext<'a> {
328331
};
329332
let node_id = NodeId::new();
330333
self.network_interface.insert_node(node_id, flatten_path_definition.default_node_template(), &[]);
331-
self.network_interface.move_node_to_chain_start(&node_id, output_layer, &[]);
334+
self.network_interface.move_node_to_chain_start(&node_id, output_layer, &[], self.import);
332335
}
333336
}
334337
let node_id = NodeId::new();
335338
self.network_interface.insert_node(node_id, node_definition.default_node_template(), &[]);
336-
self.network_interface.move_node_to_chain_start(&node_id, output_layer, &[]);
339+
self.network_interface.move_node_to_chain_start(&node_id, output_layer, &[], self.import);
337340
Some(node_id)
338341
}
339342

editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -717,7 +717,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
717717
network_interface.move_layer_to_stack(layer, parent, insert_index, selection_network_path);
718718
}
719719
NodeGraphMessage::MoveNodeToChainStart { node_id, parent } => {
720-
network_interface.move_node_to_chain_start(&node_id, parent, selection_network_path);
720+
network_interface.move_node_to_chain_start(&node_id, parent, selection_network_path, false);
721721
}
722722
NodeGraphMessage::SetChainPosition { node_id } => {
723723
network_interface.set_chain_position(&node_id, selection_network_path);

0 commit comments

Comments
 (0)