Repository: GraphiteEditor/Graphite (language: rust)

Issue:
Make the Spline tool draw a polylines with a `Path -> Spline from Points` node setup
- [x] **Part 1** (#2200)

    Listen to the task description [in this video](https://files.keavon.com/-/ChillyTenseAdamsstaghornedbeetle/capture_16_.mp4).

- [x] **Part 2** (#2203)

1. Make the Spline tool visualize the endpoints of each spline path just like the Freehand tool does (even if there are multiple spline paths because the Path node has multiple subpaths)
2. Allow clicking on the endpoints of a spline (on either end), when not drawing, to begin drawing a continuation of the spline
3. Allow clicking the start point while drawing spline segments to add a segment which closes the path given to the Spline from Points node
4. Allow holding <kbd>Shift</kbd> to begin drawing a new spline subpath in the same layer, instead of making it add a new layer (just like the equivalent functionality with the Pen tool)

- [x] **Part 3** (#2274)

1. Create an upgrade script to transform a single existing "Spline" node, in the graph of any existing documents people have today, into a `Path -> Spline from Points` node pair with the point data transferred over to the Path node's geometry
2. Delete the "Spline" node
5. Rename the "Spline from Points" node to just "Spline"
6. Create an upgrade script to rename the "Spline from Points" node in existing documents to "Spline"

    Search the project for "document upgrade code" and you'll find all the comments relating to the upgrading scripts. The ones in `portfolio_message_handler.rs`, within the handler for the `PortfolioMessage::OpenDocumentFileWithId` message, are most relevant to read and that's likely where you'll be adding most or all the upgrading code to.

- [x] **Part 4** (PR 1 of 2: #2269, PR 2 of 2: #2319
  
  Merging path with splines, splines with paths, splines with splines, and paths with paths by joining their endpoints correctly. This is described as part of https://files.keavon.com/-/PunyUprightStagbeetle/capture_35_.mp4


Discussion / hints:
Can I take this issue as my first issue?
@kliwongan this is currently being worked on by @indierusty (here's the [Discord thread](https://discord.com/channels/731730685944922173/1327206573071667202)). If there isn't a viable PR opened within a week, feel free to ask again if it's available then.
@kliwongan I ended up adding a new part to this issue, so now it's a three-parter. Feel free to work on part 3, which isn't blocked by the previous two parts, if you would like. If you plan to work on it, please let me know when you'll be beginning.
Implemented Part 1 i.e. make spline tool to use Path and Spline from Points node instead of just Spline node to make spline tool further non-destructive in #2200 

Fix patch:
diff --git a/editor/src/messages/tool/tool_messages/spline_tool.rs b/editor/src/messages/tool/tool_messages/spline_tool.rs
index d7eb508cdb..044cf9159e 100644
--- a/editor/src/messages/tool/tool_messages/spline_tool.rs
+++ b/editor/src/messages/tool/tool_messages/spline_tool.rs
@@ -1,12 +1,14 @@
 use super::tool_prelude::*;
 use crate::consts::{DEFAULT_STROKE_WIDTH, DRAG_THRESHOLD, PATH_JOIN_THRESHOLD, SNAP_POINT_TOLERANCE};
-use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
+use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
+use crate::messages::portfolio::document::node_graph::document_node_definitions::{self, resolve_document_node_type};
 use crate::messages::portfolio::document::overlays::utility_functions::path_endpoint_overlays;
 use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
 use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
+use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector};
 use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
 use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
-use crate::messages::tool::common_functionality::graph_modification_utils;
+use crate::messages::tool::common_functionality::graph_modification_utils::{self};
 use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapData, SnapManager, SnapTypeConfiguration, SnappedPoint};
 use crate::messages::tool::common_functionality::utility_functions::{closest_point, should_extend};
 
@@ -195,6 +197,7 @@ struct SplineToolData {
 	extend: bool,
 	weight: f64,
 	layer: Option<LayerNodeIdentifier>,
+	starting_layer: Option<LayerNodeIdentifier>,
 	snap_manager: SnapManager,
 	auto_panning: AutoPanning,
 }
@@ -206,6 +209,7 @@ impl SplineToolData {
 		self.preview_segment = None;
 		self.extend = false;
 		self.points = Vec::new();
+		self.starting_layer = None;
 	}
 
 	/// Get the snapped point while ignoring current layer
@@ -249,6 +253,19 @@ impl Fsm for SplineToolFsmState {
 				let snapped = tool_data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default());
 				let viewport = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document);
 
+				// Check if we're starting from an endpoint of any layer, even if not extending
+				let closest_endpoint = closest_point(
+					document,
+					viewport,
+					PATH_JOIN_THRESHOLD,
+					LayerNodeIdentifier::ROOT_PARENT.descendants(document.metadata()),
+					|_| false, // Don't exclude any points
+					preferences,
+				);
+				if let Some((start_layer, _, _)) = closest_endpoint {
+					tool_data.starting_layer = Some(start_layer);
+				}
+
 				// Extend an endpoint of the selected path
 				let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap();
 				if let Some((layer, point, position)) = should_extend(document, viewport, SNAP_POINT_TOLERANCE, selected_nodes.selected_layers(document.metadata()), preferences) {
@@ -405,29 +422,133 @@ impl Fsm for SplineToolFsmState {
 /// Return `true` only if new segment is inserted to connect two end points in the selected layer otherwise `false`.
 fn join_path(document: &DocumentMessageHandler, mouse_pos: DVec2, tool_data: &mut SplineToolData, preferences: &PreferencesMessageHandler, responses: &mut VecDeque<Message>) -> bool {
 	let Some(&(endpoint, _)) = tool_data.points.last() else { return false };
-
+	let Some(&(start_point, _)) = tool_data.points.first() else { return false };
+	let Some(starting_layer) = tool_data.starting_layer else { return false };
+	let Some(current_layer) = tool_data.layer else { return false };
 	let preview_point = tool_data.preview_point;
-	let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap();
-	let selected_layers = selected_nodes.selected_layers(document.metadata());
 
 	// Get the closest point to mouse position which is not preview_point or end_point.
 	let closest_point = closest_point(
 		document,
 		mouse_pos,
 		PATH_JOIN_THRESHOLD,
-		selected_layers,
+		LayerNodeIdentifier::ROOT_PARENT.descendants(document.metadata()),
 		|cp| preview_point.is_some_and(|pp| pp == cp) || cp == endpoint,
 		preferences,
 	);
-	let Some((layer, join_point, _)) = closest_point else { return false };
+	let Some((other_layer, join_point, _)) = closest_point else { return false };
 
 	// Last end point inserted was the preview point and segment therefore we delete it before joining the end_point & join_point.
 	delete_preview(tool_data, responses);
 
-	let points = [endpoint, join_point];
-	let id = SegmentId::generate();
-	let modification_type = VectorModificationType::InsertSegment { id, points, handles: [None, None] };
-	responses.add(GraphOperationMessage::Vector { layer, modification_type });
+	// If the points are in different layers, merge them first
+	if current_layer == other_layer {
+		// If points are in the same layer, just connect them
+		let points = [endpoint, join_point];
+		let id = SegmentId::generate();
+		let modification_type = VectorModificationType::InsertSegment { id, points, handles: [None, None] };
+		responses.add(GraphOperationMessage::Vector {
+			layer: current_layer,
+			modification_type,
+		});
+
+		return true;
+	}
+
+	match (is_layer_spline(document, starting_layer), is_layer_spline(document, other_layer)) {
+		(true, true) => {
+			merge_two_spline_layer(document, current_layer, other_layer, responses);
+			let points = [endpoint, join_point];
+			let id = SegmentId::generate();
+			let modification_type = VectorModificationType::InsertSegment { id, points, handles: [None, None] };
+			responses.add(GraphOperationMessage::Vector {
+				layer: current_layer,
+				modification_type,
+			});
+		}
+		(false, false) => {
+			let Some(current_vector_data) = document.network_interface.compute_modified_vector(current_layer) else {
+				log::error!("Could not get vector data for current layer");
+				return false;
+			};
+			let Some(starting_vector_data) = document.network_interface.compute_modified_vector(starting_layer) else {
+				log::error!("Could not get vector data for other layer");
+				return false;
+			};
+
+			let Some(starting_layer_endpoint) = starting_vector_data.end_point().last() else {
+				log::error!("Could not get endpoint");
+				return false;
+			};
+
+			let handles = (0..current_vector_data.segment_domain.handles().len())
+				.find_map(|index| {
+					let (start_id, end_id, bezier) = current_vector_data.segment_points_from_index(index);
+					if start_id == endpoint {
+						Some([bezier.handles.start(), bezier.handles.end()])
+					} else if end_id == endpoint {
+						// Reverse the handles if connecting to end point
+						Some([bezier.handles.end(), bezier.handles.start()])
+					} else {
+						None
+					}
+				})
+				.unwrap();
+
+			// Merge the layers first
+			merge_non_spline_layers(document, starting_layer, current_layer, other_layer, responses);
+
+			let points = [endpoint, join_point];
+			let points2 = [start_point, starting_layer_endpoint];
+			let id = SegmentId::generate();
+			let modification_type = VectorModificationType::InsertSegment { id, points, handles };
+			responses.add(GraphOperationMessage::Vector {
+				layer: starting_layer,
+				modification_type,
+			});
+
+			let id = SegmentId::generate();
+			let modification_type = VectorModificationType::InsertSegment { id, points: points2, handles };
+			responses.add(GraphOperationMessage::Vector {
+				layer: starting_layer,
+				modification_type,
+			});
+		}
+		_ => {
+			let current_vector_data = match document.network_interface.compute_modified_vector(current_layer) {
+				Some(data) => data,
+				None => {
+					log::error!("Could not get vector data for current layer");
+					return false;
+				}
+			};
+
+			let handles = (0..current_vector_data.segment_domain.handles().len()).find_map(|index| {
+				let (start_id, end_id, bezier) = current_vector_data.segment_points_from_index(index);
+				if start_id == endpoint {
+					Some([bezier.handles.start(), bezier.handles.end()])
+				} else if end_id == endpoint {
+					Some([bezier.handles.end(), bezier.handles.start()])
+				} else {
+					None
+				}
+			});
+
+			let Some(handles) = handles else {
+				log::error!("Could not find handles for endpoint");
+				return false;
+			};
+
+			merge_path_spline_layer(document, current_layer, other_layer, responses);
+
+			let points = [endpoint, join_point];
+			let id = SegmentId::generate();
+			responses.add(GraphOperationMessage::Vector {
+				layer: other_layer,
+				modification_type: VectorModificationType::InsertSegment { id, points, handles },
+			});
+		}
+	}
 
 	true
 }
@@ -478,3 +599,426 @@ fn delete_preview(tool_data: &mut SplineToolData, responses: &mut VecDeque<Messa
 	tool_data.preview_point = None;
 	tool_data.preview_segment = None;
 }
+
+fn merge_two_spline_layer(document: &DocumentMessageHandler, current_layer: LayerNodeIdentifier, other_layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
+	// Calculate the downstream transforms in order to bring the other vector data into the same layer space
+	let current_transform = document.metadata().downstream_transform_to_document(current_layer);
+	let other_transform = document.metadata().downstream_transform_to_document(other_layer);
+
+	// Represents the change in position that would occur if the other layer was moved below the current layer
+	let transform_delta = current_transform * other_transform.inverse();
+	let offset = transform_delta.inverse();
+	responses.add(GraphOperationMessage::TransformChange {
+		layer: other_layer,
+		transform: offset,
+		transform_in: TransformIn::Local,
+		skip_rerender: false,
+	});
+
+	// First find their IDs
+	let current_layer_nodes = document
+		.network_interface
+		.upstream_flow_back_from_nodes(
+			vec![current_layer.to_node()],
+			&[],
+			crate::messages::portfolio::document::utility_types::network_interface::FlowType::HorizontalFlow,
+		)
+		.collect::<Vec<_>>();
+
+	let other_layer_nodes = document
+		.network_interface
+		.upstream_flow_back_from_nodes(vec![other_layer.to_node()], &[], FlowType::HorizontalFlow)
+		.collect::<Vec<_>>();
+
+	// Add merge node and insert between path and spline
+	let merge_node_id = NodeId::new();
+	let merge_node = document_node_definitions::resolve_document_node_type("Merge")
+		.expect("Failed to create merge node")
+		.default_node_template();
+	responses.add(NodeGraphMessage::InsertNode {
+		node_id: merge_node_id,
+		node_template: merge_node,
+	});
+	responses.add(NodeGraphMessage::SetToNodeOrLayer {
+		node_id: merge_node_id,
+		is_layer: false,
+	});
+
+	responses.add(NodeGraphMessage::MoveNodeToChainStart {
+		node_id: merge_node_id,
+		parent: current_layer,
+	});
+
+	responses.add(NodeGraphMessage::ConnectUpstreamOutputToInput {
+		downstream_input: InputConnector::node(other_layer.to_node(), 1),
+		input_connector: InputConnector::node(merge_node_id, 1),
+	});
+
+	// Add flatten vector elements node after merge
+	let flatten_node_id = NodeId::new();
+	let flatten_node = document_node_definitions::resolve_document_node_type("Flatten Vector Elements")
+		.expect("Failed to create flatten node")
+		.default_node_template();
+	responses.add(NodeGraphMessage::InsertNode {
+		node_id: flatten_node_id,
+		node_template: flatten_node,
+	});
+	responses.add(NodeGraphMessage::MoveNodeToChainStart {
+		node_id: flatten_node_id,
+		parent: current_layer,
+	});
+
+	let path_node_id = NodeId::new();
+	let path_node = document_node_definitions::resolve_document_node_type("Path")
+		.expect("Failed to create path node")
+		.default_node_template();
+	responses.add(NodeGraphMessage::InsertNode {
+		node_id: path_node_id,
+		node_template: path_node,
+	});
+	responses.add(NodeGraphMessage::MoveNodeToChainStart {
+		node_id: path_node_id,
+		parent: current_layer,
+	});
+
+	let spline_node_id = NodeId::new();
+	let spline_node = document_node_definitions::resolve_document_node_type("Splines from Points")
+		.expect("Failed to create spline node")
+		.default_node_template();
+	responses.add(NodeGraphMessage::InsertNode {
+		node_id: spline_node_id,
+		node_template: spline_node,
+	});
+	responses.add(NodeGraphMessage::MoveNodeToChainStart {
+		node_id: spline_node_id,
+		parent: current_layer,
+	});
+
+	let stroke_node_id = NodeId::new();
+	let stroke_node = document_node_definitions::resolve_document_node_type("Stroke")
+		.expect("Failed to create stroke node")
+		.default_node_template();
+	responses.add(NodeGraphMessage::InsertNode {
+		node_id: stroke_node_id,
+		node_template: stroke_node,
+	});
+	responses.add(NodeGraphMessage::MoveNodeToChainStart {
+		node_id: stroke_node_id,
+		parent: current_layer,
+	});
+
+	responses.add(NodeGraphMessage::DeleteNodes {
+		node_ids: current_layer_nodes[1..3].to_vec(),
+		delete_children: false,
+	});
+
+	responses.add(NodeGraphMessage::DeleteNodes {
+		node_ids: other_layer_nodes[..3].to_vec(),
+		delete_children: false,
+	});
+
+	responses.add(NodeGraphMessage::RunDocumentGraph);
+	responses.add(Message::StartBuffer);
+}
+
+fn merge_non_spline_layers(
+	document: &DocumentMessageHandler,
+	starting_layer: LayerNodeIdentifier,
+	current_layer: LayerNodeIdentifier,
+	other_layer: LayerNodeIdentifier,
+	responses: &mut VecDeque<Message>,
+) {
+	// Calculate the downstream transforms in order to bring the other vector data into the same layer space
+	let current_transform = document.metadata().downstream_transform_to_document(current_layer);
+	let other_transform = document.metadata().downstream_transform_to_document(other_layer);
+
+	// Represents the change in position that would occur if the other layer was moved below the current layer
+	let transform_delta = current_transform * other_transform.inverse();
+	let offset = transform_delta.inverse();
+	responses.add(GraphOperationMessage::TransformChange {
+		layer: other_layer,
+		transform: offset,
+		transform_in: TransformIn::Local,
+		skip_rerender: false,
+	});
+
+	// Delete spline and stroke nodes from both layers
+	// First find their IDs
+	let starting_layer_nodes = document
+		.network_interface
+		.upstream_flow_back_from_nodes(
+			vec![starting_layer.to_node()],
+			&[],
+			crate::messages::portfolio::document::utility_types::network_interface::FlowType::HorizontalFlow,
+		)
+		.collect::<Vec<_>>();
+
+	let current_layer_nodes = document
+		.network_interface
+		.upstream_flow_back_from_nodes(
+			vec![current_layer.to_node()],
+			&[],
+			crate::messages::portfolio::document::utility_types::network_interface::FlowType::HorizontalFlow,
+		)
+		.collect::<Vec<_>>();
+
+	let other_layer_nodes = document
+		.network_interface
+		.upstream_flow_back_from_nodes(vec![other_layer.to_node()], &[], FlowType::HorizontalFlow)
+		.collect::<Vec<_>>();
+
+	// Add merge node and insert between path and spline
+	let merge_node_id = NodeId::new();
+	let merge_node = document_node_definitions::resolve_document_node_type("Merge")
+		.expect("Failed to create merge node")
+		.default_node_template();
+	responses.add(NodeGraphMessage::InsertNode {
+		node_id: merge_node_id,
+		node_template: merge_node,
+	});
+	responses.add(NodeGraphMessage::SetToNodeOrLayer {
+		node_id: merge_node_id,
+		is_layer: false,
+	});
+
+	responses.add(NodeGraphMessage::MoveNodeToChainStart {
+		node_id: merge_node_id,
+		parent: starting_layer,
+	});
+	responses.add(NodeGraphMessage::ConnectUpstreamOutputToInput {
+		downstream_input: InputConnector::node(other_layer.to_node(), 1),
+		input_connector: InputConnector::node(merge_node_id, 1),
+	});
+
+	let merge_node_id2 = NodeId::new();
+	let merge_node2 = document_node_definitions::resolve_document_node_type("Merge")
+		.expect("Failed to create merge node")
+		.default_node_template();
+	responses.add(NodeGraphMessage::InsertNode {
+		node_id: merge_node_id2,
+		node_template: merge_node2,
+	});
+	responses.add(NodeGraphMessage::SetToNodeOrLayer {
+		node_id: merge_node_id2,
+		is_layer: false,
+	});
+	responses.add(NodeGraphMessage::MoveNodeToChainStart {
+		node_id: merge_node_id2,
+		parent: starting_layer,
+	});
+	responses.add(NodeGraphMessage::ConnectUpstreamOutputToInput {
+		downstream_input: InputConnector::node(current_layer.to_node(), 1),
+		input_connector: InputConnector::node(merge_node_id2, 1),
+	});
+	// Add flatten vector elements node after merge
+	let flatten_node_id2 = NodeId::new();
+	let flatten_node2 = document_node_definitions::resolve_document_node_type("Flatten Vector Elements")
+		.expect("Failed to create flatten node")
+		.default_node_template();
+	responses.add(NodeGraphMessage::InsertNode {
+		node_id: flatten_node_id2,
+		node_template: flatten_node2,
+	});
+	responses.add(NodeGraphMessage::MoveNodeToChainStart {
+		node_id: flatten_node_id2,
+		parent: starting_layer,
+	});
+
+	let path_node_id = NodeId::new();
+	let path_node = document_node_definitions::resolve_document_node_type("Path")
+		.expect("Failed to create path node")
+		.default_node_template();
+	responses.add(NodeGraphMessage::InsertNode {
+		node_id: path_node_id,
+		node_template: path_node,
+	});
+	responses.add(NodeGraphMessage::MoveNodeToChainStart {
+		node_id: path_node_id,
+		parent: starting_layer,
+	});
+
+	let stroke_node_id = NodeId::new();
+	let stroke_node = document_node_definitions::resolve_document_node_type("Stroke")
+		.expect("Failed to create stroke node")
+		.default_node_template();
+	responses.add(NodeGraphMessage::InsertNode {
+		node_id: stroke_node_id,
+		node_template: stroke_node,
+	});
+	responses.add(NodeGraphMessage::MoveNodeToChainStart {
+		node_id: stroke_node_id,
+		parent: starting_layer,
+	});
+
+	responses.add(NodeGraphMessage::DeleteNodes {
+		node_ids: starting_layer_nodes[1..(if is_layer_line(document, starting_layer) { 2 } else { 3 })].to_vec(),
+		delete_children: false,
+	});
+	responses.add(NodeGraphMessage::DeleteNodes {
+		node_ids: other_layer_nodes[..(if is_layer_line(document, other_layer) { 2 } else { 3 })].to_vec(),
+		delete_children: false,
+	});
+	responses.add(NodeGraphMessage::DeleteNodes {
+		node_ids: current_layer_nodes[..2].to_vec(),
+		delete_children: false,
+	});
+
+	responses.add(NodeGraphMessage::RunDocumentGraph);
+	responses.add(Message::StartBuffer);
+}
+
+fn merge_path_spline_layer(document: &DocumentMessageHandler, current_layer: LayerNodeIdentifier, other_layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
+	// Calculate the downstream transforms in order to bring the other vector data into the same layer space
+	let current_transform = document.metadata().downstream_transform_to_document(current_layer);
+	let other_transform = document.metadata().downstream_transform_to_document(other_layer);
+
+	// Represents the change in position that would occur if the other layer was moved below the current layer
+	let transform_delta = current_transform * other_transform.inverse();
+	let offset = transform_delta.inverse();
+	responses.add(GraphOperationMessage::TransformChange {
+		layer: other_layer,
+		transform: offset,
+		transform_in: TransformIn::Local,
+		skip_rerender: false,
+	});
+
+	// First find their IDs
+	let current_layer_nodes = document
+		.network_interface
+		.upstream_flow_back_from_nodes(
+			vec![current_layer.to_node()],
+			&[],
+			crate::messages::portfolio::document::utility_types::network_interface::FlowType::HorizontalFlow,
+		)
+		.collect::<Vec<_>>();
+
+	let other_layer_nodes = document
+		.network_interface
+		.upstream_flow_back_from_nodes(vec![other_layer.to_node()], &[], FlowType::HorizontalFlow)
+		.collect::<Vec<_>>();
+
+	// Add merge node and insert between path and spline
+	let merge_node_id = NodeId::new();
+	let merge_node = document_node_definitions::resolve_document_node_type("Merge")
+		.expect("Failed to create merge node")
+		.default_node_template();
+	responses.add(NodeGraphMessage::InsertNode {
+		node_id: merge_node_id,
+		node_template: merge_node,
+	});
+	responses.add(NodeGraphMessage::SetToNodeOrLayer {
+		node_id: merge_node_id,
+		is_layer: false,
+	});
+
+	responses.add(NodeGraphMessage::MoveNodeToChainStart {
+		node_id: merge_node_id,
+		parent: other_layer,
+	});
+
+	responses.add(NodeGraphMessage::ConnectUpstreamOutputToInput {
+		downstream_input: InputConnector::node(current_layer.to_node(), 1),
+		input_connector: InputConnector::node(merge_node_id, 1),
+	});
+
+	// Add flatten vector elements node after merge
+	let flatten_node_id = NodeId::new();
+	let flatten_node = document_node_definitions::resolve_document_node_type("Flatten Vector Elements")
+		.expect("Failed to create flatten node")
+		.default_node_template();
+	responses.add(NodeGraphMessage::InsertNode {
+		node_id: flatten_node_id,
+		node_template: flatten_node,
+	});
+	responses.add(NodeGraphMessage::MoveNodeToChainStart {
+		node_id: flatten_node_id,
+		parent: other_layer,
+	});
+
+	let path_node_id = NodeId::new();
+	let path_node = document_node_definitions::resolve_document_node_type("Path")
+		.expect("Failed to create path node")
+		.default_node_template();
+	responses.add(NodeGraphMessage::InsertNode {
+		node_id: path_node_id,
+		node_template: path_node,
+	});
+	responses.add(NodeGraphMessage::MoveNodeToChainStart {
+		node_id: path_node_id,
+		parent: other_layer,
+	});
+
+	let stroke_node_id = NodeId::new();
+	let stroke_node = document_node_definitions::resolve_document_node_type("Stroke")
+		.expect("Failed to create stroke node")
+		.default_node_template();
+	responses.add(NodeGraphMessage::InsertNode {
+		node_id: stroke_node_id,
+		node_template: stroke_node,
+	});
+	responses.add(NodeGraphMessage::MoveNodeToChainStart {
+		node_id: stroke_node_id,
+		parent: other_layer,
+	});
+
+	let node_range = if is_layer_line(document, other_layer) {
+		1..2 // transform node is not deleted
+	} else {
+		1..3
+	};
+
+	responses.add(NodeGraphMessage::DeleteNodes {
+		node_ids: other_layer_nodes[node_range].to_vec(),
+		delete_children: false,
+	});
+
+	responses.add(NodeGraphMessage::DeleteNodes {
+		node_ids: current_layer_nodes[..2].to_vec(),
+		delete_children: false,
+	});
+
+	responses.add(NodeGraphMessage::RunDocumentGraph);
+	responses.add(Message::StartBuffer);
+}
+
+fn is_layer_spline(document: &DocumentMessageHandler, layer: LayerNodeIdentifier) -> bool {
+	let nodes = document
+		.network_interface
+		.upstream_flow_back_from_nodes(vec![layer.to_node()], &[], FlowType::HorizontalFlow)
+		.collect::<Vec<NodeId>>();
+
+	// Check node types in the chain
+	let mut has_spline = false;
+
+	for node in nodes {
+		if let Some(reference) = document.network_interface.reference(&node, &[]) {
+			match reference.as_deref() {
+				Some("Splines from Points") => has_spline = true,
+				_ => continue,
+			}
+		}
+	}
+
+	has_spline
+}
+
+fn is_layer_line(document: &DocumentMessageHandler, layer: LayerNodeIdentifier) -> bool {
+	let nodes = document
+		.network_interface
+		.upstream_flow_back_from_nodes(vec![layer.to_node()], &[], FlowType::HorizontalFlow)
+		.collect::<Vec<NodeId>>();
+
+	// Check node types in the chain
+	let mut has_line = false;
+
+	for node in nodes {
+		if let Some(reference) = document.network_interface.reference(&node, &[]) {
+			match reference.as_deref() {
+				Some("Line") => has_line = true,
+				_ => continue,
+			}
+		}
+	}
+
+	has_line
+}


Test patch: