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 
Fixed part of part 4 in #2269

Fix patch:
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 18c6c0b48d..c024ecab41 100644
--- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs
+++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs
@@ -10,34 +10,62 @@ use graphene_core::raster::image::ImageFrame;
 use graphene_core::raster::BlendMode;
 use graphene_core::text::{Font, TypesettingConfig};
 use graphene_core::vector::style::Gradient;
-use graphene_core::vector::PointId;
 use graphene_core::Color;
+use graphene_std::vector::{ManipulatorPointId, PointId, SegmentId, VectorModificationType};
 
 use glam::DVec2;
 use std::collections::VecDeque;
 
-pub fn merge_layers(document: &DocumentMessageHandler, current_layer: LayerNodeIdentifier, other_layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
+/// Returns the ID of the first Spline node in the horizontal flow which is not followed by a `Path` node, or `None` if none exists.
+pub fn find_spline(document: &DocumentMessageHandler, layer: LayerNodeIdentifier) -> Option<NodeId> {
+	document
+		.network_interface
+		.upstream_flow_back_from_nodes([layer.to_node()].to_vec(), &[], FlowType::HorizontalFlow)
+		.map(|node_id| (document.network_interface.reference(&node_id, &[]).unwrap(), node_id))
+		.take_while(|(reference, _)| reference.as_ref().is_some_and(|node_ref| node_ref != "Path"))
+		.find(|(reference, _)| reference.as_ref().is_some_and(|node_ref| node_ref == "Spline"))
+		.map(|node| node.1)
+}
+
+/// Merge `second_layer` to the `first_layer`.
+pub fn merge_layers(document: &DocumentMessageHandler, first_layer: LayerNodeIdentifier, second_layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
+	if first_layer == second_layer {
+		return;
+	}
 	// 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);
+	let first_layer_transform = document.metadata().downstream_transform_to_document(first_layer);
+	let second_layer_transform = document.metadata().downstream_transform_to_document(second_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 transform_delta = first_layer_transform * second_layer_transform.inverse();
 	let offset = transform_delta.inverse();
 	responses.add(GraphOperationMessage::TransformChange {
-		layer: other_layer,
+		layer: second_layer,
 		transform: offset,
 		transform_in: TransformIn::Local,
 		skip_rerender: false,
 	});
 
-	// Move the other layer below the current layer for positioning purposes
-	let current_layer_parent = current_layer.parent(document.metadata()).unwrap();
-	let current_layer_index = current_layer_parent.children(document.metadata()).position(|child| child == current_layer).unwrap();
+	let mut current_and_other_layer_is_spline = false;
+
+	match (find_spline(document, first_layer), find_spline(document, second_layer)) {
+		(Some(current_layer_spline), Some(other_layer_spline)) => {
+			responses.add(NodeGraphMessage::DeleteNodes {
+				node_ids: [current_layer_spline, other_layer_spline].to_vec(),
+				delete_children: false,
+			});
+			current_and_other_layer_is_spline = true;
+		}
+		_ => {}
+	}
+
+	// Move the `second_layer` below the `first_layer` for positioning purposes
+	let first_layer_parent = first_layer.parent(document.metadata()).unwrap();
+	let first_layer_index = first_layer_parent.children(document.metadata()).position(|child| child == first_layer).unwrap();
 	responses.add(NodeGraphMessage::MoveLayerToStack {
-		layer: other_layer,
-		parent: current_layer_parent,
-		insert_index: current_layer_index + 1,
+		layer: second_layer,
+		parent: first_layer_parent,
+		insert_index: first_layer_index + 1,
 	});
 
 	// Merge the inputs of the two layers
@@ -55,14 +83,14 @@ pub fn merge_layers(document: &DocumentMessageHandler, current_layer: LayerNodeI
 	});
 	responses.add(NodeGraphMessage::MoveNodeToChainStart {
 		node_id: merge_node_id,
-		parent: current_layer,
+		parent: first_layer,
 	});
 	responses.add(NodeGraphMessage::ConnectUpstreamOutputToInput {
-		downstream_input: InputConnector::node(other_layer.to_node(), 1),
+		downstream_input: InputConnector::node(second_layer.to_node(), 1),
 		input_connector: InputConnector::node(merge_node_id, 1),
 	});
 	responses.add(NodeGraphMessage::DeleteNodes {
-		node_ids: vec![other_layer.to_node()],
+		node_ids: vec![second_layer.to_node()],
 		delete_children: false,
 	});
 
@@ -77,7 +105,7 @@ pub fn merge_layers(document: &DocumentMessageHandler, current_layer: LayerNodeI
 	});
 	responses.add(NodeGraphMessage::MoveNodeToChainStart {
 		node_id: flatten_node_id,
-		parent: current_layer,
+		parent: first_layer,
 	});
 
 	// Add a path node after the flatten node
@@ -91,9 +119,25 @@ pub fn merge_layers(document: &DocumentMessageHandler, current_layer: LayerNodeI
 	});
 	responses.add(NodeGraphMessage::MoveNodeToChainStart {
 		node_id: path_node_id,
-		parent: current_layer,
+		parent: first_layer,
 	});
 
