Repository: GraphiteEditor/Graphite (language: rust)

Issue:
Dragging images into a fresh editor
If a user drags an image into the welcome screen (shown when zero documents are open), it should create a new document with an artboard matching the dimensions of the image, and the image placed at 100% scale with the top left corner at the document's origin coordinates. Pasting an image from the clipboard should have the same effect.

Additionally, when the welcome screen is not shown, users should be able to drag an image into the tab bar (the dark horizontal bar occupied by the tabs, including the tabs themselves).

All this should also work for .graphite files.

Related: we'd like to support URLs (similar to <https://editor.graphite.rs/#demo/red-dress>) where we can open a bitmap or SVG as a new document. Care should be taken to ensure security for potentially malicious files that could be loaded and opened by the editor (someday stealing account info, for example, if the user is logged into Graphite?).


Discussion / hints:
Hi! I can work on this if it's not being worked on already
Sure, go for it! Just please be careful about which places in the frontend that the drop handler should exist. When on the welcome screen (no docs open), it should be everywhere. When a document is open, it needs to exclude parts related to the currently open document (so at least the viewport) but still allow it being dropped in the tab bar and other editor areas. In the layer panel would probably be fine for now but eventually that should probably be inserted into the currently open document equivalent to dropping it in the viewport of the document (feel free to work on that change too if you'd like).

You can also reuse this functionality to open .graphite files dropped into the editor.
Currently, trying to open an non-graphite image file by clicking on 'Open Document' gives an error. Do we want 'Open Document' for an image file to have the same behavior as what's described in this issue?
Probably not because we'd like the file picker dialog to filter by `.graphite` files (which it also currently doesn't do), however File > Import should probably work from an empty editor.
Understood. I think it does filter by `.graphite` files as I had to manually change the filter to "All Files". File > Import seems to be disabled on the welcome screen (if that's what you mean by empty editor). 
Yeah, you'd have to reenable File > Import on the welcome screen once that functionality is added.
I would like to remove myself from this task as I wasn't able to make much progress and I will be busy for the next couple of weeks. I don't want to keep someone else from taking on this task. But maybe I'll pick it up again in a few weeks if it's still here.
Sounds good @abumsoo, thanks for the update.

Fix patch:
diff --git a/.github/workflows/comment-profiling-changes.yaml b/.github/workflows/comment-profiling-changes.yaml
index 8ff65294af..6f7fc17dba 100644
--- a/.github/workflows/comment-profiling-changes.yaml
+++ b/.github/workflows/comment-profiling-changes.yaml
@@ -9,6 +9,8 @@ env:
 
 jobs:
   profile:
+    # TODO(TrueDoctor): Fix and reenable this action
+    if: false
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v3
diff --git a/editor/src/consts.rs b/editor/src/consts.rs
index 89385ad2f5..37287a9dfa 100644
--- a/editor/src/consts.rs
+++ b/editor/src/consts.rs
@@ -40,6 +40,9 @@ pub const SLOWING_DIVISOR: f64 = 10.;
 pub const NUDGE_AMOUNT: f64 = 1.;
 pub const BIG_NUDGE_AMOUNT: f64 = 10.;
 
+// Tools
+pub const DEFAULT_STROKE_WIDTH: f64 = 2.;
+
 // Select tool
 pub const SELECTION_TOLERANCE: f64 = 5.;
 pub const SELECTION_DRAG_ANGLE: f64 = 90.;
@@ -65,6 +68,7 @@ pub const LINE_ROTATE_SNAP_ANGLE: f64 = 15.;
 
 // Brush tool
 pub const BRUSH_SIZE_CHANGE_KEYBOARD: f64 = 5.;
+pub const DEFAULT_BRUSH_SIZE: f64 = 20.;
 
 // Scrollbars
 pub const SCROLLBAR_SPACING: f64 = 0.1;
diff --git a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs
index efad5063ea..ff4b27ba57 100644
--- a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs
+++ b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs
@@ -34,6 +34,8 @@ impl MessageHandler<NewDocumentDialogMessage, ()> for NewDocumentDialogMessageHa
 
 				responses.add(NodeGraphMessage::RunDocumentGraph);
 				responses.add(NodeGraphMessage::UpdateNewNodeGraph);
+
+				// TODO: Figure out how to get StartBuffer to work here so we can delete this and use `DocumentMessage::ZoomCanvasToFitAll` instead
 				responses.add(Message::StartBuffer);
 				responses.add(FrontendMessage::TriggerDelayedZoomCanvasToFitAll);
 			}
diff --git a/editor/src/messages/dialog/simple_dialogs/close_document_dialog.rs b/editor/src/messages/dialog/simple_dialogs/close_document_dialog.rs
index 8939b8992a..d370b2fde8 100644
--- a/editor/src/messages/dialog/simple_dialogs/close_document_dialog.rs
+++ b/editor/src/messages/dialog/simple_dialogs/close_document_dialog.rs
@@ -41,12 +41,22 @@ impl DialogLayoutHolder for CloseDocumentDialog {
 
 impl LayoutHolder for CloseDocumentDialog {
 	fn layout(&self) -> Layout {
+		let max_length = 60;
+		let max_one_line_length = 40;
+
+		let mut name = self.document_name.clone();
+
+		name.truncate(max_length);
+		let ellipsis = if self.document_name.len() > max_length { "…" } else { "" };
+
+		let break_lines = if self.document_name.len() > max_one_line_length { '\n' } else { ' ' };
+
 		Layout::WidgetLayout(WidgetLayout::new(vec![
 			LayoutGroup::Row {
 				widgets: vec![TextLabel::new("Save document before closing it?").bold(true).widget_holder()],
 			},
 			LayoutGroup::Row {
-				widgets: vec![TextLabel::new(format!("\"{}\" has unsaved changes", self.document_name)).multiline(true).widget_holder()],
+				widgets: vec![TextLabel::new(format!("\"{name}{ellipsis}\"{break_lines}has unsaved changes")).multiline(true).widget_holder()],
 			},
 		]))
 	}
diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs
index cd4cccd3c2..7a7c87dba5 100644
--- a/editor/src/messages/portfolio/document/document_message.rs
+++ b/editor/src/messages/portfolio/document/document_message.rs
@@ -77,13 +77,6 @@ pub enum DocumentMessage {
 		imaginate_node: Vec<NodeId>,
 		then_generate: bool,
 	},
-	ImportSvg {
-		id: NodeId,
-		svg: String,
-		transform: DAffine2,
-		parent: LayerNodeIdentifier,
-		insert_index: usize,
-	},
 	MoveSelectedLayersTo {
 		parent: LayerNodeIdentifier,
 		insert_index: usize,
@@ -98,12 +91,16 @@ pub enum DocumentMessage {
 		resize_opposite_corner: Key,
 	},
 	PasteImage {
+		name: Option<String>,
 		image: Image<Color>,
 		mouse: Option<(f64, f64)>,
+		parent_and_insert_index: Option<(LayerNodeIdentifier, usize)>,
 	},
 	PasteSvg {
+		name: Option<String>,
 		svg: String,
 		mouse: Option<(f64, f64)>,
+		parent_and_insert_index: Option<(LayerNodeIdentifier, usize)>,
 	},
 	Redo,
 	RenameDocument {
@@ -176,6 +173,9 @@ pub enum DocumentMessage {
 	PTZUpdate,
 	SelectionStepBack,
 	SelectionStepForward,
+	WrapContentInArtboard {
+		place_artboard_at_origin: bool,
+	},
 	ZoomCanvasTo100Percent,
 	ZoomCanvasTo200Percent,
 	ZoomCanvasToFitAll,
diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs
index cfe6b08108..ce43a36c7d 100644
--- a/editor/src/messages/portfolio/document/document_message_handler.rs
+++ b/editor/src/messages/portfolio/document/document_message_handler.rs
@@ -1,9 +1,10 @@
+use super::node_graph::document_node_definitions;
 use super::node_graph::utility_types::Transform;
 use super::overlays::utility_types::Pivot;
 use super::utility_types::clipboards::Clipboard;
 use super::utility_types::error::EditorError;
 use super::utility_types::misc::{SnappingOptions, SnappingState, GET_SNAP_BOX_FUNCTIONS, GET_SNAP_GEOMETRY_FUNCTIONS};
-use super::utility_types::network_interface::{NodeNetworkInterface, TransactionStatus};
+use super::utility_types::network_interface::{self, NodeNetworkInterface, TransactionStatus};
 use super::utility_types::nodes::{CollapsedLayers, SelectedNodes};
 use crate::application::{generate_uuid, GRAPHITE_GIT_COMMIT_HASH};
 use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL};
@@ -18,7 +19,7 @@ use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate,
 use crate::messages::portfolio::document::utility_types::nodes::RawBuffer;
 use crate::messages::portfolio::utility_types::PersistentData;
 use crate::messages::prelude::*;
-use crate::messages::tool::common_functionality::graph_modification_utils::{get_blend_mode, get_opacity};
+use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_blend_mode, get_opacity};
 use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys;
 use crate::messages::tool::tool_messages::tool_prelude::Key;
 use crate::messages::tool::utility_types::ToolType;
@@ -29,7 +30,7 @@ use graph_craft::document::{NodeId, NodeNetwork, OldNodeNetwork};
 use graphene_core::raster::{BlendMode, ImageFrame};
 use graphene_core::vector::style::ViewMode;
 
-use glam::{DAffine2, DVec2};
+use glam::{DAffine2, DVec2, IVec2};
 
 pub struct DocumentMessageData<'a> {
 	pub document_id: DocumentId,
@@ -380,7 +381,11 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
 					let Some(bounds) = self.metadata().bounding_box_document(layer) else { continue };
 
 					let name = self.network_interface.frontend_display_name(&layer.to_node(), &[]);
-					let transform = self.metadata().document_to_viewport * DAffine2::from_translation(bounds[0].min(bounds[1]) - DVec2::Y * 4.);
+
+					let (_, angle, translation) = self.metadata().document_to_viewport.to_scale_angle_translation();
+					let translation = translation + bounds[0].min(bounds[1]) - DVec2::Y * 4.;
+					let transform = DAffine2::from_angle_translation(angle, translation);
+
 					overlay_context.text_with_transform(&name, COLOR_OVERLAY_GRAY, None, transform, Pivot::BottomLeft);
 				}
 			}
@@ -498,7 +503,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
 				let insert_index = DocumentMessageHandler::get_calculated_insert_index(self.metadata(), self.network_interface.selected_nodes(&[]).unwrap(), parent);
 
 				let node_id = NodeId(generate_uuid());
-				let new_group_node = super::node_graph::document_node_definitions::resolve_document_node_type("Merge")
+				let new_group_node = document_node_definitions::resolve_document_node_type("Merge")
 					.expect("Failed to create merge node")
 					.default_node_template();
 				responses.add(NodeGraphMessage::InsertNode {
@@ -546,23 +551,6 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
 					responses.add(DocumentMessage::ImaginateGenerate { imaginate_node });
 				}
 			}
-			DocumentMessage::ImportSvg {
-				id,
-				svg,
-				transform,
-				parent,
-				insert_index,
-			} => {
-				responses.add(DocumentMessage::StartTransaction);
-				responses.add(GraphOperationMessage::NewSvg {
-					id,
-					svg,
-					transform,
-					parent,
-					insert_index,
-				});
-				responses.add(DocumentMessage::EndTransaction);
-			}
 			DocumentMessage::MoveSelectedLayersTo { parent, insert_index } => {
 				if !self.selection_network_path.is_empty() {
 					log::error!("Moving selected layers is only supported for the Document Network");
@@ -608,7 +596,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
 
 				let layers_to_move = self.network_interface.shallowest_unique_layers_sorted(&self.selection_network_path);
 				// Offset the index for layers to move that are below another layer to move. For example when moving 1 and 2 between 3 and 4, 2 should be inserted at the same index as 1 since 1 is moved first.
-				let layers_to_move_with_insert_offset: Vec<(LayerNodeIdentifier, usize)> = layers_to_move
+				let layers_to_move_with_insert_offset = layers_to_move
 					.iter()
 					.map(|layer| {
 						if layer.parent(self.metadata()) != Some(parent) {
@@ -727,7 +715,12 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
 					}
 				}
 			}
-			DocumentMessage::PasteImage { image, mouse } => {
+			DocumentMessage::PasteImage {
+				name,
+				image,
+				mouse,
+				parent_and_insert_index,
+			} => {
 				// 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);
@@ -744,12 +737,27 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
 
 				let transform = center_in_viewport_layerspace * fit_image_size;
 
+				let layer_node_id = NodeId(generate_uuid());
+				let layer_id = LayerNodeIdentifier::new_unchecked(layer_node_id);
+
 				responses.add(DocumentMessage::AddTransaction);
 
 				let image_frame = ImageFrame { image, ..Default::default() };
+				let layer = graph_modification_utils::new_image_layer(image_frame, layer_node_id, self.new_layer_parent(true), responses);
 
-				use crate::messages::tool::common_functionality::graph_modification_utils;
-				let layer = graph_modification_utils::new_image_layer(image_frame, NodeId(generate_uuid()), self.new_layer_parent(true), responses);
+				if let Some(name) = name {
+					responses.add(NodeGraphMessage::SetDisplayName {
+						node_id: layer.to_node(),
+						alias: name,
+					});
+				}
+				if let Some((parent, insert_index)) = parent_and_insert_index {
+					responses.add(NodeGraphMessage::MoveLayerToStack {
+						layer: layer_id,
+						parent,
+						insert_index,
+					});
+				}
 
 				// `layer` cannot be `ROOT_PARENT` since it is the newly created layer
 				responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] });
@@ -764,12 +772,37 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
 				// Force chosen tool to be Select Tool after importing image.
 				responses.add(ToolMessage::ActivateTool { tool_type: ToolType::Select });
 			}
-			DocumentMessage::PasteSvg { svg, mouse } => {
-				use crate::messages::tool::common_functionality::graph_modification_utils;
-				let viewport_location = mouse.map_or(ipp.viewport_bounds.center() + ipp.viewport_bounds.top_left, |pos| pos.into());
+			DocumentMessage::PasteSvg {
+				name,
+				svg,
+				mouse,
+				parent_and_insert_index,
+			} => {
 				let document_to_viewport = self.navigation_handler.calculate_offset_transform(ipp.viewport_bounds.center(), &self.document_ptz);
+				let viewport_location = mouse.map_or(ipp.viewport_bounds.center() + ipp.viewport_bounds.top_left, |pos| pos.into());
 				let center_in_viewport = DAffine2::from_translation(document_to_viewport.inverse().transform_point2(viewport_location - ipp.viewport_bounds.top_left));
-				let layer = graph_modification_utils::new_svg_layer(svg, center_in_viewport, NodeId(generate_uuid()), self.new_layer_parent(true), responses);
+
+				let layer_node_id = NodeId(generate_uuid());
+				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);
+
+				if let Some(name) = name {
+					responses.add(NodeGraphMessage::SetDisplayName {
+						node_id: layer.to_node(),
+						alias: name,
+					});
+				}
+				if let Some((parent, insert_index)) = parent_and_insert_index {
+					responses.add(NodeGraphMessage::MoveLayerToStack {
+						layer: layer_id,
+						parent,
+						insert_index,
+					});
+				}
+
 				responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] });
 				responses.add(ToolMessage::ActivateTool { tool_type: ToolType::Select });
 			}
@@ -1181,6 +1214,44 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
 				self.network_interface.selection_step_forward(&self.selection_network_path);
 				responses.add(BroadcastEvent::SelectionChanged);
 			}
+			DocumentMessage::WrapContentInArtboard { place_artboard_at_origin } => {
+				// Get bounding box of all layers
+				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();
+
+				// Create an artboard and set its dimensions to the bounding box size and location
+				let node_id = NodeId(generate_uuid());
+				let node_layer_id = LayerNodeIdentifier::new_unchecked(node_id);
+				let new_artboard_node = document_node_definitions::resolve_document_node_type("Artboard")
+					.expect("Failed to create artboard node")
+					.default_node_template();
+				responses.add(NodeGraphMessage::InsertNode {
+					node_id,
+					node_template: new_artboard_node,
+				});
+				responses.add(NodeGraphMessage::ShiftNodePosition { node_id, x: 15, 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() },
+					dimensions: bounds_rounded_dimensions.as_ivec2(),
+				});
+
+				// Connect the current output data to the artboard's input data, and the artboard's output to the document output
+				responses.add(NodeGraphMessage::InsertNodeBetween {
+					node_id,
+					input_connector: network_interface::InputConnector::Export(0),
+					insert_node_input_index: 1,
+				});
+
+				// Shift the content by half its width and height so it gets centered in the artboard
+				responses.add(GraphOperationMessage::TransformChange {
+					layer: node_layer_id,
+					transform: DAffine2::from_translation(bounds_rounded_dimensions / 2.),
+					transform_in: TransformIn::Local,
+					skip_rerender: true,
+				});
+			}
 			DocumentMessage::ZoomCanvasTo100Percent => {
 				responses.add_front(NavigationMessage::CanvasZoomSet { zoom_factor: 1. });
 			}
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 f4b4a38b31..5aab007a42 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
@@ -173,7 +173,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
 			GraphOperationMessage::NewVectorLayer { id, subpaths, parent, insert_index } => {
 				let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
 				let layer = modify_inputs.create_layer(id);
-				modify_inputs.insert_vector_data(subpaths, layer);
+				modify_inputs.insert_vector_data(subpaths, layer, true, true, true);
 				network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
 				responses.add(NodeGraphMessage::RunDocumentGraph);
 			}
@@ -228,6 +228,10 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
 				};
 				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);
+
 				import_usvg_node(&mut modify_inputs, &usvg::Node::Group(Box::new(tree.root().clone())), transform, id, parent, insert_index);
 			}
 		}
@@ -260,15 +264,20 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
 		usvg::Node::Path(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_data(subpaths, layer);
+
+			modify_inputs.insert_vector_data(subpaths, layer, true, path.fill().is_some(), path.stroke().is_some());
 
 			if let Some(transform_node_id) = modify_inputs.existing_node_id("Transform") {
 				transform_utils::update_transform(modify_inputs.network_interface, &transform_node_id, transform * usvg_transform(node.abs_transform()));
 			}
 
-			let bounds_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
-			apply_usvg_fill(path.fill(), modify_inputs, transform * usvg_transform(node.abs_transform()), bounds_transform);
-			apply_usvg_stroke(path.stroke(), modify_inputs, transform * usvg_transform(node.abs_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, transform * usvg_transform(node.abs_transform()), bounds_transform);
+			}
+			if let Some(stroke) = path.stroke() {
+				apply_usvg_stroke(stroke, modify_inputs, transform * usvg_transform(node.abs_transform()));
+			}
 		}
 		usvg::Node::Image(_image) => {
 			warn!("Skip image")
@@ -281,96 +290,90 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
 	}
 }
 
-fn apply_usvg_stroke(stroke: Option<&usvg::Stroke>, modify_inputs: &mut ModifyInputsContext, transform: DAffine2) {
-	if let Some(stroke) = stroke {
-		if let usvg::Paint::Color(color) = &stroke.paint() {
-			modify_inputs.stroke_set(Stroke {
-				color: Some(usvg_color(*color, stroke.opacity().get())),
-				weight: stroke.width().get() as f64,
-				dash_lengths: stroke.dasharray().as_ref().map(|lengths| lengths.iter().map(|&length| length as f64).collect()).unwrap_or_default(),
-				dash_offset: stroke.dashoffset() as f64,
-				line_cap: match stroke.linecap() {
-					usvg::LineCap::Butt => LineCap::Butt,
-					usvg::LineCap::Round => LineCap::Round,
-					usvg::LineCap::Square => LineCap::Square,
-				},
-				line_join: match stroke.linejoin() {
-					usvg::LineJoin::Miter => LineJoin::Miter,
-					usvg::LineJoin::MiterClip => LineJoin::Miter,
-					usvg::LineJoin::Round => LineJoin::Round,
-					usvg::LineJoin::Bevel => LineJoin::Bevel,
-				},
-				line_join_miter_limit: stroke.miterlimit().get() as f64,
-				transform,
-			})
-		} else {
-			warn!("Skip non-solid stroke")
-		}
+fn apply_usvg_stroke(stroke: &usvg::Stroke, modify_inputs: &mut ModifyInputsContext, transform: DAffine2) {
+	if let usvg::Paint::Color(color) = &stroke.paint() {
+		modify_inputs.stroke_set(Stroke {
+			color: Some(usvg_color(*color, stroke.opacity().get())),
+			weight: stroke.width().get() as f64,
+			dash_lengths: stroke.dasharray().as_ref().map(|lengths| lengths.iter().map(|&length| length as f64).collect()).unwrap_or_default(),
+			dash_offset: stroke.dashoffset() as f64,
+			line_cap: match stroke.linecap() {
+				usvg::LineCap::Butt => LineCap::Butt,
+				usvg::LineCap::Round => LineCap::Round,
+				usvg::LineCap::Square => LineCap::Square,
+			},
+			line_join: match stroke.linejoin() {
+				usvg::LineJoin::Miter => LineJoin::Miter,
+				usvg::LineJoin::MiterClip => LineJoin::Miter,
+				usvg::LineJoin::Round => LineJoin::Round,
+				usvg::LineJoin::Bevel => LineJoin::Bevel,
+			},
+			line_join_miter_limit: stroke.miterlimit().get() as f64,
+			transform,
+		})
 	}
 }
 
-fn apply_usvg_fill(fill: Option<&usvg::Fill>, modify_inputs: &mut ModifyInputsContext, transform: DAffine2, bounds_transform: DAffine2) {
-	if let Some(fill) = &fill {
-		modify_inputs.fill_set(match &fill.paint() {
-			usvg::Paint::Color(color) => Fill::solid(usvg_color(*color, fill.opacity().get())),
-			usvg::Paint::LinearGradient(linear) => {
-				let local = [DVec2::new(linear.x1() as f64, linear.y1() as f64), DVec2::new(linear.x2() as f64, linear.y2() as f64)];
+fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, transform: DAffine2, bounds_transform: DAffine2) {
+	modify_inputs.fill_set(match &fill.paint() {
+		usvg::Paint::Color(color) => Fill::solid(usvg_color(*color, fill.opacity().get())),
+		usvg::Paint::LinearGradient(linear) => {
+			let local = [DVec2::new(linear.x1() as f64, linear.y1() as f64), DVec2::new(linear.x2() as f64, linear.y2() as f64)];
 
-				// TODO: fix this
-				// let to_doc_transform = if linear.base.units() == usvg::Units::UserSpaceOnUse {
-				// 	transform
-				// } else {
-				// 	transformed_bound_transform
-				// };
-				let to_doc_transform = transform;
-				let to_doc = to_doc_transform * usvg_transform(linear.transform());
+			// TODO: fix this
+			// let to_doc_transform = if linear.base.units() == usvg::Units::UserSpaceOnUse {
+			// 	transform
+			// } else {
+			// 	transformed_bound_transform
+			// };
+			let to_doc_transform = transform;
+			let to_doc = to_doc_transform * usvg_transform(linear.transform());
 
-				let document = [to_doc.transform_point2(local[0]), to_doc.transform_point2(local[1])];
-				let layer = [transform.inverse().transform_point2(document[0]), transform.inverse().transform_point2(document[1])];
+			let document = [to_doc.transform_point2(local[0]), to_doc.transform_point2(local[1])];
+			let layer = [transform.inverse().transform_point2(document[0]), transform.inverse().transform_point2(document[1])];
 
-				let [start, end] = [bounds_transform.inverse().transform_point2(layer[0]), bounds_transform.inverse().transform_point2(layer[1])];
-				let stops = linear.stops().iter().map(|stop| (stop.offset().get() as f64, usvg_color(stop.color(), stop.opacity().get()))).collect();
-				let stops = GradientStops(stops);
+			let [start, end] = [bounds_transform.inverse().transform_point2(layer[0]), bounds_transform.inverse().transform_point2(layer[1])];
+			let stops = linear.stops().iter().map(|stop| (stop.offset().get() as f64, usvg_color(stop.color(), stop.opacity().get()))).collect();
+			let stops = GradientStops(stops);
 
-				Fill::Gradient(Gradient {
-					start,
-					end,
-					transform: DAffine2::IDENTITY,
-					gradient_type: GradientType::Linear,
-					stops,
-				})
-			}
-			usvg::Paint::RadialGradient(radial) => {
-				let local = [DVec2::new(radial.cx() as f64, radial.cy() as f64), DVec2::new(radial.fx() as f64, radial.fy() as f64)];
+			Fill::Gradient(Gradient {
+				start,
+				end,
+				transform: DAffine2::IDENTITY,
+				gradient_type: GradientType::Linear,
+				stops,
+			})
+		}
+		usvg::Paint::RadialGradient(radial) => {
+			let local = [DVec2::new(radial.cx() as f64, radial.cy() as f64), DVec2::new(radial.fx() as f64, radial.fy() as f64)];
 
-				// TODO: fix this
-				// let to_doc_transform = if radial.base.units == usvg::Units::UserSpaceOnUse {
-				// 	transform
-				// } else {
-				// 	transformed_bound_transform
-				// };
-				let to_doc_transform = transform;
-				let to_doc = to_doc_transform * usvg_transform(radial.transform());
+			// TODO: fix this
+			// let to_doc_transform = if radial.base.units == usvg::Units::UserSpaceOnUse {
+			// 	transform
+			// } else {
+			// 	transformed_bound_transform
+			// };
+			let to_doc_transform = transform;
+			let to_doc = to_doc_transform * usvg_transform(radial.transform());
 
-				let document = [to_doc.transform_point2(local[0]), to_doc.transform_point2(local[1])];
-				let layer = [transform.inverse().transform_point2(document[0]), transform.inverse().transform_point2(document[1])];
+			let document = [to_doc.transform_point2(local[0]), to_doc.transform_point2(local[1])];
+			let layer = [transform.inverse().transform_point2(document[0]), transform.inverse().transform_point2(document[1])];
 
-				let [start, end] = [bounds_transform.inverse().transform_point2(layer[0]), bounds_transform.inverse().transform_point2(layer[1])];
-				let stops = radial.stops().iter().map(|stop| (stop.offset().get() as f64, usvg_color(stop.color(), stop.opacity().get()))).collect();
-				let stops = GradientStops(stops);
+			let [start, end] = [bounds_transform.inverse().transform_point2(layer[0]), bounds_transform.inverse().transform_point2(layer[1])];
+			let stops = radial.stops().iter().map(|stop| (stop.offset().get() as f64, usvg_color(stop.color(), stop.opacity().get()))).collect();
+			let stops = GradientStops(stops);
 
-				Fill::Gradient(Gradient {
-					start,
-					end,
-					transform: DAffine2::IDENTITY,
-					gradient_type: GradientType::Radial,
-					stops,
-				})
-			}
-			usvg::Paint::Pattern(_) => {
-				warn!("Skip pattern");
-				return;
-			}
-		});
-	}
+			Fill::Gradient(Gradient {
+				start,
+				end,
+				transform: DAffine2::IDENTITY,
+				gradient_type: GradientType::Radial,
+				stops,
+			})
+		}
+		usvg::Paint::Pattern(_) => {
+			warn!("Skip pattern");
+			return;
+		}
+	});
 }
diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs
index 1fdb0859ce..870750b99f 100644
--- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs
+++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs
@@ -145,32 +145,36 @@ impl<'a> ModifyInputsContext<'a> {
 		self.network_interface.move_node_to_chain_start(&boolean_id, layer, &[]);
 	}
 
-	pub fn insert_vector_data(&mut self, subpaths: Vec<Subpath<PointId>>, layer: LayerNodeIdentifier) {
+	pub fn insert_vector_data(&mut self, subpaths: Vec<Subpath<PointId>>, layer: LayerNodeIdentifier, include_transform: bool, include_fill: bool, include_stroke: bool) {
 		let vector_data = VectorData::from_subpaths(subpaths, true);
 
-		let path = resolve_document_node_type("Path")
+		let shape = resolve_document_node_type("Path")
 			.expect("Path node does not exist")
 			.node_template_input_override([Some(NodeInput::value(TaggedValue::VectorData(vector_data), false))]);
-
-		let transform = resolve_document_node_type("Transform").expect("Transform node does not exist").default_node_template();
-		let fill = resolve_document_node_type("Fill").expect("Fill node does not exist").default_node_template();
-		let stroke = resolve_document_node_type("Stroke").expect("Stroke node does not exist").default_node_template();
-
 		let shape_id = NodeId(generate_uuid());
-		self.network_interface.insert_node(shape_id, path, &[]);
+		self.network_interface.insert_node(shape_id, shape, &[]);
 		self.network_interface.move_node_to_chain_start(&shape_id, layer, &[]);
 
-		let transform_id = NodeId(generate_uuid());
-		self.network_interface.insert_node(transform_id, transform, &[]);
-		self.network_interface.move_node_to_chain_start(&transform_id, layer, &[]);
+		if include_transform {
+			let transform = resolve_document_node_type("Transform").expect("Transform node does not exist").default_node_template();
+			let transform_id = NodeId(generate_uuid());
+			self.network_interface.insert_node(transform_id, transform, &[]);
+			self.network_interface.move_node_to_chain_start(&transform_id, layer, &[]);
+		}
 
-		let fill_id = NodeId(generate_uuid());
-		self.network_interface.insert_node(fill_id, fill, &[]);
-		self.network_interface.move_node_to_chain_start(&fill_id, layer, &[]);
+		if include_fill {
+			let fill = resolve_document_node_type("Fill").expect("Fill node does not exist").default_node_template();
+			let fill_id = NodeId(generate_uuid());
+			self.network_interface.insert_node(fill_id, fill, &[]);
+			self.network_interface.move_node_to_chain_start(&fill_id, layer, &[]);
+		}
 
-		let stroke_id = NodeId(generate_uuid());
-		self.network_interface.insert_node(stroke_id, stroke, &[]);
-		self.network_interface.move_node_to_chain_start(&stroke_id, layer, &[]);
+		if include_stroke {
+			let stroke = resolve_document_node_type("Stroke").expect("Stroke node does not exist").default_node_template();
+			let stroke_id = NodeId(generate_uuid());
+			self.network_interface.insert_node(stroke_id, stroke, &[]);
+			self.network_interface.move_node_to_chain_start(&stroke_id, layer, &[]);
+		}
 	}
 
 	pub fn insert_text(&mut self, text: String, font: Font, size: f64, layer: LayerNodeIdentifier) {
diff --git a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs
index 40a8547569..a88e426ef5 100644
--- a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs
+++ b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs
@@ -103,7 +103,6 @@ impl LayoutHolder for MenuBarMessageHandler {
 							label: "Import…".into(),
 							shortcut: action_keys!(PortfolioMessageDiscriminant::Import),
 							action: MenuBarEntry::create_action(|_| PortfolioMessage::Import.into()),
-							disabled: no_active_document, // TODO: Allow importing an image (or dragging it in, or pasting) without an active document to create a new one with an artboards of the image's size (issue #1140)
 							..MenuBarEntry::default()
 						},
 						MenuBarEntry {
diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs
index 742894636a..02bb698a40 100644
--- a/editor/src/messages/portfolio/portfolio_message.rs
+++ b/editor/src/messages/portfolio/portfolio_message.rs
@@ -4,7 +4,9 @@ use crate::messages::frontend::utility_types::{ExportBounds, FileType};
 use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
 use crate::messages::prelude::*;
 
+use graphene_core::raster::Image;
 use graphene_core::text::Font;
+use graphene_core::Color;
 
 #[impl_message(Message, Portfolio)]
 #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
@@ -87,6 +89,18 @@ pub enum PortfolioMessage {
 	PasteSerializedData {
 		data: String,
 	},
+	PasteImage {
+		name: Option<String>,
+		image: Image<Color>,
+		mouse: Option<(f64, f64)>,
+		parent_and_insert_index: Option<(LayerNodeIdentifier, usize)>,
+	},
+	PasteSvg {
+		name: Option<String>,
+		svg: String,
+		mouse: Option<(f64, f64)>,
+		parent_and_insert_index: Option<(LayerNodeIdentifier, usize)>,
+	},
 	PrevDocument,
 	SetActivePanel {
 		panel: PanelType,
diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs
index 2468c9ec98..b83fb90360 100644
--- a/editor/src/messages/portfolio/portfolio_message_handler.rs
+++ b/editor/src/messages/portfolio/portfolio_message_handler.rs
@@ -317,9 +317,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
 			}
 			PortfolioMessage::Import => {
 				// This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages
-				if self.active_document().is_some() {
-					responses.add(FrontendMessage::TriggerImport);
-				}
+				responses.add(FrontendMessage::TriggerImport);
 			}
 			PortfolioMessage::LoadDocumentResources { document_id } => {
 				if let Some(document) = self.document_mut(document_id) {
@@ -636,6 +634,68 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
 					}
 				}
 			}
+			PortfolioMessage::PasteImage {
+				name,
+				image,
+				mouse,
+				parent_and_insert_index,
+			} => {
+				let create_document = self.documents.is_empty();
+
+				if create_document {
+					responses.add(PortfolioMessage::NewDocumentWithName {
+						name: name.clone().unwrap_or("Untitled Document".into()),
+					});
+				}
+
+				responses.add(DocumentMessage::PasteImage {
+					name,
+					image,
+					mouse,
+					parent_and_insert_index,
+				});
+
+				if create_document {
+					// 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(Message::StartBuffer);
+					responses.add(DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true });
+
+					// TODO: Figure out how to get StartBuffer to work here so we can delete this and use `DocumentMessage::ZoomCanvasToFitAll` instead
+					responses.add(Message::StartBuffer);
+					responses.add(FrontendMessage::TriggerDelayedZoomCanvasToFitAll);
+				}
+			}
+			PortfolioMessage::PasteSvg {
+				name,
+				svg,
+				mouse,
+				parent_and_insert_index,
+			} => {
+				let create_document = self.documents.is_empty();
+
+				if create_document {
+					responses.add(PortfolioMessage::NewDocumentWithName {
+						name: name.clone().unwrap_or("Untitled Document".into()),
+					});
+				}
+
+				responses.add(DocumentMessage::PasteSvg {
+					name,
+					svg,
+					mouse,
+					parent_and_insert_index,
+				});
+
+				if create_document {
+					// 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(Message::StartBuffer);
+					responses.add(DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true });
+
+					// TODO: Figure out how to get StartBuffer to work here so we can delete this and use `DocumentMessage::ZoomCanvasToFitAll` instead
+					responses.add(Message::StartBuffer);
+					responses.add(FrontendMessage::TriggerDelayedZoomCanvasToFitAll);
+				}
+			}
 			PortfolioMessage::PrevDocument => {
 				if let Some(active_document_id) = self.active_document_id {
 					let len = self.document_ids.len();
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 6ec5f05074..69dbc4136c 100644
--- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs
+++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs
@@ -36,7 +36,7 @@ pub fn new_image_layer(image_frame: ImageFrame<Color>, id: NodeId, parent: Layer
 /// Create a new group layer from an svg
 pub fn new_svg_layer(svg: String, transform: glam::DAffine2, id: NodeId, parent: LayerNodeIdentifier, responses: &mut VecDeque<Message>) -> LayerNodeIdentifier {
 	let insert_index = 0;
-	responses.add(DocumentMessage::ImportSvg {
+	responses.add(GraphOperationMessage::NewSvg {
 		id,
 		svg,
 		transform,
diff --git a/editor/src/messages/tool/tool_messages/brush_tool.rs b/editor/src/messages/tool/tool_messages/brush_tool.rs
index fffe3e280a..f2928c15b9 100644
--- a/editor/src/messages/tool/tool_messages/brush_tool.rs
+++ b/editor/src/messages/tool/tool_messages/brush_tool.rs
@@ -1,4 +1,5 @@
 use super::tool_prelude::*;
+use crate::consts::DEFAULT_BRUSH_SIZE;
 use crate::messages::portfolio::document::graph_operation::transform_utils::{get_current_normalized_pivot, get_current_transform};
 use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
 use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
@@ -41,7 +42,7 @@ pub struct BrushOptions {
 impl Default for BrushOptions {
 	fn default() -> Self {
 		Self {
-			diameter: 40.,
+			diameter: DEFAULT_BRUSH_SIZE,
 			hardness: 0.,
 			flow: 100.,
 			spacing: 20.,
diff --git a/editor/src/messages/tool/tool_messages/ellipse_tool.rs b/editor/src/messages/tool/tool_messages/ellipse_tool.rs
index 4e06a23e4c..3e174cce4d 100644
--- a/editor/src/messages/tool/tool_messages/ellipse_tool.rs
+++ b/editor/src/messages/tool/tool_messages/ellipse_tool.rs
@@ -1,4 +1,5 @@
 use super::tool_prelude::*;
+use crate::consts::DEFAULT_STROKE_WIDTH;
 use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
 use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
 use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
@@ -28,7 +29,7 @@ pub struct EllipseToolOptions {
 impl Default for EllipseToolOptions {
 	fn default() -> Self {
 		Self {
-			line_weight: 5.,
+			line_weight: DEFAULT_STROKE_WIDTH,
 			fill: ToolColorOptions::new_secondary(),
 			stroke: ToolColorOptions::new_primary(),
 		}
diff --git a/editor/src/messages/tool/tool_messages/freehand_tool.rs b/editor/src/messages/tool/tool_messages/freehand_tool.rs
index 6ea841cc94..110022bc22 100644
--- a/editor/src/messages/tool/tool_messages/freehand_tool.rs
+++ b/editor/src/messages/tool/tool_messages/freehand_tool.rs
@@ -1,4 +1,5 @@
 use super::tool_prelude::*;
+use crate::consts::DEFAULT_STROKE_WIDTH;
 use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
 use crate::messages::portfolio::document::overlays::utility_functions::path_endpoint_overlays;
 use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
@@ -31,7 +32,7 @@ pub struct FreehandOptions {
 impl Default for FreehandOptions {
 	fn default() -> Self {
 		Self {
-			line_weight: 5.,
+			line_weight: DEFAULT_STROKE_WIDTH,
 			fill: ToolColorOptions::new_none(),
 			stroke: ToolColorOptions::new_primary(),
 		}
diff --git a/editor/src/messages/tool/tool_messages/line_tool.rs b/editor/src/messages/tool/tool_messages/line_tool.rs
index 5808d62e49..e20751eb1b 100644
--- a/editor/src/messages/tool/tool_messages/line_tool.rs
+++ b/editor/src/messages/tool/tool_messages/line_tool.rs
@@ -1,5 +1,5 @@
 use super::tool_prelude::*;
-use crate::consts::LINE_ROTATE_SNAP_ANGLE;
+use crate::consts::{DEFAULT_STROKE_WIDTH, LINE_ROTATE_SNAP_ANGLE};
 use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
 use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
 use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
@@ -28,7 +28,7 @@ pub struct LineOptions {
 impl Default for LineOptions {
 	fn default() -> Self {
 		Self {
-			line_weight: 5.,
+			line_weight: DEFAULT_STROKE_WIDTH,
 			stroke: ToolColorOptions::new_primary(),
 		}
 	}
diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs
index 934c8a12d1..2ccd736dcb 100644
--- a/editor/src/messages/tool/tool_messages/pen_tool.rs
+++ b/editor/src/messages/tool/tool_messages/pen_tool.rs
@@ -1,6 +1,5 @@
 use super::tool_prelude::*;
-use crate::consts::HIDE_HANDLE_DISTANCE;
-use crate::consts::LINE_ROTATE_SNAP_ANGLE;
+use crate::consts::{DEFAULT_STROKE_WIDTH, HIDE_HANDLE_DISTANCE, LINE_ROTATE_SNAP_ANGLE};
 use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
 use crate::messages::portfolio::document::overlays::utility_functions::path_overlays;
 use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
@@ -34,7 +33,7 @@ pub struct PenOptions {
 impl Default for PenOptions {
 	fn default() -> Self {
 		Self {
-			line_weight: 5.,
+			line_weight: DEFAULT_STROKE_WIDTH,
 			fill: ToolColorOptions::new_secondary(),
 			stroke: ToolColorOptions::new_primary(),
 		}
diff --git a/editor/src/messages/tool/tool_messages/polygon_tool.rs b/editor/src/messages/tool/tool_messages/polygon_tool.rs
index 66b415e947..0ddb4cddd2 100644
--- a/editor/src/messages/tool/tool_messages/polygon_tool.rs
+++ b/editor/src/messages/tool/tool_messages/polygon_tool.rs
@@ -1,4 +1,5 @@
 use super::tool_prelude::*;
+use crate::consts::DEFAULT_STROKE_WIDTH;
 use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
 use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
 use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
@@ -33,7 +34,7 @@ impl Default for PolygonOptions {
 	fn default() -> Self {
 		Self {
 			vertices: 5,
-			line_weight: 5.,
+			line_weight: DEFAULT_STROKE_WIDTH,
 			fill: ToolColorOptions::new_secondary(),
 			stroke: ToolColorOptions::new_primary(),
 			polygon_type: PolygonType::Convex,
diff --git a/editor/src/messages/tool/tool_messages/rectangle_tool.rs b/editor/src/messages/tool/tool_messages/rectangle_tool.rs
index 87b91cf955..f79f21fd28 100644
--- a/editor/src/messages/tool/tool_messages/rectangle_tool.rs
+++ b/editor/src/messages/tool/tool_messages/rectangle_tool.rs
@@ -1,4 +1,5 @@
 use super::tool_prelude::*;
+use crate::consts::DEFAULT_STROKE_WIDTH;
 use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
 use crate::messages::portfolio::document::{graph_operation::utility_types::TransformIn, overlays::utility_types::OverlayContext};
 use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
@@ -27,7 +28,7 @@ pub struct RectangleToolOptions {
 impl Default for RectangleToolOptions {
 	fn default() -> Self {
 		Self {
-			line_weight: 5.,
+			line_weight: DEFAULT_STROKE_WIDTH,
 			fill: ToolColorOptions::new_secondary(),
 			stroke: ToolColorOptions::new_primary(),
 		}
diff --git a/editor/src/messages/tool/tool_messages/spline_tool.rs b/editor/src/messages/tool/tool_messages/spline_tool.rs
index ddf33b5396..e36dddfd32 100644
--- a/editor/src/messages/tool/tool_messages/spline_tool.rs
+++ b/editor/src/messages/tool/tool_messages/spline_tool.rs
@@ -1,5 +1,5 @@
 use super::tool_prelude::*;
-use crate::consts::DRAG_THRESHOLD;
+use crate::consts::{DEFAULT_STROKE_WIDTH, DRAG_THRESHOLD};
 use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
 use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
 use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
@@ -27,7 +27,7 @@ pub struct SplineOptions {
 impl Default for SplineOptions {
 	fn default() -> Self {
 		Self {
-			line_weight: 5.,
+			line_weight: DEFAULT_STROKE_WIDTH,
 			fill: ToolColorOptions::new_none(),
 			stroke: ToolColorOptions::new_primary(),
 		}
diff --git a/frontend/src/components/layout/LayoutCol.svelte b/frontend/src/components/layout/LayoutCol.svelte
index 08165dcbbb..b69b6bfabb 100644
--- a/frontend/src/components/layout/LayoutCol.svelte
+++ b/frontend/src/components/layout/LayoutCol.svelte
@@ -42,6 +42,7 @@
 	on:dragleave
 	on:dragover
 	on:dragstart
+	on:drop
 	on:mouseup
 	on:pointerdown
 	on:pointerenter
@@ -58,7 +59,6 @@ on:copy
 on:cut
 on:drag
 on:dragenter
-on:drop
 on:focus
 on:fullscreenchange
 on:fullscreenerror
diff --git a/frontend/src/components/layout/LayoutRow.svelte b/frontend/src/components/layout/LayoutRow.svelte
index ea622de02f..fad0220a76 100644
--- a/frontend/src/components/layout/LayoutRow.svelte
+++ b/frontend/src/components/layout/LayoutRow.svelte
@@ -42,6 +42,7 @@
 	on:dragleave
 	on:dragover
 	on:dragstart
+	on:drop
 	on:mouseup
 	on:pointerdown
 	on:pointerenter
@@ -58,7 +59,6 @@ on:copy
 on:cut
 on:drag
 on:dragenter
-on:drop
 on:focus
 on:fullscreenchange
 on:fullscreenerror
diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte
index 1868aa9538..8faa6b3fd5 100644
--- a/frontend/src/components/panels/Document.svelte
+++ b/frontend/src/components/panels/Document.svelte
@@ -118,23 +118,33 @@
 		};
 	})($document.toolShelfLayout.layout[0]);
 
-	function pasteFile(e: DragEvent) {
+	function dropFile(e: DragEvent) {
 		const { dataTransfer } = e;
+		const [x, y] = e.target instanceof Element && e.target.closest("[data-viewport]") ? [e.clientX, e.clientY] : [undefined, undefined];
 		if (!dataTransfer) return;
+
 		e.preventDefault();
 
 		Array.from(dataTransfer.items).forEach(async (item) => {
 			const file = item.getAsFile();
-			if (file?.type.includes("svg")) {
-				const svgData = await file.text();
-				editor.handle.pasteSvg(svgData, e.clientX, e.clientY);
+			if (!file) return;
 
+			if (file.type.includes("svg")) {
+				const svgData = await file.text();
+				editor.handle.pasteSvg(file.name, svgData, x, y);
 				return;
 			}
 
-			if (file?.type.startsWith("image")) {
+			if (file.type.startsWith("image")) {
 				const imageData = await extractPixelData(file);
-				editor.handle.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height, e.clientX, e.clientY);
+				editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height, x, y);
+				return;
+			}
+
+			if (file.name.endsWith(".graphite")) {
+				const content = await file.text();
+				editor.handle.openDocumentFile(file.name, content);
+				return;
 			}
 		});
 	}
@@ -426,7 +436,7 @@
 	});
 </script>
 
-<LayoutCol class="document">
+<LayoutCol class="document" on:dragover={(e) => e.preventDefault()} on:drop={dropFile}>
 	<LayoutRow class="options-bar" classes={{ "for-graph": $document.graphViewOverlayOpen }} scrollableX={true}>
 		{#if !$document.graphViewOverlayOpen}
 			<WidgetLayout layout={$document.documentModeLayout} />
@@ -482,7 +492,7 @@
 							y={cursorTop}
 						/>
 					{/if}
-					<div class="viewport" on:pointerdown={(e) => canvasPointerDown(e)} on:dragover={(e) => e.preventDefault()} on:drop={(e) => pasteFile(e)} bind:this={viewport} data-viewport>
+					<div class="viewport" on:pointerdown={(e) => canvasPointerDown(e)} bind:this={viewport} data-viewport>
 						<svg class="artboards" style:width={canvasWidthCSS} style:height={canvasHeightCSS}>
 							{@html artworkSvg}
 						</svg>
diff --git a/frontend/src/components/panels/Layers.svelte b/frontend/src/components/panels/Layers.svelte
index 3901b37567..ea6acd5bac 100644
--- a/frontend/src/components/panels/Layers.svelte
+++ b/frontend/src/components/panels/Layers.svelte
@@ -4,6 +4,7 @@
 	import { beginDraggingElement } from "@graphite/io-managers/drag";
 	import type { NodeGraphState } from "@graphite/state-providers/node-graph";
 	import { platformIsMac } from "@graphite/utility-functions/platform";
+	import { extractPixelData } from "@graphite/utility-functions/rasterization";
 	import type { Editor } from "@graphite/wasm-communication/editor";
 	import { defaultWidgetLayout, patchWidgetLayout, UpdateDocumentLayerDetails, UpdateDocumentLayerStructureJs, UpdateLayersPanelOptionsLayout } from "@graphite/wasm-communication/messages";
 	import type { DataBuffer, LayerPanelEntry } from "@graphite/wasm-communication/messages";
@@ -305,6 +306,8 @@
 	}
 
 	function updateInsertLine(event: DragEvent) {
+		if (!draggable) return;
+
 		// Stop the drag from being shown as cancelled
 		event.preventDefault();
 		dragInPanel = true;
@@ -312,13 +315,48 @@
 		if (list) draggingData = calculateDragIndex(list, event.clientY, draggingData?.select);
 	}
 
-	async function drop() {
-		if (draggingData && dragInPanel) {
-			const { select, insertParentId, insertIndex } = draggingData;
+	function drop(e: DragEvent) {
+		if (!draggingData) return;
+		const { select, insertParentId, insertIndex } = draggingData;
+
+		e.preventDefault();
+
+		if (e.dataTransfer) {
+			// Moving layers
+			if (e.dataTransfer.items.length === 0) {
+				if (draggable && dragInPanel) {
+					select?.();
+					editor.handle.moveLayerInTree(insertParentId, insertIndex);
+				}
+			}
+			// Importing files
+			else {
+				Array.from(e.dataTransfer.items).forEach(async (item) => {
+					const file = item.getAsFile();
+					if (!file) return;
+
+					if (file.type.includes("svg")) {
+						const svgData = await file.text();
+						editor.handle.pasteSvg(file.name, svgData, undefined, undefined, insertParentId, insertIndex);
+						return;
+					}
+
+					if (file.type.startsWith("image")) {
+						const imageData = await extractPixelData(file);
+						editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height, undefined, undefined, insertParentId, insertIndex);
+						return;
+					}
 
-			select?.();
-			editor.handle.moveLayerInTree(insertParentId, insertIndex);
+					// When we eventually have sub-documents, this should be changed to import the document instead of opening it in a separate tab
+					if (file.name.endsWith(".graphite")) {
+						const content = await file.text();
+						editor.handle.openDocumentFile(file.name, content);
+						return;
+					}
+				});
+			}
 		}
+
 		draggingData = undefined;
 		fakeHighlight = undefined;
 		dragInPanel = false;
@@ -369,7 +407,7 @@
 		<WidgetLayout layout={layersPanelOptionsLayout} />
 	</LayoutRow>
 	<LayoutRow class="list-area" scrollableY={true}>
-		<LayoutCol class="list" data-layer-panel bind:this={list} on:click={() => deselectAllLayers()} on:dragover={(e) => draggable && updateInsertLine(e)} on:dragend={() => draggable && drop()}>
+		<LayoutCol class="list" data-layer-panel bind:this={list} on:click={() => deselectAllLayers()} on:dragover={updateInsertLine} on:dragend={drop} on:drop={drop}>
 			{#each layers as listing, index}
 				<LayoutRow
 					class="layer"
diff --git a/frontend/src/components/window/workspace/Panel.svelte b/frontend/src/components/window/workspace/Panel.svelte
index 9b2fe7cb11..293a049cac 100644
--- a/frontend/src/components/window/workspace/Panel.svelte
+++ b/frontend/src/components/window/workspace/Panel.svelte
@@ -2,8 +2,6 @@
 	import Document from "@graphite/components/panels/Document.svelte";
 	import Layers from "@graphite/components/panels/Layers.svelte";
 	import Properties from "@graphite/components/panels/Properties.svelte";
-	import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
-	import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
 
 	const PANEL_COMPONENTS = {
 		Document,
@@ -18,11 +16,14 @@
 
 	import { platformIsMac, isEventSupported } from "@graphite/utility-functions/platform";
 
+	import { extractPixelData } from "@graphite/utility-functions/rasterization";
 	import type { Editor } from "@graphite/wasm-communication/editor";
 	import { type LayoutKeysGroup, type Key } from "@graphite/wasm-communication/messages";
 
 	import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
 	import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
+	import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
+	import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
 	import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
 	import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
 	import UserInputLabel from "@graphite/components/widgets/labels/UserInputLabel.svelte";
@@ -50,6 +51,35 @@
 		return reservedKey ? [CONTROL, ALT] : [CONTROL];
 	}
 
+	function dropFile(e: DragEvent) {
+		if (!e.dataTransfer) return;
+
+		e.preventDefault();
+
+		Array.from(e.dataTransfer.items).forEach(async (item) => {
+			const file = item.getAsFile();
+			if (!file) return;
+
+			if (file.type.includes("svg")) {
+				const svgData = await file.text();
+				editor.handle.pasteSvg(file.name, svgData);
+				return;
+			}
+
+			if (file.type.startsWith("image")) {
+				const imageData = await extractPixelData(file);
+				editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height);
+				return;
+			}
+
+			if (file.name.endsWith(".graphite")) {
+				const content = await file.text();
+				editor.handle.openDocumentFile(file.name, content);
+				return;
+			}
+		});
+	}
+
 	export async function scrollTabIntoView(newIndex: number) {
 		await tick();
 		tabElements[newIndex]?.div?.()?.scrollIntoView();
@@ -76,7 +106,7 @@
 						}
 					}}
 					on:mouseup={(e) => {
-						// Fallback for Safari:
+						// Middle mouse button click fallback for Safari:
 						// https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event#browser_compatibility
 						// The downside of using mouseup is that the mousedown didn't have to originate in the same element.
 						// A possible future improvement could save the target element during mousedown and check if it's the same here.
@@ -110,7 +140,7 @@
 		{#if panelType}
 			<svelte:component this={PANEL_COMPONENTS[panelType]} />
 		{:else}
-			<LayoutCol class="empty-panel">
+			<LayoutCol class="empty-panel" on:dragover={(e) => e.preventDefault()} on:drop={dropFile}>
 				<LayoutCol class="content">
 					<LayoutRow class="logotype">
 						<IconLabel icon="GraphiteLogotypeSolid" />
diff --git a/frontend/src/io-managers/drag.ts b/frontend/src/io-managers/drag.ts
index 7f713c0ec3..4d4264331c 100644
--- a/frontend/src/io-managers/drag.ts
+++ b/frontend/src/io-managers/drag.ts
@@ -11,9 +11,7 @@ export function createDragManager(): () => void {
 	// Return the destructor
 	return () => {
 		// We use setTimeout to sequence this drop after any potential users in the current call stack progression, since this will begin in an entirely new call stack later
-		setTimeout(() => {
-			document.removeEventListener("drop", clearDraggingElement);
-		}, 0);
+		setTimeout(() => document.removeEventListener("drop", clearDraggingElement), 0);
 	};
 }
 
diff --git a/frontend/src/io-managers/input.ts b/frontend/src/io-managers/input.ts
index 7e2dff6efc..5dbcaaa9c8 100644
--- a/frontend/src/io-managers/input.ts
+++ b/frontend/src/io-managers/input.ts
@@ -283,17 +283,21 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
 			}
 
 			const file = item.getAsFile();
+			if (!file) return;
 
-			if (file?.type === "svg") {
+			if (file.type.includes("svg")) {
 				const text = await file.text();
-				editor.handle.pasteSvg(text);
-
+				editor.handle.pasteSvg(file.name, text);
 				return;
 			}
 
-			if (file?.type.startsWith("image")) {
+			if (file.type.startsWith("image")) {
 				const imageData = await extractPixelData(file);
-				editor.handle.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height);
+				editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height);
+			}
+
+			if (file.name.endsWith(".graphite")) {
+				editor.handle.openDocumentFile(file.name, await file.text());
 			}
 		});
 	}
@@ -316,52 +320,63 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
 			if (!clipboardItems) throw new Error("Clipboard API unsupported");
 
 			// Read any layer data or images from the clipboard
-			Array.from(clipboardItems).forEach(async (item) => {
-				// Read plain text and, if it is a layer, pass it to the editor
-				if (item.types.includes("text/plain")) {
-					const blob = await item.getType("text/plain");
-					const reader = new FileReader();
-					reader.onload = () => {
-						const text = reader.result as string;
-
-						if (text.startsWith("graphite/layer: ")) {
-							editor.handle.pasteSerializedData(text.substring(16, text.length));
-						}
-					};
-					reader.readAsText(blob);
-				}
-
-				// Read an image from the clipboard and pass it to the editor to be loaded
-				const imageType = item.types.find((type) => type.startsWith("image/"));
-
-				if (imageType === "svg") {
-					const blob = await item.getType("text/plain");
-					const reader = new FileReader();
-					reader.onload = () => {
-						const text = reader.result as string;
-						editor.handle.pasteSvg(text);
-					};
-					reader.readAsText(blob);
-
-					return;
-				}
-
-				if (imageType) {
-					const blob = await item.getType(imageType);
-					const reader = new FileReader();
-					reader.onload = async () => {
-						if (reader.result instanceof ArrayBuffer) {
-							const imageData = await extractPixelData(new Blob([reader.result], { type: imageType }));
-							editor.handle.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height);
-						}
-					};
-					reader.readAsArrayBuffer(blob);
-				}
-			});
+			const success = await Promise.any(
+				Array.from(clipboardItems).map(async (item) => {
+					// Read plain text and, if it is a layer, pass it to the editor
+					if (item.types.includes("text/plain")) {
+						const blob = await item.getType("text/plain");
+						const reader = new FileReader();
+						reader.onload = () => {
+							const text = reader.result as string;
+
+							if (text.startsWith("graphite/layer: ")) {
+								editor.handle.pasteSerializedData(text.substring(16, text.length));
+							}
+						};
+						reader.readAsText(blob);
+						return true;
+					}
+
+					// Read an image from the clipboard and pass it to the editor to be loaded
+					const imageType = item.types.find((type) => type.startsWith("image/"));
+
+					// Import the actual SVG content if it's an SVG
+					if (imageType?.includes("svg")) {
+						const blob = await item.getType("text/plain");
+						const reader = new FileReader();
+						reader.onload = () => {
+							const text = reader.result as string;
+							editor.handle.pasteSvg(undefined, text);
+						};
+						reader.readAsText(blob);
+						return true;
+					}
+
+					// Import the bitmap image if it's an image
+					if (imageType) {
+						const blob = await item.getType(imageType);
+						const reader = new FileReader();
+						reader.onload = async () => {
+							if (reader.result instanceof ArrayBuffer) {
+								const imageData = await extractPixelData(new Blob([reader.result], { type: imageType }));
+								editor.handle.pasteImage(undefined, new Uint8Array(imageData.data), imageData.width, imageData.height);
+							}
+						};
+						reader.readAsArrayBuffer(blob);
+						return true;
+					}
+
+					// The API limits what kinds of data we can access, so we can get copied images and our text encodings of copied nodes, but not files (like
+					// .graphite or even image files). However, the user can paste those with Ctrl+V, which we recommend they in the error message that's shown to them.
+					return false;
+				}),
+			);
+
+			if (!success) throw new Error("No valid clipboard data");
 		} catch (err) {
 			const unsupported = stripIndents`
 				This browser does not support reading from the clipboard.
-				Use the keyboard shortcut to paste instead.
+				Use the standard keyboard shortcut to paste instead.
 				`;
 			const denied = stripIndents`
 				The browser's clipboard permission has been denied.
@@ -369,11 +384,16 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
 				Open the browser's website settings (usually accessible
 				just left of the URL) to allow this permission.
 				`;
+			const nothing = stripIndents`
+				No valid clipboard data was found. You may have better
+				luck pasting with the standard keyboard shortcut instead.
+				`;
 
 			const matchMessage = {
 				"clipboard-read": unsupported,
 				"Clipboard API unsupported": unsupported,
 				"Permission denied": denied,
+				"No valid clipboard data": nothing,
 			};
 			const message = Object.entries(matchMessage).find(([key]) => String(err).includes(key))?.[1] || String(err);
 
diff --git a/frontend/src/state-providers/document.ts b/frontend/src/state-providers/document.ts
index f5d4682820..2fe94185bb 100644
--- a/frontend/src/state-providers/document.ts
+++ b/frontend/src/state-providers/document.ts
@@ -92,11 +92,9 @@ export function createDocumentState(editor: Editor) {
 	});
 	editor.subscriptions.subscribeJsMessage(TriggerDelayedZoomCanvasToFitAll, () => {
 		// TODO: This is horribly hacky
-		setTimeout(() => editor.handle.zoomCanvasToFitAll(), 0);
-		setTimeout(() => editor.handle.zoomCanvasToFitAll(), 1);
-		setTimeout(() => editor.handle.zoomCanvasToFitAll(), 10);
-		setTimeout(() => editor.handle.zoomCanvasToFitAll(), 50);
-		setTimeout(() => editor.handle.zoomCanvasToFitAll(), 100);
+		[0, 1, 10, 50, 100, 200, 300, 400, 500].forEach((delay) => {
+			setTimeout(() => editor.handle.zoomCanvasToFitAll(), delay);
+		});
 	});
 
 	return {
diff --git a/frontend/src/state-providers/portfolio.ts b/frontend/src/state-providers/portfolio.ts
index c441fb694f..a80342b5c5 100644
--- a/frontend/src/state-providers/portfolio.ts
+++ b/frontend/src/state-providers/portfolio.ts
@@ -65,17 +65,22 @@ export function createPortfolioState(editor: Editor) {
 		editor.handle.openDocumentFile(data.filename, data.content);
 	});
 	editor.subscriptions.subscribeJsMessage(TriggerImport, async () => {
-		const data = await upload("image/*", "data");
+		const data = await upload("image/*", "both");
 
 		if (data.type.includes("svg")) {
-			const svg = new TextDecoder().decode(data.content);
-			editor.handle.pasteSvg(svg);
+			const svg = new TextDecoder().decode(data.content.data);
+			editor.handle.pasteSvg(data.filename, svg);
+			return;
+		}
 
+		// In case the user accidentally uploads a Graphite file, open it instead of failing to import it
+		if (data.filename.endsWith(".graphite")) {
+			editor.handle.openDocumentFile(data.filename, data.content.text);
 			return;
 		}
 
-		const imageData = await extractPixelData(new Blob([data.content], { type: data.type }));
-		editor.handle.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height);
+		const imageData = await extractPixelData(new Blob([data.content.data], { type: data.type }));
+		editor.handle.pasteImage(data.filename, new Uint8Array(imageData.data), imageData.width, imageData.height);
 	});
 	editor.subscriptions.subscribeJsMessage(TriggerDownloadTextFile, (triggerFileDownload) => {
 		downloadFileText(triggerFileDownload.name, triggerFileDownload.document);
diff --git a/frontend/src/utility-functions/files.ts b/frontend/src/utility-functions/files.ts
index fb22e7a0a2..a9d30f6736 100644
--- a/frontend/src/utility-functions/files.ts
+++ b/frontend/src/utility-functions/files.ts
@@ -22,7 +22,7 @@ export function downloadFileText(filename: string, text: string) {
 	downloadFileBlob(filename, blob);
 }
 
-export async function upload<T extends "text" | "data">(acceptedExtensions: string, textOrData: T): Promise<UploadResult<T>> {
+export async function upload<T extends "text" | "data" | "both">(acceptedExtensions: string, textOrData: T): Promise<UploadResult<T>> {
 	return new Promise<UploadResult<T>>((resolve, _) => {
 		const element = document.createElement("input");
 		element.type = "file";
@@ -36,7 +36,15 @@ export async function upload<T extends "text" | "data">(acceptedExtensions: stri
 
 					const filename = file.name;
 					const type = file.type;
-					const content = (textOrData === "text" ? await file.text() : new Uint8Array(await file.arrayBuffer())) as UploadResultType<T>;
+					const content = (
+						textOrData === "text"
+							? await file.text()
+							: textOrData === "data"
+								? new Uint8Array(await file.arrayBuffer())
+								: textOrData === "both"
+									? { text: await file.text(), data: new Uint8Array(await file.arrayBuffer()) }
+									: undefined
+					) as UploadResultType<T>;
 
 					resolve({ filename, type, content });
 				}
@@ -50,7 +58,7 @@ export async function upload<T extends "text" | "data">(acceptedExtensions: stri
 	});
 }
 export type UploadResult<T> = { filename: string; type: string; content: UploadResultType<T> };
-type UploadResultType<T> = T extends "text" ? string : T extends "data" ? Uint8Array : never;
+type UploadResultType<T> = T extends "text" ? string : T extends "data" ? Uint8Array : T extends "both" ? { text: string; data: Uint8Array } : never;
 
 export function blobToBase64(blob: Blob): Promise<string> {
 	return new Promise((resolve) => {
diff --git a/frontend/src/wasm-communication/messages.ts b/frontend/src/wasm-communication/messages.ts
index afe81ac821..7839a0d62f 100644
--- a/frontend/src/wasm-communication/messages.ts
+++ b/frontend/src/wasm-communication/messages.ts
@@ -129,7 +129,8 @@ export abstract class DocumentDetails {
 
 	readonly isSaved!: boolean;
 
-	readonly id!: bigint | string;
+	// This field must be provided by the subclass implementation
+	// readonly id!: bigint | string;
 
 	get displayName(): string {
 		return `${this.name}${this.isSaved ? "" : "*"}`;
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index c3f3f196c5..3a5b77e237 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -1,7 +1,7 @@
 {
 	"compilerOptions": {
-		"target": "ES2020",
-		"module": "esnext",
+		"target": "ESNext",
+		"module": "ESNext",
 		"strict": true,
 		"importHelpers": true,
 		"moduleResolution": "node",
@@ -18,7 +18,7 @@
 			"@graphite-frontend/*": ["./*"],
 			"@graphite/*": ["src/*"]
 		},
-		"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
+		"lib": ["ESNext", "DOM", "DOM.Iterable", "ScriptHost"]
 	},
 	"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.svelte", "*.ts", "*.js", "*.cjs"],
 	"exclude": ["node_modules"],
diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs
index b6baad82bc..4345c9092b 100644
--- a/frontend/wasm/src/editor_api.rs
+++ b/frontend/wasm/src/editor_api.rs
@@ -593,17 +593,55 @@ impl EditorHandle {
 
 	/// Pastes an image
 	#[wasm_bindgen(js_name = pasteImage)]
-	pub fn paste_image(&self, image_data: Vec<u8>, width: u32, height: u32, mouse_x: Option<f64>, mouse_y: Option<f64>) {
+	pub fn paste_image(
+		&self,
+		name: Option<String>,
+		image_data: Vec<u8>,
+		width: u32,
+		height: u32,
+		mouse_x: Option<f64>,
+		mouse_y: Option<f64>,
+		insert_parent_id: Option<u64>,
+		insert_index: Option<usize>,
+	) {
 		let mouse = mouse_x.and_then(|x| mouse_y.map(|y| (x, y)));
 		let image = graphene_core::raster::Image::from_image_data(&image_data, width, height);
-		let message = DocumentMessage::PasteImage { image, mouse };
+
+		let parent_and_insert_index = if let (Some(insert_parent_id), Some(insert_index)) = (insert_parent_id, insert_index) {
+			let insert_parent_id = NodeId(insert_parent_id);
+			let parent = LayerNodeIdentifier::new_unchecked(insert_parent_id);
+			Some((parent, insert_index))
+		} else {
+			None
+		};
+
+		let message = PortfolioMessage::PasteImage {
+			name,
+			image,
+			mouse,
+			parent_and_insert_index,
+		};
 		self.dispatch(message);
 	}
 
 	#[wasm_bindgen(js_name = pasteSvg)]
-	pub fn paste_svg(&self, svg: String, mouse_x: Option<f64>, mouse_y: Option<f64>) {
+	pub fn paste_svg(&self, name: Option<String>, svg: String, mouse_x: Option<f64>, mouse_y: Option<f64>, insert_parent_id: Option<u64>, insert_index: Option<usize>) {
 		let mouse = mouse_x.and_then(|x| mouse_y.map(|y| (x, y)));
-		let message = DocumentMessage::PasteSvg { svg, mouse };
+
+		let parent_and_insert_index = if let (Some(insert_parent_id), Some(insert_index)) = (insert_parent_id, insert_index) {
+			let insert_parent_id = NodeId(insert_parent_id);
+			let parent = LayerNodeIdentifier::new_unchecked(insert_parent_id);
+			Some((parent, insert_index))
+		} else {
+			None
+		};
+
+		let message = PortfolioMessage::PasteSvg {
+			name,
+			svg,
+			mouse,
+			parent_and_insert_index,
+		};
 		self.dispatch(message);
 	}
 
diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs
index 31a68019c1..cf08168291 100644
--- a/node-graph/gcore/src/vector/vector_nodes.rs
+++ b/node-graph/gcore/src/vector/vector_nodes.rs
@@ -141,7 +141,7 @@ async fn stroke<F: 'n + Send, T: Into<Option<Color>> + 'n + Send>(
 	)]
 	#[default(Color::BLACK)]
 	color: T,
-	#[default(5.)] weight: f64,
+	#[default(2.)] weight: f64,
 	dash_lengths: Vec<f64>,
 	dash_offset: f64,
 	line_cap: crate::vector::style::LineCap,
diff --git a/website/other/bezier-rs-demos/tsconfig.json b/website/other/bezier-rs-demos/tsconfig.json
index 297b6a7e31..4d4d94dd71 100644
--- a/website/other/bezier-rs-demos/tsconfig.json
+++ b/website/other/bezier-rs-demos/tsconfig.json
@@ -1,7 +1,7 @@
 {
 	"compilerOptions": {
-		"target": "ES2020",
-		"module": "esnext",
+		"target": "ESNext",
+		"module": "ESNext",
 		"strict": true,
 		"importHelpers": true,
 		"moduleResolution": "node",
@@ -14,7 +14,7 @@
 		"paths": {
 			"@/*": ["src/*"]
 		},
-		"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
+		"lib": ["ESNext", "DOM", "DOM.Iterable", "ScriptHost"]
 	},
 	"include": ["src/**/*.ts", "src/**/*.d.ts", "*.ts", "*.js", "*.cjs"],
 	"exclude": ["node_modules"],


Test patch: