Repository: GraphiteEditor/Graphite (language: rust)

Issue:
Text tool completly broken
The text tool is completely broken:
- The typing is always at the origin
- The bounding box doesn't disappear when finishing typing
- Text is only rendered properly after the document is panned
- etc.
SVG import is broken
File > Import for an SVG doesn't work anymore.


Discussion / hints:
Regression introduced in #1946


Fix patch:
diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs
index 32af59cb1c..533dde5b70 100644
--- a/editor/src/dispatcher.rs
+++ b/editor/src/dispatcher.rs
@@ -6,6 +6,7 @@ use graphene_core::text::Font;
 
 #[derive(Debug, Default)]
 pub struct Dispatcher {
+	buffered_queue: Option<Vec<VecDeque<Message>>>,
 	message_queues: Vec<VecDeque<Message>>,
 	pub responses: Vec<FrontendMessage>,
 	pub message_handlers: DispatcherMessageHandlers,
@@ -65,8 +66,18 @@ impl Dispatcher {
 
 	pub fn handle_message<T: Into<Message>>(&mut self, message: T) {
 		self.message_queues.push(VecDeque::from_iter([message.into()]));
-
 		while let Some(message) = self.message_queues.last_mut().and_then(VecDeque::pop_front) {
+			// Do not buffer the EndBuffer message
+			if !matches!(message, Message::EndBuffer(_)) {
+				if let Some(buffered_queue) = &mut self.buffered_queue {
+					// Store each message in a deque so that its children are added before future messages
+					let mut message_deque = VecDeque::new();
+					message_deque.push_back(message);
+					buffered_queue.push(message_deque);
+					continue;
+				}
+			}
+
 			// Skip processing of this message if it will be processed later (at the end of the shallowest level queue)
 			if SIDE_EFFECT_FREE_MESSAGES.contains(&message.to_discriminant()) {
 				let already_in_queue = self.message_queues.first().filter(|queue| queue.contains(&message)).is_some();
@@ -90,6 +101,25 @@ impl Dispatcher {
 
 			// Process the action by forwarding it to the relevant message handler, or saving the FrontendMessage to be sent to the frontend
 			match message {
+				Message::StartBuffer => {
+					self.buffered_queue = Some(std::mem::take(&mut self.message_queues));
+				}
+				Message::EndBuffer(render_metadata) => {
+					// The buffered vec is added before the metadata messages, because the end of the vec is processed first
+					if let Some(buffered_queue) = self.buffered_queue.take() {
+						self.message_queues.extend(buffered_queue);
+					};
+
+					let graphene_std::renderer::RenderMetadata { footprints, click_targets } = render_metadata;
+
+					let mut update_upstream_transform = VecDeque::new();
+					update_upstream_transform.push_back(DocumentMessage::UpdateUpstreamTransforms { upstream_transforms: footprints }.into());
+					self.message_queues.push(update_upstream_transform);
+
+					let mut update_click_targets = VecDeque::new();
+					update_click_targets.push_back(DocumentMessage::UpdateClickTargets { click_targets }.into());
+					self.message_queues.push(update_click_targets);
+				}
 				Message::NoOp => {}
 				Message::Init => {
 					// Load persistent data from the browser database
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 cacb4295ce..efad5063ea 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
@@ -30,11 +30,12 @@ impl MessageHandler<NewDocumentDialogMessage, ()> for NewDocumentDialogMessageHa
 						id: NodeId(generate_uuid()),
 						artboard: graphene_core::Artboard::new(IVec2::ZERO, self.dimensions.as_ivec2()),
 					});
-					responses.add(FrontendMessage::TriggerDelayedZoomCanvasToFitAll);
 				}
 
 				responses.add(NodeGraphMessage::RunDocumentGraph);
 				responses.add(NodeGraphMessage::UpdateNewNodeGraph);
+				responses.add(Message::StartBuffer);
+				responses.add(FrontendMessage::TriggerDelayedZoomCanvasToFitAll);
 			}
 		}
 
diff --git a/editor/src/messages/message.rs b/editor/src/messages/message.rs
index cb6de732ab..1c84c98695 100644
--- a/editor/src/messages/message.rs
+++ b/editor/src/messages/message.rs
@@ -8,6 +8,8 @@ pub enum Message {
 	NoOp,
 	Init,
 	Batched(Box<[Message]>),
+	StartBuffer,
+	EndBuffer(graphene_std::renderer::RenderMetadata),
 
 	#[child]
 	Broadcast(BroadcastMessage),
diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs
index d7da13f138..cfe6b08108 100644
--- a/editor/src/messages/portfolio/document/document_message_handler.rs
+++ b/editor/src/messages/portfolio/document/document_message_handler.rs
@@ -1028,7 +1028,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
 					return;
 				}
 
-				self.document_undo_history.pop_back();
+				self.undo(ipp, responses);
 				self.network_interface.finish_transaction();
 				responses.add(OverlaysMessage::Draw);
 			}
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 43ce424899..f4b4a38b31 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
@@ -67,10 +67,8 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
 				transform_in,
 				skip_rerender,
 			} => {
-				let parent_transform = network_interface.document_metadata().downstream_transform_to_viewport(layer);
-				let current_transform = Some(network_interface.document_metadata().transform_to_viewport(layer));
 				if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
-					modify_inputs.transform_set(transform, transform_in, parent_transform, current_transform, skip_rerender);
+					modify_inputs.transform_set(transform, transform_in, skip_rerender);
 				}
 			}
 			GraphOperationMessage::TransformSetPivot { layer, pivot } => {
@@ -191,6 +189,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
 				let layer = modify_inputs.create_layer(id);
 				modify_inputs.insert_text(text, font, size, layer);
 				network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
+				responses.add(GraphOperationMessage::StrokeSet { layer, stroke: Stroke::default() });
 				responses.add(NodeGraphMessage::RunDocumentGraph);
 			}
 			GraphOperationMessage::ResizeArtboard { layer, location, dimensions } => {
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 01d710758a..1fdb0859ce 100644
--- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs
+++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs
@@ -15,6 +15,7 @@ use graphene_core::vector::{PointId, VectorModificationType};
 use graphene_core::{Artboard, Color};
 
 use glam::{DAffine2, DVec2, IVec2};
+use graphene_std::vector::VectorData;
 
 #[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
 pub enum TransformIn {
@@ -145,9 +146,12 @@ impl<'a> ModifyInputsContext<'a> {
 	}
 
 	pub fn insert_vector_data(&mut self, subpaths: Vec<Subpath<PointId>>, layer: LayerNodeIdentifier) {
+		let vector_data = VectorData::from_subpaths(subpaths, true);
+
 		let path = resolve_document_node_type("Path")
 			.expect("Path node does not exist")
-			.node_template_input_override([Some(NodeInput::value(TaggedValue::Subpaths(subpaths), false))]);
+			.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();
@@ -195,8 +199,6 @@ impl<'a> ModifyInputsContext<'a> {
 		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, &[]);
-
-		self.responses.add(NodeGraphMessage::RunDocumentGraph);
 	}
 
 	pub fn insert_image_data(&mut self, image_frame: ImageFrame<Color>, layer: LayerNodeIdentifier) {
@@ -334,22 +336,15 @@ impl<'a> ModifyInputsContext<'a> {
 		}
 	}
 
-	pub fn transform_set(&mut self, mut transform: DAffine2, transform_in: TransformIn, parent_transform: DAffine2, current_transform: Option<DAffine2>, skip_rerender: bool) {
-		let Some(transform_node_id) = self.existing_node_id("Transform") else { return };
-		let upstream_transform = self.network_interface.document_metadata().upstream_transform(transform_node_id);
-		let to = match transform_in {
-			TransformIn::Local => DAffine2::IDENTITY,
-			TransformIn::Scope { scope } => scope * parent_transform,
-			TransformIn::Viewport => parent_transform,
+	pub fn transform_set(&mut self, transform: DAffine2, transform_in: TransformIn, skip_rerender: bool) {
+		let final_transform = match transform_in {
+			TransformIn::Local => DAffine2::IDENTITY * transform,
+			TransformIn::Scope { scope } => scope * transform,
+			TransformIn::Viewport => self.network_interface.document_metadata().downstream_transform_to_viewport(self.layer_node.unwrap()).inverse() * transform,
 		};
 
-		if current_transform
-			.filter(|transform| transform.matrix2.determinant() != 0. && upstream_transform.matrix2.determinant() != 0.)
-			.is_some()
-		{
-			transform *= upstream_transform.inverse();
-		}
-		let final_transform = to.inverse() * transform;
+		let Some(transform_node_id) = self.existing_node_id("Transform") else { return };
+
 		transform_utils::update_transform(self.network_interface, &transform_node_id, final_transform);
 
 		self.responses.add(PropertiesPanelMessage::Refresh);
diff --git a/editor/src/messages/tool/tool_messages/artboard_tool.rs b/editor/src/messages/tool/tool_messages/artboard_tool.rs
index 263a1bd416..03fa0773dc 100644
--- a/editor/src/messages/tool/tool_messages/artboard_tool.rs
+++ b/editor/src/messages/tool/tool_messages/artboard_tool.rs
@@ -196,6 +196,19 @@ impl ArtboardToolData {
 			location: position.round().as_ivec2(),
 			dimensions: size.round().as_ivec2(),
 		});
+
+		// TODO: Resize artboard children when resizing left/top edges so that they stay in the same viewport space
+		// let old_top_left = bounds.bounds[0].round().as_ivec2();
+		// let new_top_left = position.round().as_ivec2();
+		// let top_left_delta = new_top_left - old_top_left;
+		// if top_left_delta != IVec2::ZERO {
+		// 	responses.add(GraphOperationMessage::TransformChange {
+		// 		layer: self.selected_artboard.unwrap(),
+		// 		transform: DAffine2::from_translation((-top_left_delta).into()),
+		// 		transform_in: TransformIn::Local,
+		// 		skip_rerender: false,
+		// 	});
+		// }
 	}
 }
 
diff --git a/editor/src/messages/tool/tool_messages/ellipse_tool.rs b/editor/src/messages/tool/tool_messages/ellipse_tool.rs
index 690d13f94b..4e06a23e4c 100644
--- a/editor/src/messages/tool/tool_messages/ellipse_tool.rs
+++ b/editor/src/messages/tool/tool_messages/ellipse_tool.rs
@@ -206,6 +206,7 @@ impl Fsm for EllipseToolFsmState {
 				let nodes = vec![(NodeId(0), node)];
 
 				let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, document.new_layer_parent(true), responses);
+				responses.add(Message::StartBuffer);
 				responses.add(GraphOperationMessage::TransformSet {
 					layer,
 					transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position),
diff --git a/editor/src/messages/tool/tool_messages/freehand_tool.rs b/editor/src/messages/tool/tool_messages/freehand_tool.rs
index 5fd2f313df..6ea841cc94 100644
--- a/editor/src/messages/tool/tool_messages/freehand_tool.rs
+++ b/editor/src/messages/tool/tool_messages/freehand_tool.rs
@@ -229,15 +229,11 @@ impl Fsm for FreehandToolFsmState {
 				let nodes = vec![(NodeId(0), node)];
 
 				let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, parent, responses);
+				responses.add(Message::StartBuffer);
 				tool_options.fill.apply_fill(layer, responses);
 				tool_options.stroke.apply_stroke(tool_data.weight, layer, responses);
 				tool_data.layer = Some(layer);
 
-				let transform = document.metadata().transform_to_viewport(parent);
-				let position = transform.inverse().transform_point2(input.mouse.position);
-
-				extend_path_with_next_segment(tool_data, position, responses);
-
 				FreehandToolFsmState::Drawing
 			}
 			(FreehandToolFsmState::Drawing, FreehandToolMessage::PointerMove) => {
diff --git a/editor/src/messages/tool/tool_messages/line_tool.rs b/editor/src/messages/tool/tool_messages/line_tool.rs
index 2943193ee8..5808d62e49 100644
--- a/editor/src/messages/tool/tool_messages/line_tool.rs
+++ b/editor/src/messages/tool/tool_messages/line_tool.rs
@@ -188,6 +188,7 @@ impl Fsm for LineToolFsmState {
 				let nodes = vec![(NodeId(0), node)];
 
 				let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, document.new_layer_parent(false), responses);
+				responses.add(Message::StartBuffer);
 				responses.add(GraphOperationMessage::TransformSet {
 					layer,
 					transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position),
diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs
index 2821cd7f2c..934c8a12d1 100644
--- a/editor/src/messages/tool/tool_messages/pen_tool.rs
+++ b/editor/src/messages/tool/tool_messages/pen_tool.rs
@@ -525,21 +525,11 @@ impl Fsm for PenToolFsmState {
 					tool_data.layer = Some(layer);
 					tool_data.next_point = position;
 					tool_data.next_handle_start = position;
-				} else {
-					// New path layer
-					let node_type = resolve_document_node_type("Path").expect("Path node does not exist");
-					let nodes = vec![(NodeId(0), node_type.default_node_template())];
-
-					let parent = document.new_layer_parent(true);
-					let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, parent, responses);
-					tool_options.fill.apply_fill(layer, responses);
-					tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
-					tool_data.layer = Some(layer);
-
+				} else if let Some(layer) = tool_data.layer {
+					// Add the first point to a new layer
 					// Generate first point
 					let id = PointId::generate();
-					let transform = document.metadata().transform_to_document(parent);
-					let pos = transform.inverse().transform_point2(snapped.snapped_point_document);
+					let pos = document.metadata().transform_to_viewport(layer).inverse().transform_point2(viewport);
 					let modification_type = VectorModificationType::InsertPoint { id, position: pos };
 					responses.add(GraphOperationMessage::Vector { layer, modification_type });
 					tool_data.add_point(LastPoint {
@@ -550,9 +540,21 @@ impl Fsm for PenToolFsmState {
 					});
 					tool_data.next_point = pos;
 					tool_data.next_handle_start = pos;
+				} else {
+					// New path layer
+					let node_type = resolve_document_node_type("Path").expect("Path node does not exist");
+					let nodes = vec![(NodeId(0), node_type.default_node_template())];
+
+					let parent = document.new_layer_parent(true);
+					let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, parent, responses);
+					tool_options.fill.apply_fill(layer, responses);
+					tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
+					tool_data.layer = Some(layer);
+					responses.add(Message::StartBuffer);
+					responses.add(PenToolMessage::DragStart);
+					return PenToolFsmState::Ready;
 				}
 				tool_data.handle_end = None;
-
 				// Enter the dragging handle state while the mouse is held down, allowing the user to move the mouse and position the handle
 				PenToolFsmState::DraggingHandle
 			}
diff --git a/editor/src/messages/tool/tool_messages/polygon_tool.rs b/editor/src/messages/tool/tool_messages/polygon_tool.rs
index 96e076c78b..66b415e947 100644
--- a/editor/src/messages/tool/tool_messages/polygon_tool.rs
+++ b/editor/src/messages/tool/tool_messages/polygon_tool.rs
@@ -265,6 +265,7 @@ impl Fsm for PolygonToolFsmState {
 				let nodes = vec![(NodeId(0), node)];
 
 				let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, document.new_layer_parent(false), responses);
+				responses.add(Message::StartBuffer);
 				responses.add(GraphOperationMessage::TransformSet {
 					layer,
 					transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position),
diff --git a/editor/src/messages/tool/tool_messages/rectangle_tool.rs b/editor/src/messages/tool/tool_messages/rectangle_tool.rs
index 7684c6b264..87b91cf955 100644
--- a/editor/src/messages/tool/tool_messages/rectangle_tool.rs
+++ b/editor/src/messages/tool/tool_messages/rectangle_tool.rs
@@ -212,6 +212,7 @@ impl Fsm for RectangleToolFsmState {
 				let nodes = vec![(NodeId(0), node)];
 
 				let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, document.new_layer_parent(true), responses);
+				responses.add(Message::StartBuffer);
 				responses.add(GraphOperationMessage::TransformSet {
 					layer,
 					transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position),
diff --git a/editor/src/messages/tool/tool_messages/spline_tool.rs b/editor/src/messages/tool/tool_messages/spline_tool.rs
index da6558db57..ddf33b5396 100644
--- a/editor/src/messages/tool/tool_messages/spline_tool.rs
+++ b/editor/src/messages/tool/tool_messages/spline_tool.rs
@@ -205,14 +205,6 @@ impl Fsm for SplineToolFsmState {
 				responses.add(DocumentMessage::DeselectAllLayers);
 
 				let parent = document.new_layer_parent(true);
-				let transform = document.metadata().transform_to_viewport(parent);
-
-				let snapped_position = input.mouse.position;
-
-				let pos = transform.inverse().transform_point2(snapped_position);
-
-				tool_data.points.push(pos);
-				tool_data.next_point = pos;
 
 				tool_data.weight = tool_options.line_weight;
 
@@ -225,6 +217,8 @@ impl Fsm for SplineToolFsmState {
 				tool_options.stroke.apply_stroke(tool_data.weight, layer, responses);
 				tool_data.layer = Some(layer);
 
+				responses.add(Message::StartBuffer);
+
 				SplineToolFsmState::Drawing
 			}
 			(SplineToolFsmState::Drawing, SplineToolMessage::DragStop) => {
@@ -237,11 +231,9 @@ impl Fsm for SplineToolFsmState {
 				let transform = document.metadata().transform_to_viewport(layer);
 				let pos = transform.inverse().transform_point2(snapped_position);
 
-				if let Some(last_pos) = tool_data.points.last() {
-					if last_pos.distance(pos) > DRAG_THRESHOLD {
-						tool_data.points.push(pos);
-						tool_data.next_point = pos;
-					}
+				if tool_data.points.last().map_or(true, |last_pos| last_pos.distance(pos) > DRAG_THRESHOLD) {
+					tool_data.points.push(pos);
+					tool_data.next_point = pos;
 				}
 
 				update_spline(document, tool_data, true, responses);
diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs
index a68afe7469..ab0540d834 100644
--- a/editor/src/messages/tool/tool_messages/text_tool.rs
+++ b/editor/src/messages/tool/tool_messages/text_tool.rs
@@ -213,11 +213,7 @@ struct TextToolData {
 
 impl TextToolData {
 	/// Set the editing state of the currently modifying layer
-	fn set_editing(&self, editable: bool, font_cache: &FontCache, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
-		if let Some(node_id) = graph_modification_utils::get_fill_id(self.layer, &document.network_interface) {
-			responses.add(NodeGraphMessage::SetVisibility { node_id, visible: !editable });
-		}
-
+	fn set_editing(&self, editable: bool, font_cache: &FontCache, responses: &mut VecDeque<Message>) {
 		if let Some(editing_text) = self.editing_text.as_ref().filter(|_| editable) {
 			responses.add(FrontendMessage::DisplayEditableTextbox {
 				text: editing_text.text.clone(),
@@ -229,6 +225,8 @@ impl TextToolData {
 			});
 		} else {
 			responses.add(FrontendMessage::DisplayRemoveEditableTextbox);
+			// Clear all selected nodes when no longer editing
+			responses.add(NodeGraphMessage::SelectedNodesSet { nodes: Vec::new() });
 		}
 	}
 
@@ -253,17 +251,23 @@ impl TextToolData {
 		}
 
 		if tool_state == TextToolFsmState::Editing {
-			self.set_editing(false, font_cache, document, responses);
+			self.set_editing(false, font_cache, responses);
 		}
 
 		self.layer = layer;
-		self.load_layer_text_node(document);
-
-		responses.add(DocumentMessage::AddTransaction);
+		if self.load_layer_text_node(document).is_some() {
+			responses.add(DocumentMessage::AddTransaction);
 
-		self.set_editing(true, font_cache, document, responses);
+			self.set_editing(true, font_cache, responses);
 
-		responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![self.layer.to_node()] });
+			responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![self.layer.to_node()] });
+			// Make the rendered text invisible while editing
+			responses.add(NodeGraphMessage::SetInput {
+				input_connector: InputConnector::node(graph_modification_utils::get_text_id(self.layer, &document.network_interface).unwrap(), 1),
+				input: NodeInput::value(TaggedValue::String("".to_string()), false),
+			});
+			responses.add(NodeGraphMessage::RunDocumentGraph);
+		};
 	}
 
 	fn interact(
@@ -294,6 +298,7 @@ impl TextToolData {
 				parent: document.new_layer_parent(true),
 				insert_index: 0,
 			});
+			responses.add(Message::StartBuffer);
 			responses.add(GraphOperationMessage::FillSet {
 				layer: self.layer,
 				fill: if editing_text.color.is_some() { Fill::Solid(editing_text.color.unwrap()) } else { Fill::None },
@@ -305,14 +310,15 @@ impl TextToolData {
 				skip_rerender: true,
 			});
 
-			self.set_editing(true, font_cache, document, responses);
+			self.set_editing(true, font_cache, responses);
 
 			responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![self.layer.to_node()] });
 
+			responses.add(NodeGraphMessage::RunDocumentGraph);
 			TextToolFsmState::Editing
 		} else {
 			// Removing old text as editable
-			self.set_editing(false, font_cache, document, responses);
+			self.set_editing(false, font_cache, responses);
 
 			TextToolFsmState::Ready
 		}
@@ -404,7 +410,7 @@ impl Fsm for TextToolFsmState {
 			}
 			(state, TextToolMessage::Abort) => {
 				if state == TextToolFsmState::Editing {
-					tool_data.set_editing(false, font_cache, document, responses);
+					tool_data.set_editing(false, font_cache, responses);
 				}
 
 				TextToolFsmState::Ready
@@ -415,12 +421,13 @@ impl Fsm for TextToolFsmState {
 				TextToolFsmState::Editing
 			}
 			(TextToolFsmState::Editing, TextToolMessage::TextChange { new_text }) => {
+				tool_data.set_editing(false, font_cache, responses);
+
 				responses.add(NodeGraphMessage::SetInput {
 					input_connector: InputConnector::node(graph_modification_utils::get_text_id(tool_data.layer, &document.network_interface).unwrap(), 1),
 					input: NodeInput::value(TaggedValue::String(new_text), false),
 				});
-
-				tool_data.set_editing(false, font_cache, document, responses);
+				responses.add(NodeGraphMessage::RunDocumentGraph);
 
 				TextToolFsmState::Ready
 			}
diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs
index 695a98b422..a5a1ff7a8b 100644
--- a/editor/src/node_graph_executor.rs
+++ b/editor/src/node_graph_executor.rs
@@ -15,7 +15,8 @@ use graphene_core::renderer::{RenderSvgSegmentList, SvgSegment};
 use graphene_core::text::FontCache;
 use graphene_core::transform::Footprint;
 use graphene_core::vector::style::ViewMode;
-use graphene_std::renderer::{format_transform_matrix, RenderMetadata};
+use graphene_std::renderer::format_transform_matrix;
+use graphene_std::vector::VectorData;
 use graphene_std::wasm_application_io::{WasmApplicationIo, WasmEditorApi};
 use interpreted_executor::dynamic_executor::{DynamicExecutor, IntrospectError, ResolvedDocumentNodeTypesDelta};
 
@@ -44,6 +45,7 @@ pub struct NodeRuntime {
 	// TODO: Remove, it doesn't need to be persisted anymore
 	/// The current renders of the thumbnails for layer nodes.
 	thumbnail_renders: HashMap<NodeId, Vec<SvgSegment>>,
+	vector_modify: HashMap<NodeId, VectorData>,
 }
 
 /// Messages passed from the editor thread to the node runtime thread.
@@ -74,6 +76,7 @@ pub struct ExecutionResponse {
 	result: Result<TaggedValue, String>,
 	responses: VecDeque<FrontendMessage>,
 	transform: DAffine2,
+	vector_modify: HashMap<NodeId, VectorData>,
 }
 
 pub struct CompilationResponse {
@@ -131,6 +134,7 @@ impl NodeRuntime {
 			monitor_nodes: Vec::new(),
 
 			thumbnail_renders: Default::default(),
+			vector_modify: Default::default(),
 		}
 	}
 
@@ -203,6 +207,7 @@ impl NodeRuntime {
 
 					let result = self.execute_network(render_config).await;
 					let mut responses = VecDeque::new();
+					// TODO: Only process monitor nodes if the graph has changed, not when only the Footprint changes
 					self.process_monitor_nodes(&mut responses, self.update_thumbnails);
 					self.update_thumbnails = false;
 
@@ -211,6 +216,7 @@ impl NodeRuntime {
 						result,
 						responses,
 						transform,
+						vector_modify: self.vector_modify.clone(),
 					});
 				}
 			}
@@ -282,8 +288,18 @@ impl NodeRuntime {
 
 			if let Some(io) = introspected_data.downcast_ref::<IORecord<Footprint, graphene_core::GraphicElement>>() {
 				Self::process_graphic_element(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses, update_thumbnails)
+			} else if let Some(io) = introspected_data.downcast_ref::<IORecord<(), graphene_core::GraphicElement>>() {
+				Self::process_graphic_element(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses, update_thumbnails)
 			} else if let Some(io) = introspected_data.downcast_ref::<IORecord<Footprint, graphene_core::Artboard>>() {
 				Self::process_graphic_element(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses, update_thumbnails)
+			} else if let Some(io) = introspected_data.downcast_ref::<IORecord<(), graphene_core::Artboard>>() {
+				Self::process_graphic_element(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses, update_thumbnails)
+			}
+			// Insert the vector modify if we are dealing with vector data
+			else if let Some(record) = introspected_data.downcast_ref::<IORecord<Footprint, VectorData>>() {
+				self.vector_modify.insert(parent_network_node_id, record.output.clone());
+			} else if let Some(record) = introspected_data.downcast_ref::<IORecord<(), VectorData>>() {
+				self.vector_modify.insert(parent_network_node_id, record.output.clone());
 			}
 		}
 	}
@@ -535,6 +551,7 @@ impl NodeGraphExecutor {
 						result,
 						responses: existing_responses,
 						transform,
+						vector_modify,
 					} = execution_response;
 
 					responses.add(OverlaysMessage::Draw);
@@ -550,6 +567,7 @@ impl NodeGraphExecutor {
 					};
 
 					responses.extend(existing_responses.into_iter().map(Into::into));
+					document.network_interface.update_vector_modify(vector_modify);
 
 					let execution_context = self.futures.remove(&execution_id).ok_or_else(|| "Invalid generation ID".to_string())?;
 					if let Some(export_config) = execution_context.export_config {
@@ -614,12 +632,6 @@ impl NodeGraphExecutor {
 	fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, transform: DAffine2, responses: &mut VecDeque<Message>) -> Result<(), String> {
 		match node_graph_output {
 			TaggedValue::RenderOutput(render_output) => {
-				let RenderMetadata {
-					footprints,
-					click_targets,
-					vector_data,
-				} = render_output.metadata;
-
 				match render_output.data {
 					graphene_std::wasm_application_io::RenderOutputType::Svg(svg) => {
 						// Send to frontend
@@ -638,9 +650,8 @@ impl NodeGraphExecutor {
 						return Err(format!("Invalid node graph output type: {:#?}", render_output.data));
 					}
 				}
-				responses.add(DocumentMessage::UpdateUpstreamTransforms { upstream_transforms: footprints });
-				responses.add(DocumentMessage::UpdateClickTargets { click_targets });
-				responses.add(DocumentMessage::UpdateVectorModify { vector_modify: vector_data });
+
+				responses.add(Message::EndBuffer(render_output.metadata));
 				responses.add(DocumentMessage::RenderScrollbars);
 				responses.add(DocumentMessage::RenderRulers);
 				responses.add(OverlaysMessage::Draw);
diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs
index 2f37e2bf27..de459e1834 100644
--- a/node-graph/gcore/src/graphic_element/renderer.rs
+++ b/node-graph/gcore/src/graphic_element/renderer.rs
@@ -270,12 +270,13 @@ pub fn to_transform(transform: DAffine2) -> usvg::Transform {
 	usvg::Transform::from_row(cols[0] as f32, cols[1] as f32, cols[2] as f32, cols[3] as f32, cols[4] as f32, cols[5] as f32)
 }
 
+// TODO: Click targets can be removed from the render output, since the vector data is available in the vector modify data from Monitor nodes.
+// This will require that the transform for child layers into that layer space be calculated, or it could be returned from the RenderOutput instead of click targets.
 #[derive(Debug, Clone, PartialEq, DynAny)]
 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 pub struct RenderMetadata {
 	pub footprints: HashMap<NodeId, (Footprint, DAffine2)>,
 	pub click_targets: HashMap<NodeId, Vec<ClickTarget>>,
-	pub vector_data: HashMap<NodeId, VectorData>,
 }
 
 pub trait GraphicElementRendered {
@@ -454,10 +455,14 @@ impl GraphicElementRendered for VectorData {
 				}
 				subpath
 			};
-			metadata
-				.click_targets
-				.insert(element_id, self.stroke_bezier_paths().map(fill).map(|subpath| ClickTarget::new(subpath, stroke_width)).collect());
-			metadata.vector_data.insert(element_id, self.clone());
+
+			let click_targets = self
+				.stroke_bezier_paths()
+				.map(fill)
+				.map(|subpath| ClickTarget::new(subpath, stroke_width))
+				.collect::<Vec<ClickTarget>>();
+
+			metadata.click_targets.insert(element_id, click_targets);
 		}
 
 		if let Some(upstream_graphic_group) = &self.upstream_graphic_group {
@@ -606,14 +611,19 @@ impl GraphicElementRendered for Artboard {
 			"g",
 			// Group tag attributes
 			|attributes| {
+				let matrix = format_transform_matrix(self.transform());
+				if !matrix.is_empty() {
+					attributes.push("transform", matrix);
+				}
+
 				if self.clip {
 					let id = format!("artboard-{}", generate_uuid());
 					let selector = format!("url(#{id})");
 
 					write!(
 						&mut attributes.0.svg_defs,
-						r##"<clipPath id="{id}"><rect x="0" y="0" width="{}" height="{}" /></clipPath>"##,
-						self.dimensions.x, self.dimensions.y
+						r##"<clipPath id="{id}"><rect x="0" y="0" width="{}" height="{}"/></clipPath>"##,
+						self.dimensions.x, self.dimensions.y,
 					)
 					.unwrap();
 					attributes.push("clip-path", selector);
@@ -635,13 +645,13 @@ impl GraphicElementRendered for Artboard {
 		}
 	}
 
-	fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
+	fn collect_metadata(&self, metadata: &mut RenderMetadata, mut footprint: Footprint, element_id: Option<NodeId>) {
 		if let Some(element_id) = element_id {
 			let subpath = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2());
 			metadata.click_targets.insert(element_id, vec![ClickTarget::new(subpath, 0.)]);
 			metadata.footprints.insert(element_id, (footprint, DAffine2::from_translation(self.location.as_dvec2())));
 		}
-
+		footprint.transform *= self.transform();
 		self.graphic_group.collect_metadata(metadata, footprint, None);
 	}
 
diff --git a/node-graph/gstd/src/wasm_application_io.rs b/node-graph/gstd/src/wasm_application_io.rs
index 20ea989fe1..0140d43c61 100644
--- a/node-graph/gstd/src/wasm_application_io.rs
+++ b/node-graph/gstd/src/wasm_application_io.rs
@@ -225,7 +225,6 @@ async fn render<'a: 'n, T: 'n + GraphicElementRendered + WasmNotSend>(
 	let mut metadata = RenderMetadata {
 		footprints: HashMap::new(),
 		click_targets: HashMap::new(),
-		vector_data: HashMap::new(),
 	};
 	data.collect_metadata(&mut metadata, footprint, None);
 


Test patch: