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
This commit is contained in:
Keavon Chambers 2026-03-22 23:33:58 -07:00 committed by GitHub
parent 9727e14fe9
commit a10092c10c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 363 additions and 60 deletions

View File

@ -4,6 +4,10 @@ pub const EXPORTS_TO_TOP_EDGE_PIXEL_GAP: u32 = 72;
pub const EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP: u32 = 120;
pub const IMPORTS_TO_TOP_EDGE_PIXEL_GAP: u32 = 72;
pub const IMPORTS_TO_LEFT_EDGE_PIXEL_GAP: u32 = 120;
/// Vertical grid distance between adjacent stack siblings, or between a parent layer and its first stack child.
pub const STACK_VERTICAL_GAP: i32 = 3;
/// Horizontal grid indentation of a child layer relative to its parent layer.
pub const LAYER_INDENT_OFFSET: i32 = 8;
// VIEWPORT
pub const VIEWPORT_ZOOM_WHEEL_RATE: f64 = (1. / 600.) * 3.;

View File

@ -1,5 +1,6 @@
use super::transform_utils;
use super::utility_types::ModifyInputsContext;
use crate::consts::{LAYER_INDENT_OFFSET, STACK_VERTICAL_GAP};
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
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> {
Some(Color::from_rgbaf32_unchecked(r, g, b, opacity))
}
/// 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.
fn import_usvg_node(
modify_inputs: &mut ModifyInputsContext,
node: &usvg::Node,
@ -452,19 +459,119 @@ fn import_usvg_node(
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, 3), &[]);
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() {
import_usvg_node(modify_inputs, child, transform, NodeId::new(), layer, 0, graphite_gradient_stops);
let extent = import_usvg_node_inner(modify_inputs, child, transform, 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, transform, 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.
fn import_usvg_node_inner(
modify_inputs: &mut ModifyInputsContext,
node: &usvg::Node,
transform: DAffine2,
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, transform, 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, transform, 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
}
}
}
/// Helper to apply path data (vector geometry, fill, stroke, transform) to a layer.
fn import_usvg_path(
modify_inputs: &mut ModifyInputsContext,
node: &usvg::Node,
path: &usvg::Path,
transform: DAffine2,
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();
@ -481,15 +588,63 @@ fn import_usvg_node(
if let Some(stroke) = path.stroke() {
apply_usvg_stroke(stroke, modify_inputs, transform * usvg_transform(node.abs_transform()));
}
}
/// 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.
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;
}
usvg::Node::Image(_image) => {
warn!("Skip image")
// 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), &[]);
}
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));
// 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;
}
}

View File

@ -33,6 +33,8 @@ pub struct ModifyInputsContext<'a> {
pub responses: &'a mut VecDeque<Message>,
// Cannot be LayerNodeIdentifier::ROOT_PARENT
pub layer_node: Option<LayerNodeIdentifier>,
/// When true, uses lightweight import paths that skip expensive checks during bulk import.
pub import: bool,
}
impl<'a> ModifyInputsContext<'a> {
@ -42,6 +44,7 @@ impl<'a> ModifyInputsContext<'a> {
network_interface,
responses,
layer_node: None,
import: false,
}
}
@ -150,7 +153,7 @@ impl<'a> ModifyInputsContext<'a> {
let boolean_id = NodeId::new();
self.network_interface.insert_node(boolean_id, boolean, &[]);
self.network_interface.move_node_to_chain_start(&boolean_id, layer, &[]);
self.network_interface.move_node_to_chain_start(&boolean_id, layer, &[], self.import);
}
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> {
.node_template_input_override([Some(NodeInput::value(TaggedValue::Vector(vector), false))]);
let shape_id = NodeId::new();
self.network_interface.insert_node(shape_id, shape, &[]);
self.network_interface.move_node_to_chain_start(&shape_id, layer, &[]);
self.network_interface.move_node_to_chain_start(&shape_id, layer, &[], self.import);
if include_transform {
let transform = resolve_network_node_type("Transform").expect("Transform node does not exist").default_node_template();
let transform_id = NodeId::new();
self.network_interface.insert_node(transform_id, transform, &[]);
self.network_interface.move_node_to_chain_start(&transform_id, layer, &[]);
self.network_interface.move_node_to_chain_start(&transform_id, layer, &[], self.import);
}
if include_stroke {
@ -176,7 +179,7 @@ impl<'a> ModifyInputsContext<'a> {
.default_node_template();
let stroke_id = NodeId::new();
self.network_interface.insert_node(stroke_id, stroke, &[]);
self.network_interface.move_node_to_chain_start(&stroke_id, layer, &[]);
self.network_interface.move_node_to_chain_start(&stroke_id, layer, &[], self.import);
}
if include_fill {
@ -185,7 +188,7 @@ impl<'a> ModifyInputsContext<'a> {
.default_node_template();
let fill_id = NodeId::new();
self.network_interface.insert_node(fill_id, fill, &[]);
self.network_interface.move_node_to_chain_start(&fill_id, layer, &[]);
self.network_interface.move_node_to_chain_start(&fill_id, layer, &[], self.import);
}
}
@ -216,19 +219,19 @@ impl<'a> ModifyInputsContext<'a> {
let text_id = NodeId::new();
self.network_interface.insert_node(text_id, text, &[]);
self.network_interface.move_node_to_chain_start(&text_id, layer, &[]);
self.network_interface.move_node_to_chain_start(&text_id, layer, &[], self.import);
let transform_id = NodeId::new();
self.network_interface.insert_node(transform_id, transform, &[]);
self.network_interface.move_node_to_chain_start(&transform_id, layer, &[]);
self.network_interface.move_node_to_chain_start(&transform_id, layer, &[], self.import);
let stroke_id = NodeId::new();
self.network_interface.insert_node(stroke_id, stroke, &[]);
self.network_interface.move_node_to_chain_start(&stroke_id, layer, &[]);
self.network_interface.move_node_to_chain_start(&stroke_id, layer, &[], self.import);
let fill_id = NodeId::new();
self.network_interface.insert_node(fill_id, fill, &[]);
self.network_interface.move_node_to_chain_start(&fill_id, layer, &[]);
self.network_interface.move_node_to_chain_start(&fill_id, layer, &[], self.import);
}
pub fn insert_image_data(&mut self, image_frame: Table<Raster<CPU>>, layer: LayerNodeIdentifier) {
@ -239,11 +242,11 @@ impl<'a> ModifyInputsContext<'a> {
let image_id = NodeId::new();
self.network_interface.insert_node(image_id, image, &[]);
self.network_interface.move_node_to_chain_start(&image_id, layer, &[]);
self.network_interface.move_node_to_chain_start(&image_id, layer, &[], self.import);
let transform_id = NodeId::new();
self.network_interface.insert_node(transform_id, transform, &[]);
self.network_interface.move_node_to_chain_start(&transform_id, layer, &[]);
self.network_interface.move_node_to_chain_start(&transform_id, layer, &[], self.import);
}
fn get_output_layer(&self) -> Option<LayerNodeIdentifier> {
@ -328,12 +331,12 @@ impl<'a> ModifyInputsContext<'a> {
};
let node_id = NodeId::new();
self.network_interface.insert_node(node_id, flatten_path_definition.default_node_template(), &[]);
self.network_interface.move_node_to_chain_start(&node_id, output_layer, &[]);
self.network_interface.move_node_to_chain_start(&node_id, output_layer, &[], self.import);
}
}
let node_id = NodeId::new();
self.network_interface.insert_node(node_id, node_definition.default_node_template(), &[]);
self.network_interface.move_node_to_chain_start(&node_id, output_layer, &[]);
self.network_interface.move_node_to_chain_start(&node_id, output_layer, &[], self.import);
Some(node_id)
}

View File

@ -717,7 +717,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
network_interface.move_layer_to_stack(layer, parent, insert_index, selection_network_path);
}
NodeGraphMessage::MoveNodeToChainStart { node_id, parent } => {
network_interface.move_node_to_chain_start(&node_id, parent, selection_network_path);
network_interface.move_node_to_chain_start(&node_id, parent, selection_network_path, false);
}
NodeGraphMessage::SetChainPosition { node_id } => {
network_interface.set_chain_position(&node_id, selection_network_path);

View File

@ -5,7 +5,9 @@ mod resolved_types;
use super::document_metadata::{DocumentMetadata, LayerNodeIdentifier, NodeRelations};
use super::misc::PTZ;
use super::nodes::SelectedNodes;
use crate::consts::{EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP, EXPORTS_TO_TOP_EDGE_PIXEL_GAP, GRID_SIZE, IMPORTS_TO_LEFT_EDGE_PIXEL_GAP, IMPORTS_TO_TOP_EDGE_PIXEL_GAP};
use crate::consts::{
EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP, EXPORTS_TO_TOP_EDGE_PIXEL_GAP, GRID_SIZE, IMPORTS_TO_LEFT_EDGE_PIXEL_GAP, IMPORTS_TO_TOP_EDGE_PIXEL_GAP, LAYER_INDENT_OFFSET, STACK_VERTICAL_GAP,
};
use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext;
use crate::messages::portfolio::document::node_graph::document_node_definitions::{DefinitionIdentifier, resolve_document_node_type};
use crate::messages::portfolio::document::node_graph::utility_types::{Direction, FrontendClickTargets, FrontendGraphDataType, FrontendGraphInput, FrontendGraphOutput};
@ -2065,6 +2067,30 @@ impl NodeNetworkInterface {
network_metadata.transient_metadata.outward_wires.unload();
}
/// Incrementally updates the outward_wires cache when a single input connector changes,
/// avoiding a full rebuild. If the cache is not loaded, this is a no-op (it will be fully
/// rebuilt on the next read via `outward_wires()`).
fn update_outward_wires(&mut self, network_path: &[NodeId], input_connector: &InputConnector, old_input: &NodeInput, new_input: &NodeInput) {
let Some(network_metadata) = self.network_metadata_mut(network_path) else {
return;
};
let TransientMetadata::Loaded(outward_wires) = &mut network_metadata.transient_metadata.outward_wires else {
return;
};
// Remove the input_connector from the old output's downstream list
if let Some(old_output) = OutputConnector::from_input(old_input)
&& let Some(connections) = outward_wires.get_mut(&old_output)
{
connections.retain(|c| c != input_connector);
}
// Add the input_connector to the new output's downstream list
if let Some(new_output) = OutputConnector::from_input(new_input) {
outward_wires.entry(new_output).or_default().push(*input_connector);
}
}
pub fn layer_width(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> Option<u32> {
let Some(node_metadata) = self.node_metadata(node_id, network_path) else {
log::error!("Could not get nested node_metadata in layer_width");
@ -3831,6 +3857,46 @@ impl NodeNetworkInterface {
node.context_features = context_features;
}
/// Lightweight version of `set_input` for bulk import operations.
/// Directly sets the input without `is_acyclic` checks, `load_structure`, position conversions,
/// or per-node cache invalidation. Call `load_structure`, `unload_all_nodes_click_targets`, and
/// `unload_all_nodes_bounding_box` once after all import wiring is complete.
pub fn set_input_for_import(&mut self, input_connector: &InputConnector, new_input: NodeInput, network_path: &[NodeId]) {
if matches!(input_connector, InputConnector::Export(_)) && matches!(new_input, NodeInput::Import { .. }) {
log::error!("Cannot connect a network to an export, see https://github.com/GraphiteEditor/Graphite/issues/1762");
return;
}
let Some(network) = self.network_mut(network_path) else {
log::error!("Could not get nested network in set_input_for_import");
return;
};
let old_input = match input_connector {
InputConnector::Node { node_id, input_index } => {
let Some(node) = network.nodes.get_mut(node_id) else {
log::error!("Could not get node in set_input_for_import");
return;
};
let Some(input) = node.inputs.get_mut(*input_index) else {
log::error!("Could not get input in set_input_for_import");
return;
};
std::mem::replace(input, new_input.clone())
}
InputConnector::Export(export_index) => {
let Some(export) = network.exports.get_mut(*export_index) else {
log::error!("Could not get export in set_input_for_import");
return;
};
std::mem::replace(export, new_input.clone())
}
};
self.transaction_modified();
self.update_outward_wires(network_path, input_connector, &old_input, &new_input);
}
pub fn set_input(&mut self, input_connector: &InputConnector, new_input: NodeInput, network_path: &[NodeId]) {
if matches!(input_connector, InputConnector::Export(_)) && matches!(new_input, NodeInput::Import { .. }) {
// TODO: Add support for flattening NodeInput::Import exports in flatten_with_fns https://github.com/GraphiteEditor/Graphite/issues/1762
@ -3892,8 +3958,8 @@ impl NodeNetworkInterface {
return;
};
// Ensure the network is not cyclic
if !network.is_acyclic() {
// Ensure the network is not cyclic (only Node connections can create cycles)
if matches!(new_input, NodeInput::Node { .. }) && !network.is_acyclic() {
self.set_input(input_connector, old_input, network_path);
return;
}
@ -3950,7 +4016,7 @@ impl NodeNetworkInterface {
}
}
}
self.unload_outward_wires(network_path);
self.update_outward_wires(network_path, input_connector, &old_input, &new_input);
// Layout system
let Some(current_node_position) = self.position(upstream_node_id, network_path) else {
log::error!("Could not get current node position in set_input for node {upstream_node_id}");
@ -4005,17 +4071,17 @@ impl NodeNetworkInterface {
}
// If a connection is made to the imports
(NodeInput::Value { .. } | NodeInput::Scope { .. } | NodeInput::Inline { .. }, NodeInput::Import { .. }) => {
self.unload_outward_wires(network_path);
self.update_outward_wires(network_path, input_connector, &old_input, &new_input);
self.unload_wire(input_connector, network_path);
}
// If a connection to the imports is disconnected
(NodeInput::Import { .. }, NodeInput::Value { .. } | NodeInput::Scope { .. } | NodeInput::Inline { .. }) => {
self.unload_outward_wires(network_path);
self.update_outward_wires(network_path, input_connector, &old_input, &new_input);
self.unload_wire(input_connector, network_path);
}
// If a node is disconnected.
(NodeInput::Node { .. }, NodeInput::Value { .. } | NodeInput::Scope { .. } | NodeInput::Inline { .. }) => {
self.unload_outward_wires(network_path);
self.update_outward_wires(network_path, input_connector, &old_input, &new_input);
self.unload_wire(input_connector, network_path);
if let Some((old_upstream_node_id, previous_position)) = previous_metadata {
@ -4616,7 +4682,7 @@ impl NodeNetworkInterface {
// Set the position to stack if necessary
if let Some(downstream_position) = is_layer.then_some(single_downstream_layer_position).flatten() {
node_metadata.persistent_metadata.node_type_metadata = NodeTypePersistentMetadata::Layer(LayerPersistentMetadata {
position: LayerPosition::Stack((position.y - downstream_position.y - 3).max(0) as u32),
position: LayerPosition::Stack((position.y - downstream_position.y - STACK_VERTICAL_GAP).max(0) as u32),
owned_nodes: TransientMetadata::Unloaded,
})
}
@ -4777,7 +4843,7 @@ impl NodeNetworkInterface {
return;
};
self.set_stack_position(node_id, (node_position.y - downstream_position.y - 3).max(0) as u32, network_path);
self.set_stack_position(node_id, (node_position.y - downstream_position.y - STACK_VERTICAL_GAP).max(0) as u32, network_path);
}
/// Sets the position of a node to a chain position
@ -5417,6 +5483,61 @@ impl NodeNetworkInterface {
self.unload_all_nodes_bounding_box(network_path);
}
/// Lightweight version of `move_layer_to_stack` for SVG import. Performs only the wiring
/// (connecting the layer into the stack) without any position calculation or push/collision logic.
/// Positions should be set separately after the full import tree is built.
pub fn move_layer_to_stack_for_import(&mut self, layer: LayerNodeIdentifier, mut parent: LayerNodeIdentifier, mut insert_index: usize, network_path: &[NodeId]) {
// Artboard redirection: if a non-artboard layer targets ROOT_PARENT and an artboard exists, redirect into the artboard
if let Some(first_layer) = LayerNodeIdentifier::ROOT_PARENT.children(&self.document_metadata).next()
&& parent == LayerNodeIdentifier::ROOT_PARENT
&& self
.reference(&layer.to_node(), network_path)
.is_none_or(|reference| reference != DefinitionIdentifier::Network("Artboard".into()))
&& self.is_artboard(&first_layer.to_node(), network_path)
{
parent = first_layer;
insert_index = 0;
}
let post_node = ModifyInputsContext::get_post_node_with_index(self, parent, insert_index);
let Some(post_node_input) = self.input_from_connector(&post_node, network_path).cloned() else {
log::error!("Could not get previous input in move_layer_to_stack_for_import");
return;
};
let layer_output = NodeInput::node(layer.to_node(), 0);
match post_node_input {
NodeInput::Value { .. } | NodeInput::Scope(_) | NodeInput::Inline(_) | NodeInput::Reflection(_) => {
// First child in the stack — wire layer output to the post_node input
self.set_input_for_import(&post_node, layer_output, network_path);
}
NodeInput::Node { .. } => {
// Subsequent child — insert layer between post_node and its current upstream:
// 1. Disconnect old upstream from post_node, wire layer output to post_node
self.set_input_for_import(&post_node, layer_output, network_path);
// 2. Wire old upstream into layer's primary (stack) input
self.set_input_for_import(&InputConnector::node(layer.to_node(), 0), post_node_input, network_path);
}
NodeInput::Import { .. } => {
log::error!("Cannot insert import layer into a parent that connects to the imports");
}
}
}
/// Sets a layer's position directly without triggering per-node cache invalidation.
/// Used for bulk import operations where caches are invalidated once at the end.
pub fn set_layer_position_for_import(&mut self, node_id: &NodeId, position: LayerPosition, network_path: &[NodeId]) {
let Some(node_metadata) = self.node_metadata_mut(node_id, network_path) else {
log::error!("Could not get node_metadata for node {node_id} in set_layer_position_for_import");
return;
};
if let NodeTypePersistentMetadata::Layer(layer_metadata) = &mut node_metadata.persistent_metadata.node_type_metadata {
layer_metadata.position = position;
self.transaction_modified();
}
}
/// Disconnect the layers primary output and the input to the last non layer node feeding into it through primary flow, reconnects, then moves the layer to the new layer and stack index
pub fn move_layer_to_stack(&mut self, layer: LayerNodeIdentifier, mut parent: LayerNodeIdentifier, mut insert_index: usize, network_path: &[NodeId]) {
// Prevent moving an artboard anywhere but to the ROOT_PARENT child stack
@ -5482,7 +5603,7 @@ impl NodeNetworkInterface {
let after_move_post_layer_position = if let Some(post_node_id) = post_node.node_id() {
self.position(&post_node_id, network_path)
} else {
Some(IVec2::new(8, -3))
Some(IVec2::new(LAYER_INDENT_OFFSET, -STACK_VERTICAL_GAP))
};
let Some(after_move_post_layer_position) = after_move_post_layer_position else {
@ -5499,15 +5620,15 @@ impl NodeNetworkInterface {
log::error!("Could not get downstream node position in move_layer_to_stack");
return;
};
let mut lowest_y_position = downstream_node_position.y + 3;
let mut lowest_y_position = downstream_node_position.y + STACK_VERTICAL_GAP;
for bottom_position in self.upstream_nodes_below_layer(&downstream_node, network_path).iter().filter_map(|node_id| {
let is_layer = self.is_layer(node_id, network_path);
self.position(node_id, network_path).map(|position| position.y + if is_layer { 3 } else { 2 })
self.position(node_id, network_path).map(|position| position.y + if is_layer { STACK_VERTICAL_GAP } else { 2 })
}) {
lowest_y_position = lowest_y_position.max(bottom_position);
}
downstream_height = lowest_y_position - (downstream_node_position.y + 3);
downstream_height = lowest_y_position - (downstream_node_position.y + STACK_VERTICAL_GAP);
}
let mut highest_y_position = layer_to_move_position.y;
@ -5515,7 +5636,7 @@ impl NodeNetworkInterface {
for (bottom_position, top_position) in self.upstream_nodes_below_layer(&layer.to_node(), network_path).iter().filter_map(|node_id| {
let is_layer = self.is_layer(node_id, network_path);
let bottom_position = self.position(node_id, network_path).map(|position| position.y + if is_layer { 3 } else { 2 });
let bottom_position = self.position(node_id, network_path).map(|position| position.y + if is_layer { STACK_VERTICAL_GAP } else { 2 });
let top_position = self.position(node_id, network_path).map(|position| if is_layer { position.y - 1 } else { position.y });
bottom_position.zip(top_position)
}) {
@ -5523,7 +5644,7 @@ impl NodeNetworkInterface {
lowest_y_position = lowest_y_position.max(bottom_position);
}
let height_above_layer = layer_to_move_position.y - highest_y_position + downstream_height;
let height_below_layer = lowest_y_position - layer_to_move_position.y - 3;
let height_below_layer = lowest_y_position - layer_to_move_position.y - STACK_VERTICAL_GAP;
// If there is an upstream node in the new location for the layer, create space for the moved layer by shifting the upstream node down
if let Some(upstream_node_id) = post_node_input.as_node() {
@ -5535,7 +5656,7 @@ impl NodeNetworkInterface {
let old_selected_nodes = selected_nodes.replace_with(vec![upstream_node_id]);
// Create the minimum amount space for the moved layer
for _ in 0..3 {
for _ in 0..STACK_VERTICAL_GAP {
self.vertical_shift_with_push(&upstream_node_id, 1, &mut HashSet::new(), network_path);
}
@ -5564,7 +5685,7 @@ impl NodeNetworkInterface {
NodeInput::Value { .. } | NodeInput::Scope(_) | NodeInput::Inline(_) | NodeInput::Reflection(_) => {
self.create_wire(&OutputConnector::node(layer.to_node(), 0), &post_node, network_path);
let final_layer_position = after_move_post_layer_position + IVec2::new(-8, 3);
let final_layer_position = after_move_post_layer_position + IVec2::new(-LAYER_INDENT_OFFSET, STACK_VERTICAL_GAP);
let shift = final_layer_position - previous_layer_position;
self.shift_absolute_node_position(&layer.to_node(), shift, network_path);
}
@ -5575,7 +5696,7 @@ impl NodeNetworkInterface {
return;
};
let final_layer_position = IVec2::new(stack_top_position.x, after_move_post_layer_position.y + 3 + height_above_layer);
let final_layer_position = IVec2::new(stack_top_position.x, after_move_post_layer_position.y + STACK_VERTICAL_GAP + height_above_layer);
let shift = final_layer_position - previous_layer_position;
self.shift_absolute_node_position(&layer.to_node(), shift, network_path);
insert_node_after_post = true;
@ -5588,13 +5709,13 @@ impl NodeNetworkInterface {
match post_node_input {
// Move to the bottom of the stack
NodeInput::Value { .. } | NodeInput::Scope(_) | NodeInput::Inline(_) | NodeInput::Reflection(_) => {
let offset = after_move_post_layer_position - previous_layer_position + IVec2::new(0, 3 + height_above_layer);
let offset = after_move_post_layer_position - previous_layer_position + IVec2::new(0, STACK_VERTICAL_GAP + height_above_layer);
self.shift_absolute_node_position(&layer.to_node(), offset, network_path);
self.create_wire(&OutputConnector::node(layer.to_node(), 0), &post_node, network_path);
}
// Insert into the stack
NodeInput::Node { .. } => {
let final_layer_position = after_move_post_layer_position + IVec2::new(0, 3 + height_above_layer);
let final_layer_position = after_move_post_layer_position + IVec2::new(0, STACK_VERTICAL_GAP + height_above_layer);
let shift = final_layer_position - previous_layer_position;
self.shift_absolute_node_position(&layer.to_node(), shift, network_path);
insert_node_after_post = true;
@ -5653,18 +5774,38 @@ impl NodeNetworkInterface {
self.create_wire(&upstream_output, &InputConnector::node(*node_id, insert_node_input_index), network_path);
}
// Moves a node and to the start of a layer chain (feeding into the secondary input of the layer)
pub fn move_node_to_chain_start(&mut self, node_id: &NodeId, parent: LayerNodeIdentifier, network_path: &[NodeId]) {
let Some(current_input) = self.input_from_connector(&InputConnector::node(parent.to_node(), 1), network_path) else {
/// Moves a node to the start of a layer chain (feeding into the secondary input of the layer).
/// When `import` is true, uses lightweight wiring that skips `is_acyclic` checks and per-node cache invalidation.
pub fn move_node_to_chain_start(&mut self, node_id: &NodeId, parent: LayerNodeIdentifier, network_path: &[NodeId], import: bool) {
let parent_input = InputConnector::node(parent.to_node(), 1);
let Some(current_input) = self.input_from_connector(&parent_input, network_path).cloned() else {
log::error!("Could not get input for node {node_id}");
return;
};
// Chain is empty: wire the node as the first (and only) entry in the chain
if matches!(current_input, NodeInput::Value { .. }) {
self.create_wire(&OutputConnector::node(*node_id, 0), &InputConnector::node(parent.to_node(), 1), network_path);
self.set_chain_position(node_id, network_path);
// Wire: [parent] -> [new node]
if import {
self.set_input_for_import(&parent_input, NodeInput::node(*node_id, 0), network_path);
} else {
// Insert the node in the gap and set the upstream to a chain
self.insert_node_between(node_id, &InputConnector::node(parent.to_node(), 1), 0, network_path);
self.create_wire(&OutputConnector::node(*node_id, 0), &parent_input, network_path);
}
// Mark this lone node as chain-positioned
self.set_chain_position(node_id, network_path);
}
// Chain already has nodes: splice this node between the parent and the chain's existing final downstream node
else {
// Wire: [parent] -> [new node] -> [existing node]
if import {
self.set_input_for_import(&parent_input, NodeInput::node(*node_id, 0), network_path);
self.set_input_for_import(&InputConnector::node(*node_id, 0), current_input, network_path);
} else {
self.insert_node_between(node_id, &parent_input, 0, network_path);
}
// Ensure all upstream nodes from here are marked as chain-positioned
self.force_set_upstream_to_chain(node_id, network_path);
}
}