+	// Add a Spline node after the Path node if both the layers we are merging is spline.
+	if current_and_other_layer_is_spline {
+		let spline_node_id = NodeId::new();
+		let spline_node = document_node_definitions::resolve_document_node_type("Spline")
+			.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: first_layer,
+		});
+	}
+
 	// Add a transform node to ensure correct tooling modifications
 	let transform_node_id = NodeId::new();
 	let transform_node = document_node_definitions::resolve_document_node_type("Transform")
@@ -105,7 +149,7 @@ pub fn merge_layers(document: &DocumentMessageHandler, current_layer: LayerNodeI
 	});
 	responses.add(NodeGraphMessage::MoveNodeToChainStart {
 		node_id: transform_node_id,
-		parent: current_layer,
+		parent: first_layer,
 	});
 
 	responses.add(NodeGraphMessage::RunDocumentGraph);
@@ -113,6 +157,49 @@ pub fn merge_layers(document: &DocumentMessageHandler, current_layer: LayerNodeI
 	responses.add(PenToolMessage::RecalculateLatestPointsPosition);
 }
 
+/// Merge the `first_endpoint` with `second_endpoint`.
+pub fn merge_points(document: &DocumentMessageHandler, layer: LayerNodeIdentifier, first_endpoint: PointId, second_endpont: PointId, responses: &mut VecDeque<Message>) {
+	let transform = document.metadata().transform_to_document(layer);
+	let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { return };
+
+	let segment = vector_data.segment_bezier_iter().find(|(_, _, start, end)| *end == second_endpont || *start == second_endpont);
+	let Some((segment, _, mut segment_start_point, mut segment_end_point)) = segment else {
+		log::error!("Could not get the segment for second_endpoint.");
+		return;
+	};
+
+	let mut handles = [None; 2];
+	if let Some(handle_position) = ManipulatorPointId::PrimaryHandle(segment).get_position(&vector_data) {
+		let anchor_position = ManipulatorPointId::Anchor(segment_start_point).get_position(&vector_data).unwrap();
+		let handle_position = transform.transform_point2(handle_position);
+		let anchor_position = transform.transform_point2(anchor_position);
+		let anchor_to_handle = handle_position - anchor_position;
+		handles[0] = Some(anchor_to_handle);
+	}
+	if let Some(handle_position) = ManipulatorPointId::EndHandle(segment).get_position(&vector_data) {
+		let anchor_position = ManipulatorPointId::Anchor(segment_end_point).get_position(&vector_data).unwrap();
+		let handle_position = transform.transform_point2(handle_position);
+		let anchor_position = transform.transform_point2(anchor_position);
+		let anchor_to_handle = handle_position - anchor_position;
+		handles[1] = Some(anchor_to_handle);
+	}
+
+	if segment_start_point == second_endpont {
+		core::mem::swap(&mut segment_start_point, &mut segment_end_point);
+		handles.reverse();
+	}
+
+	let modification_type = VectorModificationType::RemovePoint { id: second_endpont };
+	responses.add(GraphOperationMessage::Vector { layer, modification_type });
+	let modification_type = VectorModificationType::RemoveSegment { id: segment };
+	responses.add(GraphOperationMessage::Vector { layer, modification_type });
+
+	let points = [segment_start_point, first_endpoint];
+	let id = SegmentId::generate();
+	let modification_type = VectorModificationType::InsertSegment { id, points, handles };
+	responses.add(GraphOperationMessage::Vector { layer, modification_type });
+}
+
 /// Create a new vector layer.
 pub fn new_vector_layer(subpaths: Vec<Subpath<PointId>>, id: NodeId, parent: LayerNodeIdentifier, responses: &mut VecDeque<Message>) -> LayerNodeIdentifier {
 	let insert_index = 0;
diff --git a/editor/src/messages/tool/tool_messages/spline_tool.rs b/editor/src/messages/tool/tool_messages/spline_tool.rs
index 044cf9159e..3c217f9631 100644
--- a/editor/src/messages/tool/tool_messages/spline_tool.rs
+++ b/editor/src/messages/tool/tool_messages/spline_tool.rs
@@ -1,14 +1,12 @@
 use super::tool_prelude::*;
 use crate::consts::{DEFAULT_STROKE_WIDTH, DRAG_THRESHOLD, PATH_JOIN_THRESHOLD, SNAP_POINT_TOLERANCE};
-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::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;
 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::{self};
+use crate::messages::tool::common_functionality::graph_modification_utils::{self, find_spline, merge_layers, merge_points};
 use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapData, SnapManager, SnapTypeConfiguration, SnappedPoint};
 use crate::messages::tool::common_functionality::utility_functions::{closest_point, should_extend};
 
@@ -52,6 +50,7 @@ pub enum SplineToolMessage {
 	Confirm,
 	DragStart { append_to_selected: Key },
 	DragStop,
+	MergeEndpoints,
 	PointerMove,
 	PointerOutsideViewport,
 	Undo,
@@ -63,6 +62,7 @@ enum SplineToolFsmState {
 	#[default]
 	Ready,
 	Drawing,
+	MergingEndpoints,
 }
 
 #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
@@ -168,6 +168,9 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for SplineT
 				Confirm,
 				Abort,
 			),
+			SplineToolFsmState::MergingEndpoints => actions!(SplineToolMessageDiscriminant;
+				MergeEndpoints,
+			),
 		}
 	}
 }
@@ -196,26 +199,31 @@ struct SplineToolData {
 	preview_segment: Option<SegmentId>,
 	extend: bool,
 	weight: f64,
-	layer: Option<LayerNodeIdentifier>,
-	starting_layer: Option<LayerNodeIdentifier>,
+	/// The layer we are editing.
+	current_layer: Option<LayerNodeIdentifier>,
+	/// The layer and endpoint to merge with the spline's starting endpoint after drawing is complete.
+	start_merge_layer: Option<(LayerNodeIdentifier, PointId)>,
+	/// The endpoint pairs to merge after we have finished drawing the spline and merged the layers.
+	merge_endpoints: Vec<(PointId, PointId)>,
 	snap_manager: SnapManager,
 	auto_panning: AutoPanning,
 }
 
 impl SplineToolData {
 	fn cleanup(&mut self) {
-		self.layer = None;
+		self.current_layer = None;
+		self.start_merge_layer = None;
+		self.merge_endpoints = Vec::new();
 		self.preview_point = None;
 		self.preview_segment = None;
 		self.extend = false;
 		self.points = Vec::new();
-		self.starting_layer = None;
 	}
 
 	/// Get the snapped point while ignoring current layer
 	fn snapped_point(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler) -> SnappedPoint {
 		let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position));
-		let ignore = if let Some(layer) = self.layer { vec![layer] } else { vec![] };
+		let ignore = if let Some(layer) = self.current_layer { vec![layer] } else { vec![] };
 		let snap_data = SnapData::ignore(document, input, &ignore);
 		self.snap_manager.free_snap(&snap_data, &point, SnapTypeConfiguration::default())
 	}
@@ -243,9 +251,22 @@ impl Fsm for SplineToolFsmState {
 				tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
 				self
 			}
+			(SplineToolFsmState::MergingEndpoints, SplineToolMessage::MergeEndpoints) => {
+				let Some(layer) = tool_data.current_layer else { return SplineToolFsmState::Ready };
+
+				if let Some((first, second)) = tool_data.merge_endpoints.pop() {
+					merge_points(document, layer, first, second, responses);
+					responses.add(SplineToolMessage::MergeEndpoints);
+					return SplineToolFsmState::MergingEndpoints;
+				}
+				responses.add(DocumentMessage::EndTransaction);
+
+				SplineToolFsmState::Ready
+			}
 			(SplineToolFsmState::Ready, SplineToolMessage::DragStart { append_to_selected }) => {
 				responses.add(DocumentMessage::StartTransaction);
 
+				tool_data.snap_manager.cleanup(responses);
 				tool_data.cleanup();
 				tool_data.weight = tool_options.line_weight;
 
@@ -253,38 +274,38 @@ 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);
-				}
+				let layers = LayerNodeIdentifier::ROOT_PARENT
+					.descendants(document.metadata())
+					.filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]));
 
 				// 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) {
-					tool_data.layer = Some(layer);
-					tool_data.points.push((point, position));
-					tool_data.next_point = position;
-					tool_data.extend = true;
+				if let Some((layer, point, position)) = should_extend(document, viewport, SNAP_POINT_TOLERANCE, layers, preferences) {
+					if find_spline(document, layer).is_some() {
+						// If the point is the part of Spline then we extend it.
+						tool_data.current_layer = Some(layer);
+						tool_data.points.push((point, position));
+						tool_data.next_point = position;
+						tool_data.extend = true;
 
-					extend_spline(tool_data, true, responses);
+						extend_spline(tool_data, true, responses);
 
-					return SplineToolFsmState::Drawing;
+						return SplineToolFsmState::Drawing;
+					} else {
+						// Otherwise we keep the layer identifier and endpoint identifier to merge it with the spline later when we finish drawing the spline.
+						tool_data.start_merge_layer = Some((layer, point));
+					}
 				}
 
-				// Create new path in the same layer when shift is down
-				if input.keyboard.key(append_to_selected) {
-					let mut selected_layers_except_artboards = selected_nodes.selected_layers_except_artboards(&document.network_interface);
-					let existing_layer = selected_layers_except_artboards.next().filter(|_| selected_layers_except_artboards.next().is_none());
-					if let Some(layer) = existing_layer {
-						tool_data.layer = Some(layer);
+				let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap();
+				let mut selected_layers_except_artboards = selected_nodes.selected_layers_except_artboards(&document.network_interface);
+				let selected_layer = selected_layers_except_artboards.next().filter(|_| selected_layers_except_artboards.next().is_none());
+
+				let append_to_selected_layer = input.keyboard.key(append_to_selected);
+
+				// Create new path in the selected layer when shift is down
+				match (selected_layer, append_to_selected_layer) {
+					(Some(layer), true) => {
+						tool_data.current_layer = Some(layer);
 
 						let transform = document.metadata().transform_to_viewport(layer);
 						let position = transform.inverse().transform_point2(input.mouse.position);
@@ -292,6 +313,7 @@ impl Fsm for SplineToolFsmState {
 
 						return SplineToolFsmState::Drawing;
 					}
+					_ => {}
 				}
 
 				responses.add(DocumentMessage::DeselectAllLayers);
@@ -307,7 +329,7 @@ impl Fsm for SplineToolFsmState {
 				let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
 				tool_options.fill.apply_fill(layer, responses);
 				tool_options.stroke.apply_stroke(tool_data.weight, layer, responses);
-				tool_data.layer = Some(layer);
+				tool_data.current_layer = Some(layer);
 
 				responses.add(Message::StartBuffer);
 
@@ -319,13 +341,9 @@ impl Fsm for SplineToolFsmState {
 					tool_data.extend = false;
 					return SplineToolFsmState::Drawing;
 				}
-				if tool_data.layer.is_none() {
+				if tool_data.current_layer.is_none() {
 					return SplineToolFsmState::Ready;
 				};
-				if join_path(document, input.mouse.position, tool_data, preferences, responses) {
-					responses.add(DocumentMessage::EndTransaction);
-					return SplineToolFsmState::Ready;
-				}
 				tool_data.next_point = tool_data.snapped_point(document, input).snapped_point_document;
 				if tool_data.points.last().map_or(true, |last_pos| last_pos.1.distance(tool_data.next_point) > DRAG_THRESHOLD) {
 					extend_spline(tool_data, false, responses);
@@ -334,7 +352,7 @@ impl Fsm for SplineToolFsmState {
 				SplineToolFsmState::Drawing
 			}
 			(SplineToolFsmState::Drawing, SplineToolMessage::PointerMove) => {
-				let Some(layer) = tool_data.layer else { return SplineToolFsmState::Ready };
+				let Some(layer) = tool_data.current_layer else { return SplineToolFsmState::Ready };
 				let ignore = |cp: PointId| tool_data.preview_point.is_some_and(|pp| pp == cp) || tool_data.points.last().is_some_and(|(ep, _)| *ep == cp);
 				let join_point = closest_point(document, input.mouse.position, PATH_JOIN_THRESHOLD, vec![layer].into_iter(), ignore, preferences);
 
@@ -374,17 +392,16 @@ impl Fsm for SplineToolFsmState {
 
 				state
 			}
-			(SplineToolFsmState::Drawing, SplineToolMessage::Confirm | SplineToolMessage::Abort) => {
+			(SplineToolFsmState::Drawing, SplineToolMessage::Confirm) => {
 				if tool_data.points.len() >= 2 {
 					delete_preview(tool_data, responses);
-					responses.add(DocumentMessage::EndTransaction);
-				} else {
-					responses.add(DocumentMessage::AbortTransaction);
+					merge_paths_layer(document, tool_data, preferences, responses);
 				}
-
-				tool_data.snap_manager.cleanup(responses);
-				tool_data.cleanup();
-
+				responses.add(SplineToolMessage::MergeEndpoints);
+				SplineToolFsmState::MergingEndpoints
+			}
+			(SplineToolFsmState::Drawing, SplineToolMessage::Abort) => {
+				responses.add(DocumentMessage::AbortTransaction);
 				SplineToolFsmState::Ready
 			}
 			(_, SplineToolMessage::WorkingColorChanged) => {
@@ -409,6 +426,7 @@ impl Fsm for SplineToolFsmState {
 				HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Extend Spline")]),
 				HintGroup(vec![HintInfo::keys([Key::Enter], "End Spline")]),
 			]),
+			SplineToolFsmState::MergingEndpoints => HintData(vec![]),
 		};
 
 		responses.add(FrontendMessage::UpdateInputHints { hint_data });
@@ -419,144 +437,56 @@ 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 };
+fn merge_paths_layer(document: &DocumentMessageHandler, tool_data: &mut SplineToolData, preferences: &PreferencesMessageHandler, responses: &mut VecDeque<Message>) {
+	if tool_data.points.len() < 2 {
+		return;
+	};
+	let (last_endpoint, last_endpoint_position) = tool_data.points.last().unwrap();
+	let (start_endpoint, _) = tool_data.points.first().unwrap();
 	let preview_point = tool_data.preview_point;
+	let Some(current_layer) = tool_data.current_layer else { return };
 
-	// 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,
-		LayerNodeIdentifier::ROOT_PARENT.descendants(document.metadata()),
-		|cp| preview_point.is_some_and(|pp| pp == cp) || cp == endpoint,
-		preferences,
-	);
-	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 layers = LayerNodeIdentifier::ROOT_PARENT
+		.descendants(document.metadata())
+		.filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]));
 
-	// 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,
-		});
+	let exclude = |p: PointId| preview_point.is_some_and(|pp| pp == p) || *last_endpoint == p;
+	let position = document.metadata().transform_to_viewport(current_layer).transform_point2(*last_endpoint_position);
 
-		return true;
-	}
+	let start_merge_layer = tool_data.start_merge_layer;
+	let end_merge_layer = closest_point(document, position, PATH_JOIN_THRESHOLD, layers, exclude, preferences);
 
-	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,
-			});
+	let mut endpoints_to_merge = [None, None];
+
+	match (start_merge_layer, end_merge_layer) {
+		(Some(first), Some(last)) => {
+			merge_layers(document, first.0, last.0, responses);
+			merge_layers(document, current_layer, first.0, responses);
+			endpoints_to_merge = [first.1, last.1].map(|p| Some(p));
 		}
-		(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,
-			});
+		(Some(first), None) => {
+			endpoints_to_merge[0] = Some(first.1);
+			merge_layers(document, current_layer, first.0, responses);
 		}
-		_ => {
-			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 },
-			});
+		(None, Some(last)) => {
+			endpoints_to_merge[1] = Some(last.1);
+			merge_layers(document, current_layer, last.0, responses);
 		}
+		(None, None) => return,
 	}
 
-	true
+	if let Some(merge_endpoint) = endpoints_to_merge[0] {
+		tool_data.merge_endpoints.push((*start_endpoint, merge_endpoint));
+	}
+	if let Some(merge_endpoint) = endpoints_to_merge[1] {
+		tool_data.merge_endpoints.push((*last_endpoint, merge_endpoint));
+	}
 }
 
 fn extend_spline(tool_data: &mut SplineToolData, show_preview: bool, responses: &mut VecDeque<Message>) {
 	delete_preview(tool_data, responses);
 
-	let Some(layer) = tool_data.layer else { return };
+	let Some(layer) = tool_data.current_layer else { return };
 
 	let next_point_pos = tool_data.next_point;
 	let next_point_id = PointId::generate();
@@ -585,7 +515,7 @@ fn extend_spline(tool_data: &mut SplineToolData, show_preview: bool, responses:
 }
 
 fn delete_preview(tool_data: &mut SplineToolData, responses: &mut VecDeque<Message>) {
-	let Some(layer) = tool_data.layer else { return };
+	let Some(layer) = tool_data.current_layer else { return };
 
 	if let Some(id) = tool_data.preview_point {
 		let modification_type = VectorModificationType::RemovePoint { id };
@@ -599,426 +529,3 @@ 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: