Repository: GraphiteEditor/Graphite (language: rust)

Issue:
[Crash Report] Brush tool on top of shapes crashes
**Describe the Crash**
Using brush tool ontop of stuff 

**Steps To Reproduce**
Describe precisely how the crash occurred, step by step, starting with a new editor window.
1. Open a local build of graphite with debug assertions enabled.
2.  Delete the artboards (or create an infinite document).
3. Draw a with the brush tool.
4. Crash

**Additional Details**
Provide any further information or context that you think would be helpful in fixing the issue. Screenshots or video can be linked or attached to this issue.
[Screencast_20241019_214255.webm](https://github.com/user-attachments/assets/ad29b7a2-f159-42bd-82fb-01229c2770e7)
**Browser and OS**
Firefox 130, Arch Linux

**Stack Trace**
Copied from the crash dialog in the Graphite editor:

```
panicked at editor/src/messages/portfolio/document/utility_types/document_metadata.rs:207:9:
LayerNodeIdentifier::ROOT_PARENT cannot be converted to NodeId

createPanicManager/<@http://localhost:8080/src/io-managers/panic.ts:9:24
callCallback@http://localhost:8080/src/wasm-communication/subscription-router.ts:25:17
handleJsMessage@http://localhost:8080/src/wasm-communication/subscription-router.ts:33:5
createEditor/handle<@http://localhost:8080/src/wasm-communication/editor.ts:21:19
__wbg_get_imports/imports.wbg.__wbg_call_5fb7c8066a4a4825/<@http://localhost:8080/wasm/pkg/graphite_wasm.js:2302:26
handleError@http://localhost:8080/wasm/pkg/graphite_wasm.js:334:18
__wbg_get_imports/imports.wbg.__wbg_call_5fb7c8066a4a4825@http://localhost:8080/wasm/pkg/graphite_wasm.js:2301:67
graphite_wasm.wasm.__wbg_call_5fb7c8066a4a4825 externref shim@http://localhost:8080/wasm/pkg/graphite_wasm_bg.wasm:wasm-function[25341]:0xf2de18
graphite_wasm.wasm.js_sys::Function::call2::h2987cf074cae0c1c@http://localhost:8080/wasm/pkg/graphite_wasm_bg.wasm:wasm-function[20861]:0xee8259
graphite_wasm.wasm.graphite_wasm::editor_api::EditorHandle::send_frontend_message_to_js::ha4d06d6e54353418@http://localhost:8080/wasm/pkg/graphite_wasm_bg.wasm:wasm-function[2547]:0x828688
graphite_wasm.wasm.std::thread::local::LocalKey<T>::with::hd61b91dd691f48fa@http://localhost:8080/wasm/pkg/graphite_wasm_bg.wasm:wasm-function[14604]:0xdefa3f
graphite_wasm.wasm.graphite_wasm::panic_hook::h030f010a04469e60@http://localhost:8080/wasm/pkg/graphite_wasm_bg.wasm:wasm-function[1485]:0x69d2a0
graphite_wasm.wasm.core::ops::function::Fn::call::h326de5e8a792cab9@http://localhost:8080/wasm/pkg/graphite_wasm_bg.wasm:wasm-function[31492]:0xf51280
graphite_wasm.wasm.std::panicking::rust_panic_with_hook::h2adea73e77f917f4@http://localhost:8080/wasm/pkg/graphite_wasm_bg.wasm:wasm-function[14510]:0xdeadf2
graphite_wasm.wasm.std::panicking::begin_panic_handler::{{closure}}::hdbd2e392ad02e052@http://localhost:8080/wasm/pkg/graphite_wasm_bg.wasm:wasm-function[16650]:0xe4fc5b
graphite_wasm.wasm.std::sys::backtrace::__rust_end_short_backtrace::h7d3184d23478fc66@http://localhost:8080/wasm/pkg/graphite_wasm_bg.wasm:wasm-function[31379]:0xf50e45
graphite_wasm.wasm.rust_begin_unwind@http://localhost:8080/wasm/pkg/graphite_wasm_bg.wasm:wasm-function[24150]:0xf21538
graphite_wasm.wasm.core::panicking::panic_fmt::h1bc190b9665013d3@http://localhost:8080/wasm/pkg/graphite_wasm_bg.wasm:wasm-function[24151]:0xf21565
graphite_wasm.wasm.graphite_editor::messages::portfolio::document::utility_types::document_metadata::DocumentMetadata::transform_to_viewport::he49d91589ade3208@http://localhost:8080/wasm/pkg/graphite_wasm_bg.wasm:wasm-function[7890]:0xbdc3a2
graphite_wasm.wasm.<graphite_editor::messages::tool::tool_messages::brush_tool::BrushToolFsmState as graphite_editor::messages::tool::utility_types::Fsm>::transition::h0ef21faf21cb9338@http://localhost:8080/wasm/pkg/graphite_wasm_bg.wasm:wasm-function[455]:0x33d132
graphite_wasm.wasm.graphite_editor::messages::tool::utility_types::Fsm::process_event::hb62f32080768d26d@http://localhost:8080/wasm/pkg/graphite_wasm_bg.wasm:wasm-function[4418]:0x9e43e6
graphite_wasm.wasm.<graphite_editor::messages::tool::tool_messages::brush_tool::BrushTool as graphite_editor::utility_traits::MessageHandler<graphite_editor::messages::tool::tool_message::ToolMessage,&mut graphite_editor::messages::tool::utility_types::ToolActionHandlerData>>::process_message::h1e106856f8443c03@http://localhost:8080/wasm/pkg/graphite_wasm_bg.wasm:wasm-function[3523]:0x92ccd9
graphite_wasm.wasm.<graphite_editor::messages::tool::tool_message_handler::ToolMessageHandler as graphite_editor::utility_traits::MessageHandler<graphite_editor::messages::tool::tool_message::ToolMessage,graphite_editor::messages::tool::tool_messa

ge_handler::ToolMessageData>>::process_message::h5a01f2f51f67f0b3@http://localhost:8080/wasm/pkg/graphite_wasm_bg.wasm:wasm-function[626]:0x4413a4
graphite_wasm.wasm.graphite_editor::dispatcher::Dispatcher::handle_message::h4472643b48920805@http://localhost:8080/wasm/pkg/graphite_wasm_bg.wasm:wasm-function[543]:0x3d01ac
graphite_wasm.wasm.graphite_editor::application::Editor::handle_message::h82d0aeb69e52fc8b@http://localhost:8080/wasm/pkg/graphite_wasm_bg.wasm:wasm-function[23515]:0xf190d5
graphite_wasm.wasm.std::thread::local::LocalKey<T>::with::h0b821da661c2c76f@http://localhost:8080/wasm/pkg/graphite_wasm_bg.wasm:wasm-function[5296]:0xa81e44
graphite_wasm.wasm.graphite_wasm::editor_api::EditorHandle::dispatch::h32227cb36385de82@http://localhost:8080/wasm/pkg/graphite_wasm_bg.wasm:wasm-function[8375]:0xc0ff4f
graphite_wasm.wasm.editorhandle_onMouseDown@http://localhost:8080/wasm/pkg/graphite_wasm_bg.wasm:wasm-function[8796]:0xc3b60d
onMouseDown@http://localhost:8080/wasm/pkg/graphite_wasm.js:725:14
onPointerDown@http://localhost:8080/src/io-managers/input.ts:116:21
action@http://localhost:8080/src/io-managers/input.ts:25:77
```
Exporting an Artboard with a Scale Factor does not scale the Artboard-Location correctly
1. Create a new document
2. Change Artboard Location to something non-zero
3. Export with a Scale Factor that is not 1

The resulting file will contain the wrong canvas area.


Discussion / hints:
Thanks for the bug report @DaraghD.

I have amended your issue since you are running into a debug assert (so clearly you wasn't using editor.graphite.rs).

The issue occurs because the brush tool tries to find the transform of the parent layer. However if the parent layer is the root layer, a crash occurs since we apparently are forbidden from finding the transform of the root layer. In release mode, we correctly use the `document_to_viewport` transform.
I think this is because I forgot to migrate the brush tool to using the message buffering and using the transform of its own layer, as returned from the render output.


Fix patch:
diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs
index 8eede2d1c6..436081b7ef 100644
--- a/editor/src/messages/portfolio/portfolio_message_handler.rs
+++ b/editor/src/messages/portfolio/portfolio_message_handler.rs
@@ -749,6 +749,8 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
 				responses.add(MenuBarMessage::SendLayout);
 				responses.add(PortfolioMessage::UpdateOpenDocumentsList);
 				responses.add(FrontendMessage::UpdateActiveDocument { document_id });
+				responses.add(ToolMessage::InitTools);
+				responses.add(NodeGraphMessage::Init);
 				responses.add(OverlaysMessage::Draw);
 				responses.add(BroadcastEvent::ToolAbort);
 				responses.add(BroadcastEvent::SelectionChanged);
diff --git a/editor/src/messages/tool/tool_messages/brush_tool.rs b/editor/src/messages/tool/tool_messages/brush_tool.rs
index f2928c15b9..342ca96b0a 100644
--- a/editor/src/messages/tool/tool_messages/brush_tool.rs
+++ b/editor/src/messages/tool/tool_messages/brush_tool.rs
@@ -317,60 +317,63 @@ impl Fsm for BrushToolFsmState {
 				responses.add(DocumentMessage::StartTransaction);
 				let loaded_layer = tool_data.load_existing_strokes(document);
 
-				let layer = loaded_layer.unwrap_or_else(|| new_brush_layer(document, responses));
-				tool_data.layer = Some(layer);
-
-				let parent = layer.parent(document.metadata()).unwrap_or_else(|| document.new_layer_parent(true));
-				let parent_transform = document
-					.network_interface
-					.document_metadata()
-					.transform_to_viewport(parent)
-					.inverse()
-					.transform_point2(input.mouse.position);
-				let layer_position = tool_data.transform.inverse().transform_point2(parent_transform);
-
-				let layer_document_scale = document.metadata().transform_to_document(parent) * tool_data.transform;
-
-				// TODO: Also scale it based on the input image ('Background' input).
-				// TODO: Resizing the input image results in a different brush size from the chosen diameter.
-				let layer_scale = 0.0001_f64 // Safety against division by zero
-					.max((layer_document_scale.matrix2 * glam::DVec2::X).length())
-					.max((layer_document_scale.matrix2 * glam::DVec2::Y).length());
-
-				// Start a new stroke with a single sample
-				let blend_mode = match tool_options.draw_mode {
-					DrawMode::Draw => tool_options.blend_mode,
-					DrawMode::Erase => BlendMode::Erase,
-					DrawMode::Restore => BlendMode::Restore,
-				};
-				tool_data.strokes.push(BrushStroke {
-					trace: vec![BrushInputSample { position: layer_position }],
-					style: BrushStyle {
-						color: tool_options.color.active_color().unwrap_or_default(),
-						diameter: tool_options.diameter / layer_scale,
-						hardness: tool_options.hardness,
-						flow: tool_options.flow,
-						spacing: tool_options.spacing,
-						blend_mode,
-					},
-				});
-
-				tool_data.update_strokes(responses);
-
-				BrushToolFsmState::Drawing
+				if let Some(layer) = loaded_layer {
+					let pos = document
+						.network_interface
+						.document_metadata()
+						.downstream_transform_to_viewport(layer)
+						.inverse()
+						.transform_point2(input.mouse.position);
+					let layer_position = tool_data.transform.inverse().transform_point2(pos);
+					let layer_document_scale = document.metadata().downstream_transform_to_viewport(layer) * tool_data.transform;
+
+					// TODO: Also scale it based on the input image ('Background' input).
+					// TODO: Resizing the input image results in a different brush size from the chosen diameter.
+					let layer_scale = 0.0001_f64 // Safety against division by zero
+						.max((layer_document_scale.matrix2 * glam::DVec2::X).length())
+						.max((layer_document_scale.matrix2 * glam::DVec2::Y).length());
+
+					// Start a new stroke with a single sample
+					let blend_mode = match tool_options.draw_mode {
+						DrawMode::Draw => tool_options.blend_mode,
+						DrawMode::Erase => BlendMode::Erase,
+						DrawMode::Restore => BlendMode::Restore,
+					};
+					tool_data.strokes.push(BrushStroke {
+						trace: vec![BrushInputSample { position: layer_position }],
+						style: BrushStyle {
+							color: tool_options.color.active_color().unwrap_or_default(),
+							diameter: tool_options.diameter / layer_scale,
+							hardness: tool_options.hardness,
+							flow: tool_options.flow,
+							spacing: tool_options.spacing,
+							blend_mode,
+						},
+					});
+
+					tool_data.update_strokes(responses);
+					BrushToolFsmState::Drawing
+				}
+				// Create the new layer, wait for the render output to return its transform, and then create the rest of the layer
+				else {
+					new_brush_layer(document, responses);
+					responses.add(NodeGraphMessage::RunDocumentGraph);
+					responses.add(Message::StartBuffer);
+					responses.add(BrushToolMessage::DragStart);
+					BrushToolFsmState::Ready
+				}
 			}
 
 			(BrushToolFsmState::Drawing, BrushToolMessage::PointerMove) => {
 				if let Some(layer) = tool_data.layer {
 					if let Some(stroke) = tool_data.strokes.last_mut() {
-						let parent = layer.parent(document.metadata()).unwrap_or(LayerNodeIdentifier::ROOT_PARENT);
-						let parent_position = document
+						let layer_position = document
 							.network_interface
 							.document_metadata()
-							.transform_to_viewport(parent)
+							.downstream_transform_to_viewport(layer)
 							.inverse()
 							.transform_point2(input.mouse.position);
-						let layer_position = tool_data.transform.inverse().transform_point2(parent_position);
+						let layer_position = tool_data.transform.inverse().transform_point2(layer_position);
 
 						stroke.trace.push(BrushInputSample { position: layer_position })
 					}
diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs
index 9dc90c37a2..1dd24236e3 100644
--- a/editor/src/node_graph_executor.rs
+++ b/editor/src/node_graph_executor.rs
@@ -488,7 +488,7 @@ impl NodeGraphExecutor {
 
 		let render_config = RenderConfig {
 			viewport: Footprint {
-				transform: transform * DAffine2::from_scale(DVec2::splat(export_config.scale_factor)),
+				transform: DAffine2::from_scale(DVec2::splat(export_config.scale_factor)) * transform,
 				resolution: (size * export_config.scale_factor).as_uvec2(),
 				..Default::default()
 			},
diff --git a/node-graph/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs
index 7c106f09ae..d29c536f69 100644
--- a/node-graph/graph-craft/src/document.rs
+++ b/node-graph/graph-craft/src/document.rs
@@ -1025,7 +1025,6 @@ impl NodeNetwork {
 			warn!("The node which was supposed to be flattened does not exist in the network, id {node_id} network {self:#?}");
 			return;
 		};
-
 		// If the node is hidden, replace it with an identity node
 		let identity_node = DocumentNodeImplementation::ProtoNode("graphene_core::ops::IdentityNode".into());
 		if !node.visible && node.implementation != identity_node {
@@ -1033,8 +1032,8 @@ impl NodeNetwork {
 
 			// Connect layer node to the graphic group below
 			node.inputs.drain(1..);
+			node.manual_composition = None;
 			self.nodes.insert(id, node);
-
 			return;
 		}
 


Test patch: