From 77994266346180c0440a7aba1d9475c53ec8ba93 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 23 Mar 2026 00:39:07 -0700 Subject: [PATCH 1/9] Fix half-pixel offset on imported images --- .../document/document_message_handler.rs | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 8e8d5808e1..9fa96c0860 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -664,11 +664,11 @@ impl MessageHandler> for DocumentMes let image_size = DVec2::new(image.width as f64, image.height as f64); // Align the layer with the mouse or center of viewport - let viewport_location = mouse.map_or(viewport.center_in_viewport_space().into_dvec2() + viewport.offset().into_dvec2(), |pos| pos.into()); - - let document_to_viewport = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz); - let center_in_viewport = DAffine2::from_translation(document_to_viewport.inverse().transform_point2(viewport_location - viewport.offset().into_dvec2())); - let center_in_viewport_layerspace = center_in_viewport; + let center_in_viewport_layerspace = mouse.map_or(DAffine2::IDENTITY, |viewport_location| { + let viewport_location: DVec2 = viewport_location.into(); + let document_to_viewport = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz); + DAffine2::from_translation(document_to_viewport.inverse().transform_point2(viewport_location - viewport.offset().into_dvec2())) + }); // Make layer the size of the image let fit_image_size = DAffine2::from_scale_angle_translation(image_size, 0., image_size / -2.); @@ -716,9 +716,13 @@ impl MessageHandler> for DocumentMes mouse, parent_and_insert_index, } => { - let document_to_viewport = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz); - let viewport_location = mouse.map_or(viewport.center_in_viewport_space().into_dvec2() + viewport.offset().into_dvec2(), |pos| pos.into()); - let center_in_viewport = DAffine2::from_translation(document_to_viewport.inverse().transform_point2(viewport_location - viewport.offset().into_dvec2())); + // When mouse is None (file-open flow), use the document origin directly to avoid a ±0.5px offset + // that `calculate_offset_transform`'s anti-aliasing rounding introduces for odd-sized viewports. + let center_in_viewport = mouse.map_or(DAffine2::IDENTITY, |pos| { + let viewport_location: DVec2 = pos.into(); + let document_to_viewport = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz); + DAffine2::from_translation(document_to_viewport.inverse().transform_point2(viewport_location - viewport.offset().into_dvec2())) + }); let layer_node_id = NodeId::new(); let layer_id = LayerNodeIdentifier::new_unchecked(layer_node_id); From 2eefd724ee3d21eb5f4ee801452830a3d61947f1 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 23 Mar 2026 02:51:56 -0700 Subject: [PATCH 2/9] Break out reused function --- .../document/document_message_handler.rs | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 9fa96c0860..4ae41c6f64 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -664,11 +664,7 @@ impl MessageHandler> for DocumentMes let image_size = DVec2::new(image.width as f64, image.height as f64); // Align the layer with the mouse or center of viewport - let center_in_viewport_layerspace = mouse.map_or(DAffine2::IDENTITY, |viewport_location| { - let viewport_location: DVec2 = viewport_location.into(); - let document_to_viewport = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz); - DAffine2::from_translation(document_to_viewport.inverse().transform_point2(viewport_location - viewport.offset().into_dvec2())) - }); + let center_in_viewport_layerspace = self.document_transform_from_mouse(mouse, viewport); // Make layer the size of the image let fit_image_size = DAffine2::from_scale_angle_translation(image_size, 0., image_size / -2.); @@ -718,11 +714,7 @@ impl MessageHandler> for DocumentMes } => { // When mouse is None (file-open flow), use the document origin directly to avoid a ±0.5px offset // that `calculate_offset_transform`'s anti-aliasing rounding introduces for odd-sized viewports. - let center_in_viewport = mouse.map_or(DAffine2::IDENTITY, |pos| { - let viewport_location: DVec2 = pos.into(); - let document_to_viewport = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz); - DAffine2::from_translation(document_to_viewport.inverse().transform_point2(viewport_location - viewport.offset().into_dvec2())) - }); + let center_in_viewport = self.document_transform_from_mouse(mouse, viewport); let layer_node_id = NodeId::new(); let layer_id = LayerNodeIdentifier::new_unchecked(layer_node_id); @@ -1478,6 +1470,15 @@ impl MessageHandler> for DocumentMes } impl DocumentMessageHandler { + /// Translates a viewport mouse position to a document-space transform, or returns identity if no mouse position is given. + fn document_transform_from_mouse(&self, mouse: Option<(f64, f64)>, viewport: &ViewportMessageHandler) -> DAffine2 { + mouse.map_or(DAffine2::IDENTITY, |pos| { + let viewport_location: DVec2 = pos.into(); + let document_to_viewport = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz); + DAffine2::from_translation(document_to_viewport.inverse().transform_point2(viewport_location - viewport.offset().into_dvec2())) + }) + } + /// Runs an intersection test with all layers and a viewport space quad pub fn intersect_quad<'a>(&'a self, viewport_quad: graphene_std::renderer::Quad, viewport: &ViewportMessageHandler) -> impl Iterator + use<'a> { let document_to_viewport = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz); From 66bbdf1e7f378f0173045bb6d1fe00c1ad5f61ea Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 23 Mar 2026 03:13:26 -0700 Subject: [PATCH 3/9] Fix SVG/image open flow placing content with unnecessary Transform nodes --- .../portfolio/document/document_message.rs | 6 +++ .../document/document_message_handler.rs | 51 +++++++++++-------- .../graph_operation_message.rs | 2 + .../graph_operation_message_handler.rs | 11 ++-- .../portfolio/portfolio_message_handler.rs | 4 ++ .../graph_modification_utils.rs | 3 +- 6 files changed, 52 insertions(+), 25 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index 7fce9daa58..f0d5ab0a81 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -105,12 +105,18 @@ pub enum DocumentMessage { image: Image, mouse: Option<(f64, f64)>, parent_and_insert_index: Option<(LayerNodeIdentifier, usize)>, + /// When true (file-open flow), place the image at the document origin so `WrapContentInArtboard` + /// can wrap it without a content Transform node. When false, place at the cursor or viewport center. + place_at_origin: bool, }, PasteSvg { name: Option, svg: String, mouse: Option<(f64, f64)>, parent_and_insert_index: Option<(LayerNodeIdentifier, usize)>, + /// When true (file-open flow), place the SVG at the document origin so `WrapContentInArtboard` + /// can wrap it without a content Transform node. When false, place at the cursor or viewport center. + place_at_origin: bool, }, Redo, RenameDocument { diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 4ae41c6f64..d100936968 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -4,7 +4,7 @@ use super::utility_types::misc::{GroupFolderType, SNAP_FUNCTIONS_FOR_BOUNDING_BO use super::utility_types::network_interface::{self, NodeNetworkInterface, TransactionStatus}; use super::utility_types::nodes::{CollapsedLayers, LayerStructureEntry, SelectedNodes}; use crate::application::{GRAPHITE_GIT_COMMIT_HASH, generate_uuid}; -use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_EXTENSION, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL}; +use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_EXTENSION, LAYER_INDENT_OFFSET, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL}; use crate::messages::input_mapper::utility_types::macros::action_shortcut; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::data_panel::{DataPanelMessageContext, DataPanelMessageHandler}; @@ -658,18 +658,19 @@ impl MessageHandler> for DocumentMes image, mouse, parent_and_insert_index, + place_at_origin, } => { // All the image's pixels have been converted to 0..=1, linear, and premultiplied by `Color::from_rgba8_srgb` let image_size = DVec2::new(image.width as f64, image.height as f64); - // Align the layer with the mouse or center of viewport - let center_in_viewport_layerspace = self.document_transform_from_mouse(mouse, viewport); - - // Make layer the size of the image - let fit_image_size = DAffine2::from_scale_angle_translation(image_size, 0., image_size / -2.); - - let transform = center_in_viewport_layerspace * fit_image_size; + let transform = if place_at_origin { + // File-open flow: place at document origin without centering so `WrapContentInArtboard` can wrap it + DAffine2::from_scale(image_size) + } else { + // Clipboard paste or drag-drop: center at cursor or viewport center + self.document_transform_from_mouse(mouse, viewport) * DAffine2::from_scale_angle_translation(image_size, 0., image_size / -2.) + }; let layer_node_id = NodeId::new(); let layer_id = LayerNodeIdentifier::new_unchecked(layer_node_id); @@ -711,17 +712,22 @@ impl MessageHandler> for DocumentMes svg, mouse, parent_and_insert_index, + place_at_origin, } => { - // When mouse is None (file-open flow), use the document origin directly to avoid a ±0.5px offset - // that `calculate_offset_transform`'s anti-aliasing rounding introduces for odd-sized viewports. - let center_in_viewport = self.document_transform_from_mouse(mouse, viewport); + let transform = if place_at_origin { + // File-open flow: place at document origin so `WrapContentInArtboard` can wrap it without extra Transform nodes + DAffine2::IDENTITY + } else { + // Clipboard paste or drag-drop: center at cursor or viewport center + self.document_transform_from_mouse(mouse, viewport) + }; let layer_node_id = NodeId::new(); let layer_id = LayerNodeIdentifier::new_unchecked(layer_node_id); responses.add(DocumentMessage::AddTransaction); - let layer = graph_modification_utils::new_svg_layer(svg, center_in_viewport, layer_node_id, self.new_layer_parent(true), responses); + let layer = graph_modification_utils::new_svg_layer(svg, transform, !place_at_origin, layer_node_id, self.new_layer_parent(true), responses); if let Some(name) = name { responses.add(NodeGraphMessage::SetDisplayName { @@ -1359,7 +1365,12 @@ impl MessageHandler> for DocumentMes node_id, node_template: Box::new(new_artboard_node), }); - responses.add(NodeGraphMessage::ShiftNodePosition { node_id, x: 15, y: -3 }); + // Compute the shift needed to align the content's top-left corner to the artboard's origin. + // When content is already at the document origin this is zero → no Transform node is created. + let content_shift = -bounds[0].round(); + let needs_content_transform = !content_shift.abs_diff_eq(DVec2::ZERO, 1e-6); + // With a content Transform node: use x: 15 (8 indent + 7 for the node width). Without: use x: LAYER_INDENT_OFFSET. + responses.add(NodeGraphMessage::ShiftNodePosition { node_id, x: if needs_content_transform { 15 } else { LAYER_INDENT_OFFSET }, y: -3 }); responses.add(GraphOperationMessage::ResizeArtboard { layer: LayerNodeIdentifier::new_unchecked(node_id), location: if place_artboard_at_origin { IVec2::ZERO } else { bounds[0].round().as_ivec2() }, @@ -1373,10 +1384,10 @@ impl MessageHandler> for DocumentMes insert_node_input_index: 1, }); - // Shift the content by half its width and height so it gets centered in the artboard + // Shift the content to align its top-left to the artboard's origin (no-op when content is already at origin) responses.add(GraphOperationMessage::TransformChange { layer: node_layer_id, - transform: DAffine2::from_translation(bounds_rounded_dimensions / 2.), + transform: DAffine2::from_translation(content_shift), transform_in: TransformIn::Local, skip_rerender: false, }); @@ -1470,13 +1481,11 @@ impl MessageHandler> for DocumentMes } impl DocumentMessageHandler { - /// Translates a viewport mouse position to a document-space transform, or returns identity if no mouse position is given. + /// Translates a viewport mouse position to a document-space transform, or uses the viewport center if no mouse position is given. fn document_transform_from_mouse(&self, mouse: Option<(f64, f64)>, viewport: &ViewportMessageHandler) -> DAffine2 { - mouse.map_or(DAffine2::IDENTITY, |pos| { - let viewport_location: DVec2 = pos.into(); - let document_to_viewport = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz); - DAffine2::from_translation(document_to_viewport.inverse().transform_point2(viewport_location - viewport.offset().into_dvec2())) - }) + let viewport_pos: DVec2 = mouse.map_or_else(|| viewport.center_in_viewport_space().into_dvec2() + viewport.offset().into_dvec2(), |pos| pos.into()); + let document_to_viewport = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz); + DAffine2::from_translation(document_to_viewport.inverse().transform_point2(viewport_pos - viewport.offset().into_dvec2())) } /// Runs an intersection test with all layers and a viewport space quad diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs index 2768b875ca..4d94f713ba 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs @@ -112,5 +112,7 @@ pub enum GraphOperationMessage { transform: DAffine2, parent: LayerNodeIdentifier, insert_index: usize, + /// When true, centers the SVG at the transform origin (clipboard paste / drag-drop). When false, keeps natural SVG coordinates (file-open flow). + center: bool, }, } diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index 9abe8f0e5a..44ac8e043d 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -321,6 +321,7 @@ impl MessageHandler> for transform, parent, insert_index, + center, } => { let tree = match usvg::Tree::from_str(&svg, &usvg::Options::default()) { Ok(t) => t, @@ -334,9 +335,13 @@ impl MessageHandler> for }; let mut modify_inputs = ModifyInputsContext::new(network_interface, responses); - let size = tree.size(); - let offset_to_center = DVec2::new(size.width() as f64, size.height() as f64) / -2.; - let transform = transform * DAffine2::from_translation(offset_to_center); + let transform = if center { + let size = tree.size(); + let offset_to_center = DVec2::new(size.width() as f64, size.height() as f64) / -2.; + transform * DAffine2::from_translation(offset_to_center) + } else { + transform + }; let graphite_gradient_stops = extract_graphite_gradient_stops(&svg); diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 69b91f0d95..b5725e09c9 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -681,6 +681,7 @@ impl MessageHandler> for Portfolio image, mouse: None, parent_and_insert_index: None, + place_at_origin: true, }); // Wait for the document to be rendered so the click targets can be calculated in order to determine the artboard size that will encompass the pasted image @@ -701,6 +702,7 @@ impl MessageHandler> for Portfolio svg, mouse: None, parent_and_insert_index: None, + place_at_origin: true, }); // Wait for the document to be rendered so the click targets can be calculated in order to determine the artboard size that will encompass the pasted SVG @@ -980,6 +982,7 @@ impl MessageHandler> for Portfolio image, mouse, parent_and_insert_index, + place_at_origin: false, }); } } @@ -997,6 +1000,7 @@ impl MessageHandler> for Portfolio svg, mouse, parent_and_insert_index, + place_at_origin: false, }); } } diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index c71f199078..51ec764b0c 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -230,7 +230,7 @@ pub fn new_image_layer(image_frame: Table>, id: NodeId, parent: Laye } /// Create a new group layer from an SVG string. -pub fn new_svg_layer(svg: String, transform: glam::DAffine2, id: NodeId, parent: LayerNodeIdentifier, responses: &mut VecDeque) -> LayerNodeIdentifier { +pub fn new_svg_layer(svg: String, transform: glam::DAffine2, center: bool, id: NodeId, parent: LayerNodeIdentifier, responses: &mut VecDeque) -> LayerNodeIdentifier { let insert_index = 0; responses.add(GraphOperationMessage::NewSvg { id, @@ -238,6 +238,7 @@ pub fn new_svg_layer(svg: String, transform: glam::DAffine2, id: NodeId, parent: transform, parent, insert_index, + center, }); LayerNodeIdentifier::new_unchecked(id) } From 7ef08c5ec5aef3cd3c7d07bacabdd58065a924fa Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 23 Mar 2026 03:30:00 -0700 Subject: [PATCH 4/9] Fix redundant Transform nodes when opening SVG/image files as documents --- .../graph_operation_message_handler.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index 44ac8e043d..3d0104ff78 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -580,18 +580,23 @@ fn import_usvg_path( let subpaths = convert_usvg_path(path); let bounds = subpaths.iter().filter_map(|subpath| subpath.bounding_box()).reduce(Quad::combine_bounds).unwrap_or_default(); - modify_inputs.insert_vector(subpaths, layer, true, path.fill().is_some(), path.stroke().is_some()); + // Compute the combined transform once; skip creating a Transform node entirely when it is identity. + let node_transform = transform * usvg_transform(node.abs_transform()); + let has_transform = node_transform != DAffine2::IDENTITY; - if let Some(transform_node_id) = modify_inputs.existing_network_node_id("Transform", true) { - transform_utils::update_transform(modify_inputs.network_interface, &transform_node_id, transform * usvg_transform(node.abs_transform())); - } + 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_network_node_id("Transform", 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, transform * usvg_transform(node.abs_transform())); + apply_usvg_stroke(stroke, modify_inputs, node_transform); } } From 862820aef2c9d418f93688d6f8151b01a8fca40f Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 23 Mar 2026 03:49:07 -0700 Subject: [PATCH 5/9] Offset the parent to its destination position not the child objects --- .../document/document_message_handler.rs | 6 ++- .../graph_operation_message_handler.rs | 43 +++++++++---------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index d100936968..03b72c48c1 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1370,7 +1370,11 @@ impl MessageHandler> for DocumentMes let content_shift = -bounds[0].round(); let needs_content_transform = !content_shift.abs_diff_eq(DVec2::ZERO, 1e-6); // With a content Transform node: use x: 15 (8 indent + 7 for the node width). Without: use x: LAYER_INDENT_OFFSET. - responses.add(NodeGraphMessage::ShiftNodePosition { node_id, x: if needs_content_transform { 15 } else { LAYER_INDENT_OFFSET }, y: -3 }); + responses.add(NodeGraphMessage::ShiftNodePosition { + node_id, + x: if needs_content_transform { 15 } else { LAYER_INDENT_OFFSET }, + y: -3, + }); responses.add(GraphOperationMessage::ResizeArtboard { layer: LayerNodeIdentifier::new_unchecked(node_id), location: if place_artboard_at_origin { IVec2::ZERO } else { bounds[0].round().as_ivec2() }, diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index 3d0104ff78..06b006afc1 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -335,25 +335,34 @@ impl MessageHandler> for }; let mut modify_inputs = ModifyInputsContext::new(network_interface, responses); - let transform = if center { + // The placement transform positions the root group in document space. + // When centering (paste at cursor/viewport), shift so the SVG is centered at the transform origin. + // When not centering (file-open flow), keep the natural SVG coordinates. + let mut placement_transform = if center { let size = tree.size(); let offset_to_center = DVec2::new(size.width() as f64, size.height() as f64) / -2.; transform * DAffine2::from_translation(offset_to_center) } else { transform }; + placement_transform.translation = placement_transform.translation.round(); let graphite_gradient_stops = extract_graphite_gradient_stops(&svg); + // Pass identity so each leaf layer receives only its SVG-native transform from `abs_transform`. + // The placement offset is then applied once to the root group layer below. import_usvg_node( &mut modify_inputs, &usvg::Node::Group(Box::new(tree.root().clone())), - transform, id, parent, insert_index, &graphite_gradient_stops, ); + + // After import, `layer_node` is set to the root group. Apply the placement transform to it + // (skipped automatically when identity, so file-open with content at origin creates no Transform node). + modify_inputs.transform_set(placement_transform, TransformIn::Local, false); } } } @@ -457,7 +466,6 @@ fn parse_hex_stop_color(hex: &str, opacity: f32) -> Option { fn import_usvg_node( modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, - transform: DAffine2, id: NodeId, parent: LayerNodeIdentifier, insert_index: usize, @@ -482,7 +490,7 @@ fn import_usvg_node( modify_inputs.import = true; for child in group.children() { - let extent = import_usvg_node_inner(modify_inputs, child, transform, NodeId::new(), layer, 0, graphite_gradient_stops, &mut group_extents_map); + 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); } @@ -501,7 +509,7 @@ fn import_usvg_node( 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); + import_usvg_path(modify_inputs, node, path, layer, graphite_gradient_stops); } usvg::Node::Image(_image) => { warn!("Skip image"); @@ -522,7 +530,6 @@ fn import_usvg_node( fn import_usvg_node_inner( modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, - transform: DAffine2, id: NodeId, parent: LayerNodeIdentifier, insert_index: usize, @@ -537,7 +544,7 @@ fn import_usvg_node_inner( usvg::Node::Group(group) => { let mut child_extents: Vec = 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); + 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); @@ -552,7 +559,7 @@ fn import_usvg_node_inner( total_extent } usvg::Node::Path(path) => { - import_usvg_path(modify_inputs, node, path, transform, layer, graphite_gradient_stops); + import_usvg_path(modify_inputs, node, path, layer, graphite_gradient_stops); 0 } usvg::Node::Image(_image) => { @@ -569,27 +576,19 @@ fn import_usvg_node_inner( } /// 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, -) { +fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, path: &usvg::Path, layer: LayerNodeIdentifier, graphite_gradient_stops: &HashMap) { let subpaths = convert_usvg_path(path); let bounds = subpaths.iter().filter_map(|subpath| subpath.bounding_box()).reduce(Quad::combine_bounds).unwrap_or_default(); - // Compute the combined transform once; skip creating a Transform node entirely when it is identity. - let node_transform = transform * usvg_transform(node.abs_transform()); + // 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_network_node_id("Transform", false) { - transform_utils::update_transform(modify_inputs.network_interface, &transform_node_id, node_transform); - } + if has_transform && let Some(transform_node_id) = modify_inputs.existing_network_node_id("Transform", 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]); From 42a44603a04d9021636479c22b50527cd6831d89 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 23 Mar 2026 05:07:02 -0700 Subject: [PATCH 6/9] Fix SVG/image File > Open artboard dimensions, origin, and clipping --- .../portfolio/document/document_message.rs | 5 ++- .../document/document_message_handler.rs | 28 ++++++++---- .../graph_operation_message_handler.rs | 3 +- .../portfolio/portfolio_message_handler.rs | 44 ++++++++++++++++++- 4 files changed, 67 insertions(+), 13 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index f0d5ab0a81..babe195b5f 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -9,7 +9,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GridSnapping}; use crate::messages::portfolio::utility_types::PanelType; use crate::messages::prelude::*; -use glam::DAffine2; +use glam::{DAffine2, IVec2}; use graph_craft::document::NodeId; use graphene_std::Color; use graphene_std::raster::BlendMode; @@ -229,6 +229,9 @@ pub enum DocumentMessage { SelectionStepForward, WrapContentInArtboard { place_artboard_at_origin: bool, + /// When `Some`, use this canvas (origin, dimensions) for the artboard instead of measuring the content bounding box. + /// The origin comes from the SVG viewBox's min-x/min-y values and the dimensions from its width/height. + artboard_canvas: Option<(IVec2, IVec2)>, }, ZoomCanvasTo100Percent, ZoomCanvasTo200Percent, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 03b72c48c1..1250fbad8a 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1349,25 +1349,35 @@ impl MessageHandler> for DocumentMes self.network_interface.selection_step_forward(&self.selection_network_path); responses.add(EventMessage::SelectionChanged); } - DocumentMessage::WrapContentInArtboard { place_artboard_at_origin } => { - // Get bounding box of all layers + DocumentMessage::WrapContentInArtboard { + place_artboard_at_origin, + artboard_canvas, + } => { + // Get bounding box of all layers (always needed to confirm there is content) let bounds = self.network_interface.document_bounds_document_space(false); let Some(bounds) = bounds else { return }; - let bounds_rounded_dimensions = (bounds[1] - bounds[0]).round(); + + // When artboard_canvas is provided (SVG file-open flow), use the declared canvas origin and dimensions; + // no content-shift Transform node needed since the SVG was already placed at its natural coordinates. + let (artboard_location, artboard_dimensions, content_shift) = if let Some((origin, dimensions)) = artboard_canvas { + (origin, dimensions, DVec2::ZERO) + } else { + // No declared canvas (image or clipboard paste): derive location and dimensions from the content bounding box. + let location = if place_artboard_at_origin { IVec2::ZERO } else { bounds[0].round().as_ivec2() }; + (location, (bounds[1] - bounds[0]).round().as_ivec2(), -bounds[0].round()) + }; // Create an artboard and set its dimensions to the bounding box size and location let node_id = NodeId::new(); let node_layer_id = LayerNodeIdentifier::new_unchecked(node_id); let new_artboard_node = document_node_definitions::resolve_network_node_type("Artboard") .expect("Failed to create artboard node") - .default_node_template(); + // Enable clipping by default (input index 5) so imported content is masked to the artboard bounds + .node_template_input_override([None, None, None, None, None, Some(NodeInput::value(TaggedValue::Bool(true), false))]); responses.add(NodeGraphMessage::InsertNode { node_id, node_template: Box::new(new_artboard_node), }); - // Compute the shift needed to align the content's top-left corner to the artboard's origin. - // When content is already at the document origin this is zero → no Transform node is created. - let content_shift = -bounds[0].round(); let needs_content_transform = !content_shift.abs_diff_eq(DVec2::ZERO, 1e-6); // With a content Transform node: use x: 15 (8 indent + 7 for the node width). Without: use x: LAYER_INDENT_OFFSET. responses.add(NodeGraphMessage::ShiftNodePosition { @@ -1377,8 +1387,8 @@ impl MessageHandler> for DocumentMes }); responses.add(GraphOperationMessage::ResizeArtboard { layer: LayerNodeIdentifier::new_unchecked(node_id), - location: if place_artboard_at_origin { IVec2::ZERO } else { bounds[0].round().as_ivec2() }, - dimensions: bounds_rounded_dimensions.as_ivec2(), + location: artboard_location, + dimensions: artboard_dimensions, }); // Connect the current output data to the artboard's input data, and the artboard's output to the document output diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index 06b006afc1..72200e29bb 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -337,7 +337,8 @@ impl MessageHandler> for // The placement transform positions the root group in document space. // When centering (paste at cursor/viewport), shift so the SVG is centered at the transform origin. - // When not centering (file-open flow), keep the natural SVG coordinates. + // When not centering (file-open flow), content stays at viewport coordinates (usvg's viewBox mapping + // already places it in [0, width] × [0, height]); the artboard's X/Y handles the viewBox origin offset. let mut placement_transform = if center { let size = tree.size(); let offset_to_center = DVec2::new(size.width() as f64, size.height() as f64) / -2.; diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index b5725e09c9..8254300b0f 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -686,7 +686,13 @@ impl MessageHandler> for Portfolio // Wait for the document to be rendered so the click targets can be calculated in order to determine the artboard size that will encompass the pasted image responses.add(DeferMessage::AfterGraphRun { - messages: vec![DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true }.into()], + messages: vec![ + DocumentMessage::WrapContentInArtboard { + place_artboard_at_origin: true, + artboard_canvas: None, + } + .into(), + ], }); responses.add(DeferMessage::AfterNavigationReady { messages: vec![DocumentMessage::ZoomCanvasToFitAll.into()], @@ -697,6 +703,34 @@ impl MessageHandler> for Portfolio name: name.clone().unwrap_or(DEFAULT_DOCUMENT_NAME.into()), }); + // Parse the SVG to extract its declared canvas origin and dimensions from the viewBox attribute. + // This preserves the full canvas rather than measuring only the tighter rendered content bounding box. + let artboard_canvas = usvg::roxmltree::Document::parse(&svg) + .ok() + .and_then(|doc| { + let vb = doc.root_element().attribute("viewBox")?; + let nums: Vec = vb + .split(|c: char| c.is_ascii_whitespace() || c == ',') + .filter(|s| !s.is_empty()) + .filter_map(|s| s.parse().ok()) + .collect(); + if nums.len() >= 4 { + Some(( + glam::IVec2::new(nums[0].round() as i32, nums[1].round() as i32), + glam::IVec2::new(nums[2].round() as i32, nums[3].round() as i32), + )) + } else { + None + } + }) + .or_else(|| { + // Fall back to the viewport size when there is no viewBox attribute + usvg::Tree::from_str(&svg, &usvg::Options::default()).ok().map(|tree| { + let size = tree.size(); + (glam::IVec2::ZERO, glam::IVec2::new(size.width().round() as i32, size.height().round() as i32)) + }) + }); + responses.add(DocumentMessage::PasteSvg { name, svg, @@ -707,7 +741,13 @@ impl MessageHandler> for Portfolio // Wait for the document to be rendered so the click targets can be calculated in order to determine the artboard size that will encompass the pasted SVG responses.add(DeferMessage::AfterGraphRun { - messages: vec![DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true }.into()], + messages: vec![ + DocumentMessage::WrapContentInArtboard { + place_artboard_at_origin: true, + artboard_canvas, + } + .into(), + ], }); responses.add(DeferMessage::AfterNavigationReady { messages: vec![DocumentMessage::ZoomCanvasToFitAll.into()], From 85689259b5ad75e11e238d6e0826fe187ac1ceab Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 23 Mar 2026 14:41:35 -0700 Subject: [PATCH 7/9] Fix the SVG to drag in at the mouse position relative to its visible center --- .../graph_operation/graph_operation_message_handler.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index 72200e29bb..03fc166dd4 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -340,9 +340,12 @@ impl MessageHandler> for // When not centering (file-open flow), content stays at viewport coordinates (usvg's viewBox mapping // already places it in [0, width] × [0, height]); the artboard's X/Y handles the viewBox origin offset. let mut placement_transform = if center { - let size = tree.size(); - let offset_to_center = DVec2::new(size.width() as f64, size.height() as f64) / -2.; - transform * DAffine2::from_translation(offset_to_center) + // Center on the actual rendered content bounds rather than the viewbox size. + // An SVG may have a viewbox larger than its content, so using viewport_size/2 would place the cursor + // in that empty region instead of on the content. + let bounds = tree.root().abs_bounding_box(); + let visual_center = DVec2::new((bounds.left() + bounds.right()) as f64 / 2., (bounds.top() + bounds.bottom()) as f64 / 2.); + transform * DAffine2::from_translation(-visual_center) } else { transform }; From 1694fd3f5031f72be2db4c8f8f613ac91cae9434 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 23 Mar 2026 14:43:55 -0700 Subject: [PATCH 8/9] Fix importing images into offset artboards so they don't get offset as well --- .../document/document_message_handler.rs | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 1250fbad8a..c964a86607 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -662,14 +662,22 @@ impl MessageHandler> for DocumentMes } => { // All the image's pixels have been converted to 0..=1, linear, and premultiplied by `Color::from_rgba8_srgb` + let layer_parent = self.new_layer_parent(true); let image_size = DVec2::new(image.width as f64, image.height as f64); let transform = if place_at_origin { // File-open flow: place at document origin without centering so `WrapContentInArtboard` can wrap it DAffine2::from_scale(image_size) } else { - // Clipboard paste or drag-drop: center at cursor or viewport center - self.document_transform_from_mouse(mouse, viewport) * DAffine2::from_scale_angle_translation(image_size, 0., image_size / -2.) + // Clipboard paste or drag-drop: center at cursor or viewport center. + // Convert the document-space cursor to the parent's local coordinate space so that + // an artboard at a non-zero position does not offset the placement. + let parent_to_document = { + let metadata = self.metadata(); + metadata.document_to_viewport.inverse() * metadata.transform_to_viewport(layer_parent) + }; + let cursor_in_parent = parent_to_document.inverse() * self.document_transform_from_mouse(mouse, viewport); + cursor_in_parent * DAffine2::from_scale_angle_translation(image_size, 0., image_size / -2.) }; let layer_node_id = NodeId::new(); @@ -677,7 +685,7 @@ impl MessageHandler> for DocumentMes responses.add(DocumentMessage::AddTransaction); - let layer = graph_modification_utils::new_image_layer(Table::new_from_element(Raster::new_cpu(image)), layer_node_id, self.new_layer_parent(true), responses); + let layer = graph_modification_utils::new_image_layer(Table::new_from_element(Raster::new_cpu(image)), layer_node_id, layer_parent, responses); if let Some(name) = name { responses.add(NodeGraphMessage::SetDisplayName { @@ -714,12 +722,19 @@ impl MessageHandler> for DocumentMes parent_and_insert_index, place_at_origin, } => { + let layer_parent = self.new_layer_parent(true); let transform = if place_at_origin { // File-open flow: place at document origin so `WrapContentInArtboard` can wrap it without extra Transform nodes DAffine2::IDENTITY } else { - // Clipboard paste or drag-drop: center at cursor or viewport center - self.document_transform_from_mouse(mouse, viewport) + // Clipboard paste or drag-drop: center at cursor or viewport center. + // Convert the document-space cursor to the parent's local coordinate space so that + // an artboard at a non-zero position does not offset the placement. + let parent_to_document = { + let metadata = self.metadata(); + metadata.document_to_viewport.inverse() * metadata.transform_to_viewport(layer_parent) + }; + parent_to_document.inverse() * self.document_transform_from_mouse(mouse, viewport) }; let layer_node_id = NodeId::new(); @@ -727,7 +742,7 @@ impl MessageHandler> for DocumentMes responses.add(DocumentMessage::AddTransaction); - let layer = graph_modification_utils::new_svg_layer(svg, transform, !place_at_origin, layer_node_id, self.new_layer_parent(true), responses); + let layer = graph_modification_utils::new_svg_layer(svg, transform, !place_at_origin, layer_node_id, layer_parent, responses); if let Some(name) = name { responses.add(NodeGraphMessage::SetDisplayName { From 4bc61849ed98368b7f099d950a421e70ddd039ab Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 23 Mar 2026 15:42:50 -0700 Subject: [PATCH 9/9] Code review --- editor/src/consts.rs | 2 ++ .../portfolio/document/document_message_handler.rs | 11 +++++++---- .../document/utility_types/network_interface.rs | 10 +++++----- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/editor/src/consts.rs b/editor/src/consts.rs index 0d0c2bfb7c..78b3a128fc 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -8,6 +8,8 @@ pub const IMPORTS_TO_LEFT_EDGE_PIXEL_GAP: u32 = 120; 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; +/// Horizontal grid width of a non-layer node in a chain. +pub const NODE_CHAIN_WIDTH: i32 = 7; // VIEWPORT pub const VIEWPORT_ZOOM_WHEEL_RATE: f64 = (1. / 600.) * 3.; diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index c964a86607..68b79a8d7f 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -4,7 +4,9 @@ use super::utility_types::misc::{GroupFolderType, SNAP_FUNCTIONS_FOR_BOUNDING_BO use super::utility_types::network_interface::{self, NodeNetworkInterface, TransactionStatus}; use super::utility_types::nodes::{CollapsedLayers, LayerStructureEntry, SelectedNodes}; use crate::application::{GRAPHITE_GIT_COMMIT_HASH, generate_uuid}; -use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_EXTENSION, LAYER_INDENT_OFFSET, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL}; +use crate::consts::{ + ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_EXTENSION, LAYER_INDENT_OFFSET, NODE_CHAIN_WIDTH, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL, +}; use crate::messages::input_mapper::utility_types::macros::action_shortcut; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::data_panel::{DataPanelMessageContext, DataPanelMessageHandler}; @@ -665,7 +667,7 @@ impl MessageHandler> for DocumentMes let layer_parent = self.new_layer_parent(true); let image_size = DVec2::new(image.width as f64, image.height as f64); - let transform = if place_at_origin { + let mut transform = if place_at_origin { // File-open flow: place at document origin without centering so `WrapContentInArtboard` can wrap it DAffine2::from_scale(image_size) } else { @@ -679,6 +681,7 @@ impl MessageHandler> for DocumentMes let cursor_in_parent = parent_to_document.inverse() * self.document_transform_from_mouse(mouse, viewport); cursor_in_parent * DAffine2::from_scale_angle_translation(image_size, 0., image_size / -2.) }; + transform.translation = transform.translation.round(); let layer_node_id = NodeId::new(); let layer_id = LayerNodeIdentifier::new_unchecked(layer_node_id); @@ -1394,10 +1397,10 @@ impl MessageHandler> for DocumentMes node_template: Box::new(new_artboard_node), }); let needs_content_transform = !content_shift.abs_diff_eq(DVec2::ZERO, 1e-6); - // With a content Transform node: use x: 15 (8 indent + 7 for the node width). Without: use x: LAYER_INDENT_OFFSET. + // With a content Transform node: shift by the layer indent plus the node width. Without: use just the layer indent. responses.add(NodeGraphMessage::ShiftNodePosition { node_id, - x: if needs_content_transform { 15 } else { LAYER_INDENT_OFFSET }, + x: if needs_content_transform { LAYER_INDENT_OFFSET + NODE_CHAIN_WIDTH } else { LAYER_INDENT_OFFSET }, y: -3, }); responses.add(GraphOperationMessage::ResizeArtboard { diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index d1ad306201..3c031a39b3 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -6,7 +6,7 @@ use super::document_metadata::{DocumentMetadata, LayerNodeIdentifier, NodeRelati 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, LAYER_INDENT_OFFSET, STACK_VERTICAL_GAP, + 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, NODE_CHAIN_WIDTH, 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}; @@ -243,7 +243,7 @@ impl NodeNetworkInterface { pub fn chain_width(&self, node_id: &NodeId, network_path: &[NodeId]) -> u32 { if self.number_of_displayed_inputs(node_id, network_path) > 1 { - let mut last_chain_node_distance = 0u32; + let mut last_chain_node_distance = 0_u32; // Iterate upstream from the layer, and get the number of nodes distance to the last node with Position::Chain for (index, node_id) in self .upstream_flow_back_from_nodes(vec![*node_id], network_path, FlowType::HorizontalPrimaryOutputFlow) @@ -255,11 +255,11 @@ impl NodeNetworkInterface { if self.is_chain(&node_id, network_path) { last_chain_node_distance = (index as u32) + 1; } else { - return last_chain_node_distance * 7 + 1; + return last_chain_node_distance * NODE_CHAIN_WIDTH as u32 + 1; } } - last_chain_node_distance * 7 + 1 + last_chain_node_distance * NODE_CHAIN_WIDTH as u32 + 1 } else { // Layer with no inputs has no chain 0 @@ -2695,7 +2695,7 @@ impl NodeNetworkInterface { if downstream_node_metadata.persistent_metadata.is_layer() { // Get the position of the layer let layer_position = self.position(downstream_node_id, network_path)?; - return Some(layer_position + IVec2::new(-node_distance_from_layer * 7, 0)); + return Some(layer_position + IVec2::new(-node_distance_from_layer * NODE_CHAIN_WIDTH, 0)); } node_distance_from_layer += 1; current_node_id = *downstream_node_id;