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.

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 b680d81e06..94a1182b01 100644
--- a/editor/src/messages/tool/tool_messages/spline_tool.rs
+++ b/editor/src/messages/tool/tool_messages/spline_tool.rs
@@ -7,8 +7,9 @@ use crate::messages::tool::common_functionality::color_selector::{ToolColorOptio
 use crate::messages::tool::common_functionality::graph_modification_utils;
 use crate::messages::tool::common_functionality::snapping::SnapManager;
 
-use graph_craft::document::{value::TaggedValue, NodeId, NodeInput};
+use graph_craft::document::{NodeId, NodeInput};
 use graphene_core::Color;
+use graphene_std::vector::{PointId, SegmentId, VectorModificationType};
 
 #[derive(Default)]
 pub struct SplineTool {
@@ -177,8 +178,14 @@ impl ToolTransition for SplineTool {
 
 #[derive(Clone, Debug, Default)]
 struct SplineToolData {
-	points: Vec<DVec2>,
+	/// Points that are inserted.
+	points: Vec<(PointId, DVec2)>,
+	/// Point to be inserted.
 	next_point: DVec2,
+	/// Point that was inserted temporarily to show preview.
+	preview_point: Option<PointId>,
+	/// Segment that was inserted temporarily to show preview.
+	preview_segment: Option<SegmentId>,
 	weight: f64,
 	layer: Option<LayerNodeIdentifier>,
 	snap_manager: SnapManager,
@@ -205,9 +212,11 @@ impl Fsm for SplineToolFsmState {
 
 				tool_data.weight = tool_options.line_weight;
 
-				let node_type = resolve_document_node_type("Spline").expect("Spline node does not exist");
-				let node = node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::VecDVec2(Vec::new()), false))]);
-				let nodes = vec![(NodeId(0), node)];
+				let path_node_type = resolve_document_node_type("Path").expect("Path node does not exist");
+				let path_node = path_node_type.default_node_template();
+				let spline_node_type = resolve_document_node_type("Splines from Points").expect("Spline from Points node does not exist");
+				let spline_node = spline_node_type.node_template_input_override([Some(NodeInput::node(NodeId(1), 0))]);
+				let nodes = vec![(NodeId(1), path_node), (NodeId(0), spline_node)];
 
 				let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
 				tool_options.fill.apply_fill(layer, responses);
@@ -228,12 +237,11 @@ impl Fsm for SplineToolFsmState {
 				let transform = document.metadata().transform_to_viewport(layer);
 				let pos = transform.inverse().transform_point2(snapped_position);
 
-				if tool_data.points.last().map_or(true, |last_pos| last_pos.distance(pos) > DRAG_THRESHOLD) {
-					tool_data.points.push(pos);
+				if tool_data.points.last().map_or(true, |last_pos| last_pos.1.distance(pos) > DRAG_THRESHOLD) {
 					tool_data.next_point = pos;
 				}
 
-				update_spline(document, tool_data, true, responses);
+				update_spline(tool_data, false, responses);
 
 				SplineToolFsmState::Drawing
 			}
@@ -246,7 +254,7 @@ impl Fsm for SplineToolFsmState {
 				let pos = transform.inverse().transform_point2(snapped_position);
 				tool_data.next_point = pos;
 
-				update_spline(document, tool_data, true, responses);
+				update_spline(tool_data, true, responses);
 
 				// Auto-panning
 				let messages = [SplineToolMessage::PointerOutsideViewport.into(), SplineToolMessage::PointerMove.into()];
@@ -269,13 +277,15 @@ impl Fsm for SplineToolFsmState {
 			}
 			(SplineToolFsmState::Drawing, SplineToolMessage::Confirm | SplineToolMessage::Abort) => {
 				if tool_data.points.len() >= 2 {
-					update_spline(document, tool_data, false, responses);
+					delete_preview(tool_data, responses);
 					responses.add(DocumentMessage::EndTransaction);
 				} else {
 					responses.add(DocumentMessage::AbortTransaction);
 				}
 
 				tool_data.layer = None;
+				tool_data.preview_point = None;
+				tool_data.preview_segment = None;
 				tool_data.points.clear();
 				tool_data.snap_manager.cleanup(responses);
 
@@ -310,17 +320,49 @@ impl Fsm for SplineToolFsmState {
 	}
 }
 
-fn update_spline(document: &DocumentMessageHandler, tool_data: &SplineToolData, show_preview: bool, responses: &mut VecDeque<Message>) {
-	let mut points = tool_data.points.clone();
+fn update_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 next_point_pos = tool_data.next_point;
+	let next_point_id = PointId::generate();
+	let modification_type = VectorModificationType::InsertPoint {
+		id: next_point_id,
+		position: next_point_pos,
+	};
+	responses.add(GraphOperationMessage::Vector { layer, modification_type });
+
+	if let Some((last_point_id, _)) = tool_data.points.last() {
+		let points = [*last_point_id, next_point_id];
+		let id = SegmentId::generate();
+		let modification_type = VectorModificationType::InsertSegment { id, points, handles: [None, None] };
+		responses.add(GraphOperationMessage::Vector { layer, modification_type });
+
+		if show_preview {
+			tool_data.preview_segment = Some(id);
+		}
+	}
+
 	if show_preview {
-		points.push(tool_data.next_point)
+		tool_data.preview_point = Some(next_point_id);
+	} else {
+		tool_data.points.push((next_point_id, next_point_pos));
 	}
-	let value = TaggedValue::VecDVec2(points);
+}
 
+fn delete_preview(tool_data: &mut SplineToolData, responses: &mut VecDeque<Message>) {
 	let Some(layer) = tool_data.layer else { return };
 
-	let Some(node_id) = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface).upstream_node_id_from_name("Spline") else {
-		return;
-	};
-	responses.add_front(NodeGraphMessage::SetInputValue { node_id, input_index: 1, value });
+	if let Some(id) = tool_data.preview_point {
+		let modification_type = VectorModificationType::RemovePoint { id };
+		responses.add(GraphOperationMessage::Vector { layer, modification_type });
+	}
+	if let Some(id) = tool_data.preview_segment {
+		let modification_type = VectorModificationType::RemoveSegment { id };
+		responses.add(GraphOperationMessage::Vector { layer, modification_type });
+	}
+
+	tool_data.preview_point = None;
+	tool_data.preview_segment = None;
 }


Test patch: