Repository: GraphiteEditor/Graphite (language: rust)

Issue:
Tracking Issue: Pen and Path tool improvements
Much of this is explained/demonstrated in this detailed webcast: https://youtu.be/OmveqhzXJFg

## Path tool

When dragging a handle:
- [x] <kbd>Alt</kbd> keeps the handles equidistant while pressed (instead of <kbd>Shift</kbd> in use now) (#2058)
- [x] <kbd>Alt</kbd>, as part of keeping the handles equidistant, also creates a matching handle if the other is zero-length or not a cubic Bézier (#2196)
- [x] <kbd>Shift</kbd> locks the handle angle to 15° increments (shouldn't interfere with <kbd>Shift</kbd>-clicking handles for multi-selecting points such as handles) (#2160)
- [x] <kbd>Ctrl</kbd> locks the current angle of the handle (#2160)
- [x] <kbd>Tab</kbd> swaps to dragging the opposite handle (native: teleports the cursor / web: enters pointer lock and draws a fake cursor at the new location) (#2058)
- [x] <kbd>Space</kbd> shifts the anchor (and handles) while pressed (#2065)
- [x] <kbd>C</kbd> toggles bent vs. colinear handles (instead of <kbd>Alt</kbd> in use now) (#2058)

When dragging an anchor:
- [ ] <kbd>Alt</kbd> **(new since video was recorded)**, if held during the start of the click-drag, converts the anchor's handles to colinear and drags the handle pair instead of the anchor, keeping them equidistant unless <kbd>Alt</kbd> is released to temporarily return the opposite handle back to where it started before the <kbd>Alt</kbd>-drag began (which involves returning its colinear state back to what it was before). Should work with <kbd>Tab</kbd> to swap which is the dragged handle, so its opposite can be the one that's returned if <kbd>Alt</kbd> is temporarily released. Just <kbd>Alt</kbd> clicking but not dragging the anchor should remove both handles by setting them to zero length.
- [ ] <kbd>Ctrl</kbd> **(new since video was recorded)**, if pressed before the drag begins, should select and drag a zero-length handle out of its anchor locked to the angle of the opposite non-zero-length handle, which it should be made colinear to. This applies only to anchors with two handles where exactly one is zero-length. Otherwise, the key is ignored and the anchor is dragged per usual. One more scenario is where a non-endpoint anchor has no handles (they're both zero-length), if <kbd>Ctrl</kbd> and <kbd>Alt</kbd> (see previous bullet point) are both pressed before dragging, it locks the angle to that of the [line perpendicular to the average of the two tangents](https://files.keavon.com/-/NovelGruesomePangolin/capture.png) from that anchor to its adjacent curves.
- [ ] <kbd>Shift</kbd>, locks the drag to the nearer of the X or Y axes from its drag starting point. (If pressed before the drag begins, it shouldn't break <kbd>Shift</kbd>-clicking points to add to the selection.)
- [ ] <kbd>Ctrl</kbd><kbd>Shift</kbd> **(updated since video was recorded which indicated this would use <kbd>Alt</kbd>)** slides the anchor along its adjacent segment(s) while adjusting its handle(s) to best approximate the initial curvature. This will require a computational geometry algorithm researched and added to our Bezier-rs library. (If pressed before the drag begins, it shouldn't break <kbd>Shift</kbd>-clicking points to add to the selection.)

When double-clicking an anchor **(new since video was recorded)**:
- [ ] Currently this swaps between smooth and sharp, but doesn't work if multiple handles are multi-selected to convert them all to smooth or sharp. If they're of mixed type, the specific on that's double clicked on should be the decider for the rest of them as to which they should all be swapped to.

When clicking a segment **(new since video was recorded)**:
- [ ] Clicking without any modifier key should split the segment where clicked. Currently this has the extra complication of entering an insertion mode where the point of the segment split can be slid within the segment and left click confirms it while right click cancels. We want to remove that extra step and just insert directly. An overlay should be drawn in cases where the cursor is hovered over the segment such that clicking would insert a split, it should be a short line segment (20px in diameter) running perpendicular to the curve.
- [ ] <kbd>Ctrl</kbd> clicking should delete the segment that's clicked. Currently a user would have to split the segment then delete the newly created point.
- [ ] Clicking and dragging without any modifier key should [mold the segment](https://pomax.github.io/bezierinfo/#molding) as per #615.

When a single handle is selected:
- [x] <kbd>R</kbd> rotates about its anchor (#2180)
- [x] <kbd>S</kbd> scales about its anchor (#2180)

## Pen tool

When no drawing has begun yet (pointer is up):
- [x] **(New since video was recorded)** clicking a path's endpoint should begin drawing from it with an initial handle that's colinear and equidistant from the opposite handle, instead of the current behavior where clicking the endpoint begins drawing the segment with a zero-length handle. If the user wants the current behavior, they can simply click the endpoint anchor a second time to make the current segment's handle zero-length. This solves the pain point that switching away from the Pen tool, then back to it, causes a discontinuity at the previous endpoint where the handles can't easily be opposite each other like when drawing consecutive segments normally. (#2295)

When stretching out a segment to be placed (pointer is up):
- [x] <kbd>Backspace</kbd>/<kbd>Delete</kbd> removes the current segment's handle (equivalent to clicking the current anchor) (#2180)
- [x] <kbd>G</kbd> grabs (translates) the current segment's connected outgoing handle (#2211)
- [x] <kbd>R</kbd> rotates the current segment's connected outgoing handle about its anchor (#2211)
- [x] <kbd>S</kbd> scales the current segment's connected outgoing handle about its anchor (#2211)

When dragging the handles of the segment being placed (pointer is down):
- [x] <kbd>C</kbd> toggles bent vs. colinear handles (instead of the momentary <kbd>Alt</kbd> in use now) (#2242)
- [x] <kbd>Alt</kbd> stops keeping the handles equidistant while pressed, as long as they're currently colinear (otherwise it does nothing). Exception: the reverse is the case (it keeps the handles equidistant while pressed) when the click-drag began with a single handle (either by click-dragging the current segment's connected anchor, click-dragging an endpoint of an existing not-yet-being-edited layer, or click-dragging the start point to close the shape) but <kbd>Ctrl</kbd> was pressed to keep (#2242)
- [x] <kbd>Ctrl</kbd> locks the current angle of the handle (already implemented), but should also work to be colinear with the opposite handle if being dragged out of an existing anchor point, or if that anchor point has no handle then it should use the angle of the actual curve segment entering that handle, i.e. its tangent angle at its endpoint (#2284)
- [ ] <kbd>Space</kbd> shifts the anchor (and handles) while pressed
- [ ] <kbd>Tab</kbd> swaps to dragging the opposite handle (native: teleports the cursor / web: enters pointer lock and draws a fake cursor at the new location)
- [ ] If the current segment is closing the shape by connecting to its start point, the handle that's being dragged should be the closing handle, not a fake handle on the opposite side, and all the above shortcuts should work on that anchor/handle group spanning the start/end point (<kbd>Tab</kbd> will modify the shape of the first segment while the current/final segment is still in-progress)
- [ ] All the above shortcuts should work with the "Bend Prev. Point" case where the current segment's connected anchor can be clicked to turn it sharp or click-dragged to bend its outgoing handle (turning its handles to not colinear), meaning <kbd>C</kbd> should be able to turn it back to colinear

---

- Not yet figured out: improving the UX for extending an endpoint with a new segment, by having control to extend the handle on either end of the new segment instead of just the new endpoint's side, and by locking the angle of the new endpoint side's handle to avoid a discontinuity. [Video](https://files.keavon.com/-/PoliticalGiantQueensnake/capture_22_.mp4).
Use KeyNoRepeat instead of Key to avoid potential user confusion that Tab shouldn't be held in to swap dragged handles
If you are dragging a handle with the path tool, holding <kbd>Tab</kbd> makes everything appear to be very buggy due to key repeat, since it's a toggle-based control:
https://github.com/user-attachments/assets/29779385-83f6-4143-95b4-0323bc7bf34f

It would also be worth auditing the editor for other toggle-based controls where accidentally holding the key would result in unintended effects. That has been moved to this issue: #2266


Discussion / hints:
Aight, I start with it. Wish me luck
Here are some more potential additions (coming from features I found useful in an abandoned vector graphics editor I had been working on years ago; originally listed [here](https://news.ycombinator.com/item?id=41854972)):

- shift+G/R/S (or some other modifier) for setting handle mode (bent, colinear, and colinear+equidistant respectively) is a potential intuitive keybind option kinda following what regular g/r/s do;
- that handle mode can be displayed visually as different icons on the point (square for bent, rectangle aligned to angle for colinear, and perhaps circle for equidistant, though that may get confusing with handles being circles; ellipse?)
- allow both rotate and scale at the same time (maybe never useful but I still had this ¯\\\_(ツ)\_/¯)
- middle-mouse-dragging while holding an element should ignore the delta mouse movement during the movement (currently doing this in Graphite results in jumpiness)
- another option for visual indicators r/g/s:
  - g: a line from the starting mouse position in the canvas to where it'd be dragged to
  - s: line from the anchor point through the current and original mouse position with different colors (i.e. the ratio of the color lengths is the ratio of scaling)
  - r: lines from the anchor to original & current mouse position, with an arc in between (very busy-looking, don't quite like it)

Some general actions:
- cut a path into two, snapping to cutting on an existing point if near enough (potentially, if dragging, could cut into three segments)
- join paths if two end-points are selected (incl. closing a path)
- select all points linked to currently already selected ones

[screen recording](https://github.com/user-attachments/assets/f64318f6-7f70-4b6e-88ad-33edaccffed8) showing some of these in my editor


Thank you for posting those here @dzaima! The feedback and ideas are very useful.
Hey, I plan working on the remaining few things for the path tool. I will be traveling for the next few weeks so wont be able to start anytime soon. Although if anyone else wants to pick them up before im back feel free!
@DaraghD hello again, I hope your travels were fruitful! Just wanted to see if you're interested in returning to some of the bullet points in this issue. In particular, the Pen tool ones aren't being attempted by anybody yet so those would be free to work on anytime you'd like, perhaps if you have some time over the holidays. Cheers!
#2160 will add the following requested features-
> - [x] <kbd>Shift</kbd> locks the handle angle to 15° increments
>   - [x] (shouldn't interfere with <kbd>Shift</kbd>-clicking handles for multi-selecting points such as handles)
> - [x] <kbd>Ctrl</kbd> locks the current angle of the handle
completed #2180 


Fix patch:
diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs
index afd581873f..c87ec64027 100644
--- a/editor/src/messages/input_mapper/input_mappings.rs
+++ b/editor/src/messages/input_mapper/input_mappings.rs
@@ -212,7 +212,7 @@ pub fn input_mappings() -> Mapping {
 		entry!(KeyDown(Backspace); modifiers=[Accel], action_dispatch=PathToolMessage::DeleteAndBreakPath),
 		entry!(KeyDown(Delete); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
 		entry!(KeyDown(Backspace); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
-		entry!(KeyDown(Tab); action_dispatch=PathToolMessage::SwapSelectedHandles),
+		entry!(KeyDownNoRepeat(Tab); action_dispatch=PathToolMessage::SwapSelectedHandles),
 		entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { direct_insert_without_sliding: Control, extend_selection: Shift, lasso_select: Control }),
 		entry!(KeyDown(MouseRight); action_dispatch=PathToolMessage::RightClick),
 		entry!(KeyDown(Escape); action_dispatch=PathToolMessage::Escape),
@@ -254,7 +254,7 @@ pub fn input_mappings() -> Mapping {
 		entry!(KeyDown(KeyJ); modifiers=[Accel], action_dispatch=ToolMessage::Path(PathToolMessage::ClosePath)),
 		//
 		// PenToolMessage
-		entry!(PointerMove; refresh_keys=[Control, Alt, Shift], action_dispatch=PenToolMessage::PointerMove { snap_angle: Shift, break_handle: Alt, lock_angle: Control}),
+		entry!(PointerMove; refresh_keys=[Control, Alt, Shift, KeyC], action_dispatch=PenToolMessage::PointerMove { snap_angle: Shift, break_handle: Alt, lock_angle: Control, colinear: KeyC }),
 		entry!(KeyDown(MouseLeft); action_dispatch=PenToolMessage::DragStart { append_to_selected: Shift }),
 		entry!(KeyUp(MouseLeft); action_dispatch=PenToolMessage::DragStop),
 		entry!(KeyDown(MouseRight); action_dispatch=PenToolMessage::Confirm),
diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs
index 70948a0bb1..275c7e6c2b 100644
--- a/editor/src/messages/tool/tool_messages/path_tool.rs
+++ b/editor/src/messages/tool/tool_messages/path_tool.rs
@@ -1185,16 +1185,9 @@ impl Fsm for PathToolFsmState {
 					.push(HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]));
 
 				let drag_anchor = HintInfo::keys([Key::Space], "Drag Anchor");
-				let point_select_state_hint_group = match dragging_state.point_select_state {
-					PointSelectState::HandleNoPair => {
-						let mut hints = vec![drag_anchor];
-						hints.push(HintInfo::keys([Key::Shift], "Snap 15°"));
-						hints.push(HintInfo::keys([Key::Control], "Lock Angle"));
-						hints
-					}
-					PointSelectState::HandleWithPair => {
-						let mut hints = vec![drag_anchor];
-						hints.push(HintInfo::keys([Key::Tab], "Swap Selected Handles"));
+				let toggle_group = match dragging_state.point_select_state {
+					PointSelectState::HandleNoPair | PointSelectState::HandleWithPair => {
+						let mut hints = vec![HintInfo::keys([Key::Tab], "Swap Selected Handles")];
 						hints.push(HintInfo::keys(
 							[Key::KeyC],
 							if colinear == ManipulatorAngle::Colinear {
@@ -1203,18 +1196,40 @@ impl Fsm for PathToolFsmState {
 								"Make Handles Colinear"
 							},
 						));
+						hints
+					}
+					PointSelectState::Anchor => Vec::new(),
+				};
+				let hold_group = match dragging_state.point_select_state {
+					PointSelectState::HandleNoPair => {
+						let mut hints = vec![];
 						if colinear != ManipulatorAngle::Free {
 							hints.push(HintInfo::keys([Key::Alt], "Equidistant Handles"));
 						}
 						hints.push(HintInfo::keys([Key::Shift], "Snap 15°"));
 						hints.push(HintInfo::keys([Key::Control], "Lock Angle"));
+						hints.push(drag_anchor);
+						hints
+					}
+					PointSelectState::HandleWithPair => {
+						let mut hints = vec![];
+						if colinear != ManipulatorAngle::Free {
+							hints.push(HintInfo::keys([Key::Alt], "Equidistant Handles"));
+						}
+						hints.push(HintInfo::keys([Key::Shift], "Snap 15°"));
+						hints.push(HintInfo::keys([Key::Control], "Lock Angle"));
+						hints.push(drag_anchor);
 						hints
 					}
 					PointSelectState::Anchor => Vec::new(),
 				};
 
-				if !point_select_state_hint_group.is_empty() {
-					dragging_hint_data.0.push(HintGroup(point_select_state_hint_group));
+				if !toggle_group.is_empty() {
+					dragging_hint_data.0.push(HintGroup(toggle_group));
+				}
+
+				if !hold_group.is_empty() {
+					dragging_hint_data.0.push(HintGroup(hold_group));
 				}
 
 				dragging_hint_data
diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs
index 689eac307a..960f9804a4 100644
--- a/editor/src/messages/tool/tool_messages/pen_tool.rs
+++ b/editor/src/messages/tool/tool_messages/pen_tool.rs
@@ -15,7 +15,7 @@ use bezier_rs::{Bezier, BezierHandles};
 use graph_craft::document::NodeId;
 use graphene_core::vector::{PointId, VectorModificationType};
 use graphene_core::Color;
-use graphene_std::vector::{HandleId, SegmentId};
+use graphene_std::vector::{HandleId, ManipulatorPointId, SegmentId, VectorData};
 
 #[derive(Default)]
 pub struct PenTool {
@@ -56,8 +56,8 @@ pub enum PenToolMessage {
 	Confirm,
 	DragStart { append_to_selected: Key },
 	DragStop,
-	PointerMove { snap_angle: Key, break_handle: Key, lock_angle: Key },
-	PointerOutsideViewport { snap_angle: Key, break_handle: Key, lock_angle: Key },
+	PointerMove { snap_angle: Key, break_handle: Key, lock_angle: Key, colinear: Key },
+	PointerOutsideViewport { snap_angle: Key, break_handle: Key, lock_angle: Key, colinear: Key },
 	Redo,
 	Undo,
 	UpdateOptions(PenOptionsUpdate),
@@ -71,7 +71,7 @@ pub enum PenToolMessage {
 enum PenToolFsmState {
 	#[default]
 	Ready,
-	DraggingHandle,
+	DraggingHandle(HandleMode),
 	PlacingAnchor,
 	GRSHandle,
 }
@@ -174,7 +174,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PenTool
 				PointerMove,
 				FinalPosition
 			),
-			PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor => actions!(PenToolMessageDiscriminant;
+			PenToolFsmState::DraggingHandle(_) | PenToolFsmState::PlacingAnchor => actions!(PenToolMessageDiscriminant;
 				DragStart,
 				DragStop,
 				PointerMove,
@@ -203,6 +203,7 @@ struct ModifierState {
 	snap_angle: bool,
 	lock_angle: bool,
 	break_handle: bool,
+	colinear: bool,
 }
 #[derive(Clone, Debug)]
 struct LastPoint {
@@ -212,6 +213,17 @@ struct LastPoint {
 	handle_start: DVec2,
 }
 
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+enum HandleMode {
+	/// Pressing 'C' breaks colinearity
+	Free,
+	/// Pressing 'Alt': Handle length is locked
+	#[default]
+	ColinearLocked,
+	/// Pressing 'Alt': Handles are equidistant
+	ColinearEquidistant,
+}
+
 #[derive(Clone, Debug, Default)]
 struct PenToolData {
 	snap_manager: SnapManager,
@@ -222,6 +234,9 @@ struct PenToolData {
 	next_handle_start: DVec2,
 
 	g1_continuous: bool,
+	toggle_colinear_debounce: bool,
+
+	segment_end_before_bent: Option<SegmentId>,
 
 	angle: f64,
 	auto_panning: AutoPanning,
@@ -229,7 +244,11 @@ struct PenToolData {
 
 	buffering_merged_vector: bool,
 
-	before_grs_pos: DVec2,
+	previous_handle_start_pos: DVec2,
+	previous_handle_end_pos: Option<DVec2>,
+	alt_press: bool,
+
+	handle_mode: HandleMode,
 }
 impl PenToolData {
 	fn latest_point(&self) -> Option<&LastPoint> {
@@ -265,19 +284,24 @@ impl PenToolData {
 	}
 
 	/// If the user places the anchor on top of the previous anchor, it becomes sharp and the outgoing handle may be dragged.
-	fn bend_from_previous_point(&mut self, snap_data: SnapData, transform: DAffine2) {
+	fn bend_from_previous_point(&mut self, snap_data: SnapData, transform: DAffine2, layer: LayerNodeIdentifier) {
 		self.g1_continuous = true;
 		let document = snap_data.document;
 		self.next_handle_start = self.next_point;
+		let vector_data = document.network_interface.compute_modified_vector(layer).unwrap();
 
 		// Break the control
 		let Some(last_pos) = self.latest_point().map(|point| point.pos) else { return };
+
 		let transform = document.metadata().document_to_viewport * transform;
 		let on_top = transform.transform_point2(self.next_point).distance_squared(transform.transform_point2(last_pos)) < crate::consts::SNAP_POINT_TOLERANCE.powi(2);
 		if on_top {
 			if let Some(point) = self.latest_point_mut() {
 				point.in_segment = None;
 			}
+
+			self.segment_end_before_bent = vector_data.segment_domain.ids().last().copied();
+			self.handle_mode = HandleMode::Free;
 			self.handle_end = None;
 		}
 	}
@@ -326,6 +350,7 @@ impl PenToolData {
 
 		let points = [start, end];
 		let id = SegmentId::generate();
+		self.segment_end_before_bent = Some(id);
 		let modification_type = VectorModificationType::InsertSegment { id, points, handles };
 		responses.add(GraphOperationMessage::Vector { layer, modification_type });
 
@@ -351,19 +376,107 @@ impl PenToolData {
 		Some(if close_subpath { PenToolFsmState::Ready } else { PenToolFsmState::PlacingAnchor })
 	}
 
-	fn drag_handle(&mut self, snap_data: SnapData, transform: DAffine2, mouse: DVec2, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> {
-		let colinear = !self.modifiers.break_handle && self.handle_end.is_some();
+	fn drag_handle(&mut self, snap_data: SnapData, transform: DAffine2, mouse: DVec2, responses: &mut VecDeque<Message>, layer: Option<LayerNodeIdentifier>) -> Option<PenToolFsmState> {
+		let colinear = (self.handle_mode == HandleMode::ColinearEquidistant && self.modifiers.break_handle) || (self.handle_mode == HandleMode::ColinearLocked && !self.modifiers.break_handle);
+		let document = snap_data.document;
 		self.next_handle_start = self.compute_snapped_angle(snap_data, transform, colinear, mouse, Some(self.next_point), false);
-		if let Some(handle_end) = self.handle_end.as_mut().filter(|_| colinear) {
-			*handle_end = self.next_point * 2. - self.next_handle_start;
-			self.g1_continuous = true;
-		} else {
-			self.g1_continuous = false;
+		let Some(layer) = layer else { return Some(PenToolFsmState::DraggingHandle(self.handle_mode)) };
+		let vector_data = document.network_interface.compute_modified_vector(layer)?;
+
+		match self.handle_mode {
+			HandleMode::ColinearLocked | HandleMode::ColinearEquidistant => {
+				self.g1_continuous = true;
+				self.colinear(responses, layer, self.next_handle_start, self.next_point, &vector_data);
+				self.adjust_handle_length(responses, layer, &vector_data);
+			}
+			HandleMode::Free => {
+				self.g1_continuous = false;
+			}
 		}
 
 		responses.add(OverlaysMessage::Draw);
 
-		Some(PenToolFsmState::DraggingHandle)
+		Some(PenToolFsmState::DraggingHandle(self.handle_mode))
+	}
+
+	/// Makes the opposite handle equidistant or locks its length.
+	fn adjust_handle_length(&mut self, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier, vector_data: &VectorData) {
+		let Some(latest) = self.latest_point() else { return };
+		let anchor_pos = latest.pos;
+
+		match self.handle_mode {
+			HandleMode::ColinearEquidistant => self.adjust_equidistant_handle(anchor_pos, responses, layer, vector_data),
+			HandleMode::ColinearLocked => self.adjust_locked_length_handle(anchor_pos, responses, layer),
+			HandleMode::Free => {} // No adjustments needed in free mode
+		}
+	}
+
+	fn colinear(&mut self, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier, handle_start: DVec2, anchor_point: DVec2, vector_data: &VectorData) {
+		let Some(direction) = (anchor_point - handle_start).try_normalize() else {
+			log::trace!("Skipping colinear adjustment: handle_start and anchor_point are too close!");
+			return;
+		};
+
+		let handle_offset = if let Some(handle_end) = self.handle_end {
+			(handle_end - anchor_point).length()
+		} else {
+			let Some(segment) = self.segment_end_before_bent else { return };
+			let end_handle = ManipulatorPointId::EndHandle(segment);
+			let Some(end_handle) = end_handle.get_position(vector_data) else { return };
+			(end_handle - anchor_point).length()
+		};
+		let new_handle_position = anchor_point + handle_offset * direction;
+		self.update_handle_position(new_handle_position, anchor_point, responses, layer);
+	}
+
+	fn adjust_equidistant_handle(&mut self, anchor_pos: DVec2, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier, vector_data: &VectorData) {
+		if self.modifiers.break_handle {
+			self.store_handle(vector_data);
+			self.alt_press = true;
+			let new_position = self.next_point * 2. - self.next_handle_start;
+			self.update_handle_position(new_position, anchor_pos, responses, layer);
+		} else {
+			self.restore_previous_handle(anchor_pos, responses, layer);
+		}
+	}
+
+	fn adjust_locked_length_handle(&mut self, anchor_pos: DVec2, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier) {
+		if !self.modifiers.break_handle {
+			let new_position = self.next_point * 2. - self.next_handle_start;
+			self.update_handle_position(new_position, anchor_pos, responses, layer);
+		}
+	}
+
+	/// Temporarily stores the opposite handle position to revert back when Alt is released in equidistant mode.
+	fn store_handle(&mut self, vector_data: &VectorData) {
+		if !self.alt_press {
+			self.previous_handle_end_pos = self.handle_end.or_else(|| {
+				let segment = self.segment_end_before_bent?;
+				ManipulatorPointId::EndHandle(segment).get_position(vector_data)
+			});
+		}
+	}
+
+	fn restore_previous_handle(&mut self, anchor_pos: DVec2, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier) {
+		if self.alt_press {
+			self.alt_press = false;
+			if let Some(previous_handle) = self.previous_handle_end_pos {
+				self.update_handle_position(previous_handle, anchor_pos, responses, layer);
+			}
+			self.previous_handle_end_pos = None; // Reset storage
+		}
+	}
+
+	fn update_handle_position(&mut self, new_position: DVec2, anchor_pos: DVec2, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier) {
+		if let Some(handle) = self.handle_end.as_mut() {
+			*handle = new_position;
+		} else {
+			let Some(segment) = self.segment_end_before_bent else { return };
+			let relative_position = new_position - anchor_pos;
+			let modification_type = VectorModificationType::SetEndHandle { segment, relative_position };
+
+			responses.add(GraphOperationMessage::Vector { layer, modification_type });
+		}
 	}
 
 	fn place_anchor(&mut self, snap_data: SnapData, transform: DAffine2, mouse: DVec2, preferences: &PreferencesMessageHandler, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> {
@@ -455,7 +568,7 @@ impl PenToolData {
 		}
 
 		if let Some(relative) = relative.map(|layer| transform.transform_point2(layer)) {
-			if (relative - document_pos) != DVec2::ZERO {
+			if (relative - document_pos) != DVec2::ZERO && (relative - document_pos).length_squared() > f64::EPSILON * 100. {
 				self.angle = -(relative - document_pos).angle_to(DVec2::X)
 			}
 		}
@@ -571,36 +684,67 @@ impl Fsm for PenToolFsmState {
 		let ToolMessage::Pen(event) = event else { return self };
 		match (self, event) {
 			(PenToolFsmState::PlacingAnchor | PenToolFsmState::GRSHandle, PenToolMessage::GRS { grab, rotate, scale }) => {
-				let Some(latest) = tool_data.latest_point_mut() else { return PenToolFsmState::PlacingAnchor };
 				let Some(layer) = layer else { return PenToolFsmState::PlacingAnchor };
 
-				if latest.handle_start != latest.pos {
-					let viewport = document.metadata().transform_to_viewport(layer);
-					let last_point = viewport.transform_point2(latest.pos);
-					let handle = viewport.transform_point2(latest.handle_start);
-
-					if input.keyboard.key(grab) {
-						responses.add(TransformLayerMessage::BeginGrabPen { last_point, handle });
-					} else if input.keyboard.key(rotate) {
-						responses.add(TransformLayerMessage::BeginRotatePen { last_point, handle });
-					} else if input.keyboard.key(scale) {
-						responses.add(TransformLayerMessage::BeginScalePen { last_point, handle });
-					}
+				let Some(latest) = tool_data.latest_point() else { return PenToolFsmState::PlacingAnchor };
+				if latest.handle_start == latest.pos {
+					return PenToolFsmState::PlacingAnchor;
+				}
+
+				let viewport = document.metadata().transform_to_viewport(layer);
+				let last_point = viewport.transform_point2(latest.pos);
+				let handle = viewport.transform_point2(latest.handle_start);
 
-					tool_data.before_grs_pos = latest.handle_start;
+				if input.keyboard.key(grab) {
+					responses.add(TransformLayerMessage::BeginGrabPen { last_point, handle });
+				} else if input.keyboard.key(rotate) {
+					responses.add(TransformLayerMessage::BeginRotatePen { last_point, handle });
+				} else if input.keyboard.key(scale) {
+					responses.add(TransformLayerMessage::BeginScalePen { last_point, handle });
 				}
 
+				tool_data.previous_handle_start_pos = latest.handle_start;
+
+				// Store the handle_end position
+				let segment = tool_data.segment_end_before_bent;
+				if let Some(segment) = segment {
+					let vector_data = document.network_interface.compute_modified_vector(layer).unwrap();
+					tool_data.previous_handle_end_pos = ManipulatorPointId::EndHandle(segment).get_position(&vector_data);
+				}
 				PenToolFsmState::GRSHandle
 			}
-			(PenToolFsmState::GRSHandle, PenToolMessage::FinalPosition { final_position: final_pos }) => {
+			(PenToolFsmState::GRSHandle, PenToolMessage::FinalPosition { final_position }) => {
 				let Some(layer) = layer else { return PenToolFsmState::GRSHandle };
+				let vector_data = document.network_interface.compute_modified_vector(layer);
+				let Some(vector_data) = vector_data else { return PenToolFsmState::GRSHandle };
 
 				if let Some(latest_pt) = tool_data.latest_point_mut() {
 					let layer_space_to_viewport = document.metadata().transform_to_viewport(layer);
-					let final_pos = layer_space_to_viewport.inverse().transform_point2(final_pos);
+					let final_pos = layer_space_to_viewport.inverse().transform_point2(final_position);
 					latest_pt.handle_start = final_pos;
 				}
 
+				// Making the end handle colinear
+				match tool_data.handle_mode {
+					HandleMode::Free => {}
+					HandleMode::ColinearEquidistant | HandleMode::ColinearLocked => {
+						if let Some((latest, segment)) = tool_data.latest_point().zip(tool_data.segment_end_before_bent) {
+							let handle = ManipulatorPointId::EndHandle(segment).get_position(&vector_data);
+							let Some(handle) = handle else { return PenToolFsmState::GRSHandle };
+
+							let Some(direction) = (latest.pos - latest.handle_start).try_normalize() else {
+								log::trace!("Skipping handle adjustment: latest.pos and latest.handle_start are too close!");
+								return PenToolFsmState::GRSHandle;
+							};
+
+							let relative_distance = (handle - latest.pos).length();
+							let relative_position = relative_distance * direction;
+							let modification_type = VectorModificationType::SetEndHandle { segment, relative_position };
+							responses.add(GraphOperationMessage::Vector { layer, modification_type });
+						}
+					}
+				}
+
 				responses.add(OverlaysMessage::Draw);
 
 				PenToolFsmState::GRSHandle
@@ -614,6 +758,7 @@ impl Fsm for PenToolFsmState {
 					snap_angle: Key::Control,
 					break_handle: Key::Alt,
 					lock_angle: Key::Shift,
+					colinear: Key::KeyC,
 				});
 
 				PenToolFsmState::PlacingAnchor
@@ -622,7 +767,9 @@ impl Fsm for PenToolFsmState {
 				tool_data.next_point = input.mouse.position;
 				tool_data.next_handle_start = input.mouse.position;
 
-				let previous = tool_data.before_grs_pos;
+				let Some(layer) = layer else { return PenToolFsmState::GRSHandle };
+
+				let previous = tool_data.previous_handle_start_pos;
 				if let Some(latest) = tool_data.latest_point_mut() {
 					latest.handle_start = previous;
 				}
@@ -632,8 +779,16 @@ impl Fsm for PenToolFsmState {
 					snap_angle: Key::Control,
 					break_handle: Key::Alt,
 					lock_angle: Key::Shift,
+					colinear: Key::KeyC,
 				});
 
+				// Set the handle-end back to original position
+				if let Some(((latest, segment), handle_end)) = tool_data.latest_point().zip(tool_data.segment_end_before_bent).zip(tool_data.previous_handle_end_pos) {
+					let relative = handle_end - latest.pos;
+					let modification_type = VectorModificationType::SetEndHandle { segment, relative_position: relative };
+					responses.add(GraphOperationMessage::Vector { layer, modification_type });
+				}
+
 				PenToolFsmState::PlacingAnchor
 			}
 			(_, PenToolMessage::SelectionChanged) => {
@@ -680,9 +835,14 @@ impl Fsm for PenToolFsmState {
 					// Draw the line between the currently-being-placed anchor and its incoming handle (opposite the one currently being dragged out)
 					overlay_context.line(next_anchor, handle_end, None);
 
+					if self == PenToolFsmState::PlacingAnchor && anchor_start != handle_start && tool_data.modifiers.lock_angle {
+						// Draw the line between the currently-being-placed anchor and last-placed point (Lock angle bent overlays)
+						overlay_context.dashed_line(anchor_start, next_anchor, None, Some(4.), Some(4.), Some(0.5));
+					}
+
 					path_overlays(document, shape_editor, &mut overlay_context);
 
-					if self == PenToolFsmState::DraggingHandle && valid(next_anchor, handle_end) {
+					if self == PenToolFsmState::DraggingHandle(tool_data.handle_mode) && valid(next_anchor, handle_end) {
 						// Draw the handle circle for the currently-being-dragged-out incoming handle (opposite the one currently being dragged out)
 						overlay_context.manipulator_handle(handle_end, false, None);
 					}
@@ -696,12 +856,12 @@ impl Fsm for PenToolFsmState {
 					path_overlays(document, shape_editor, &mut overlay_context);
 				}
 
-				if self == PenToolFsmState::DraggingHandle && valid(next_anchor, next_handle_start) {
+				if self == PenToolFsmState::DraggingHandle(tool_data.handle_mode) && valid(next_anchor, next_handle_start) {
 					// Draw the handle circle for the currently-being-dragged-out outgoing handle (the one currently being dragged out, under the user's cursor)
 					overlay_context.manipulator_handle(next_handle_start, false, None);
 				}
 
-				if self == PenToolFsmState::DraggingHandle {
+				if self == PenToolFsmState::DraggingHandle(tool_data.handle_mode) {
 					// Draw the anchor square for the most recently placed anchor
 					overlay_context.manipulator_anchor(next_anchor, false, None);
 				}
@@ -720,11 +880,11 @@ impl Fsm for PenToolFsmState {
 			}
 			(PenToolFsmState::Ready, PenToolMessage::DragStart { append_to_selected }) => {
 				responses.add(DocumentMessage::StartTransaction);
-
+				tool_data.handle_mode = HandleMode::Free;
 				tool_data.create_initial_point(document, input, responses, tool_options, input.keyboard.key(append_to_selected), preferences);
 
 				// Enter the dragging handle state while the mouse is held down, allowing the user to move the mouse and position the handle
-				PenToolFsmState::DraggingHandle
+				PenToolFsmState::DraggingHandle(tool_data.handle_mode)
 			}
 			(_, PenToolMessage::AddPointLayerPosition { layer, viewport }) => {
 				tool_data.add_point_layer_position(document, responses, layer, viewport);
@@ -739,13 +899,15 @@ impl Fsm for PenToolFsmState {
 				let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position));
 				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);
+
 				// Early return if the buffer was started and this message is being run again after the buffer (so that place_anchor updates the state with the newly merged vector)
 				if tool_data.buffering_merged_vector {
 					tool_data.buffering_merged_vector = false;
-					tool_data.bend_from_previous_point(SnapData::new(document, input), transform);
+					tool_data.handle_mode = HandleMode::ColinearLocked;
+					tool_data.bend_from_previous_point(SnapData::new(document, input), transform, layer.unwrap());
 					tool_data.place_anchor(SnapData::new(document, input), transform, input.mouse.position, preferences, responses);
 					tool_data.buffering_merged_vector = false;
-					PenToolFsmState::DraggingHandle
+					PenToolFsmState::DraggingHandle(tool_data.handle_mode)
 				} else {
 					if tool_data.handle_end.is_some() {
 						responses.add(DocumentMessage::StartTransaction);
@@ -774,37 +936,80 @@ impl Fsm for PenToolFsmState {
 					last_point.handle_start = last_point.pos;
 					responses.add(OverlaysMessage::Draw);
 				} else {
-					log::warn!("No latest point available to modify handle_start.");
+					log::trace!("No latest point available to modify handle_start.");
 				}
 				self
 			}
-			(PenToolFsmState::DraggingHandle, PenToolMessage::DragStop) => tool_data
+			(PenToolFsmState::DraggingHandle(_), PenToolMessage::DragStop) => tool_data
 				.finish_placing_handle(SnapData::new(document, input), transform, preferences, responses)
 				.unwrap_or(PenToolFsmState::PlacingAnchor),
-			(PenToolFsmState::DraggingHandle, PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }) => {
+			(
+				PenToolFsmState::DraggingHandle(_),
+				PenToolMessage::PointerMove {
+					snap_angle,
+					break_handle,
+					lock_angle,
+					colinear,
+				},
+			) => {
 				tool_data.modifiers = ModifierState {
 					snap_angle: input.keyboard.key(snap_angle),
 					lock_angle: input.keyboard.key(lock_angle),
 					break_handle: input.keyboard.key(break_handle),
+					colinear: input.keyboard.key(colinear),
 				};
 				let snap_data = SnapData::new(document, input);
 
-				let state = tool_data.drag_handle(snap_data, transform, input.mouse.position, responses).unwrap_or(PenToolFsmState::Ready);
+				if tool_data.modifiers.colinear && !tool_data.toggle_colinear_debounce {
+					tool_data.handle_mode = match tool_data.handle_mode {
+						HandleMode::Free => HandleMode::ColinearEquidistant,
+						HandleMode::ColinearEquidistant | HandleMode::ColinearLocked => HandleMode::Free,
+					};
+					tool_data.toggle_colinear_debounce = true;
+				}
+
+				if !tool_data.modifiers.colinear {
+					tool_data.toggle_colinear_debounce = false;
+				}
+
+				let state = tool_data.drag_handle(snap_data, transform, input.mouse.position, responses, layer).unwrap_or(PenToolFsmState::Ready);
 
 				// Auto-panning
 				let messages = [
-					PenToolMessage::PointerOutsideViewport { snap_angle, break_handle, lock_angle }.into(),
-					PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }.into(),
+					PenToolMessage::PointerOutsideViewport {
+						snap_angle,
+						break_handle,
+						lock_angle,
+						colinear,
+					}
+					.into(),
+					PenToolMessage::PointerMove {
+						snap_angle,
+						break_handle,
+						lock_angle,
+						colinear,
+					}
+					.into(),
 				];
 				tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
 
 				state
 			}
-			(PenToolFsmState::PlacingAnchor, PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }) => {
+			(
+				PenToolFsmState::PlacingAnchor,
+				PenToolMessage::PointerMove {
+					snap_angle,
+					break_handle,
+					lock_angle,
+					colinear,
+				},
+			) => {
+				tool_data.alt_press = false;
 				tool_data.modifiers = ModifierState {
 					snap_angle: input.keyboard.key(snap_angle),
 					lock_angle: input.keyboard.key(lock_angle),
 					break_handle: input.keyboard.key(break_handle),
+					colinear: input.keyboard.key(colinear),
 				};
 				let state = tool_data
 					.place_anchor(SnapData::new(document, input), transform, input.mouse.position, preferences, responses)
@@ -812,8 +1017,20 @@ impl Fsm for PenToolFsmState {
 
 				// Auto-panning
 				let messages = [
-					PenToolMessage::PointerOutsideViewport { snap_angle, break_handle, lock_angle }.into(),
-					PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }.into(),
+					PenToolMessage::PointerOutsideViewport {
+						snap_angle,
+						break_handle,
+						lock_angle,
+						colinear,
+					}
+					.into(),
+					PenToolMessage::PointerMove {
+						snap_angle,
+						break_handle,
+						lock_angle,
+						colinear,
+					}
+					.into(),
 				];
 				tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
 
@@ -824,11 +1041,11 @@ impl Fsm for PenToolFsmState {
 				responses.add(OverlaysMessage::Draw);
 				self
 			}
-			(PenToolFsmState::DraggingHandle, PenToolMessage::PointerOutsideViewport { .. }) => {
+			(PenToolFsmState::DraggingHandle(mode), PenToolMessage::PointerOutsideViewport { .. }) => {
 				// Auto-panning
 				let _ = tool_data.auto_panning.shift_viewport(input, responses);
 
-				PenToolFsmState::DraggingHandle
+				PenToolFsmState::DraggingHandle(mode)
 			}
 			(PenToolFsmState::PlacingAnchor, PenToolMessage::PointerOutsideViewport { .. }) => {
 				// Auto-panning
@@ -836,17 +1053,37 @@ impl Fsm for PenToolFsmState {
 
 				PenToolFsmState::PlacingAnchor
 			}
-			(state, PenToolMessage::PointerOutsideViewport { snap_angle, break_handle, lock_angle }) => {
+			(
+				state,
+				PenToolMessage::PointerOutsideViewport {
+					snap_angle,
+					break_handle,
+					lock_angle,
+					colinear,
+				},
+			) => {
 				// Auto-panning
 				let messages = [
-					PenToolMessage::PointerOutsideViewport { snap_angle, break_handle, lock_angle }.into(),
-					PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }.into(),
+					PenToolMessage::PointerOutsideViewport {
+						snap_angle,
+						break_handle,
+						lock_angle,
+						colinear,
+					}
+					.into(),
+					PenToolMessage::PointerMove {
+						snap_angle,
+						break_handle,
+						lock_angle,
+						colinear,
+					}
+					.into(),
 				];
 				tool_data.auto_panning.stop(&messages, responses);
 
 				state
 			}
-			(PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor, PenToolMessage::Confirm) => {
+			(PenToolFsmState::DraggingHandle(..) | PenToolFsmState::PlacingAnchor, PenToolMessage::Confirm) => {
 				responses.add(DocumentMessage::EndTransaction);
 				tool_data.handle_end = None;
 				tool_data.latest_points.clear();
@@ -866,7 +1103,7 @@ impl Fsm for PenToolFsmState {
 
 				PenToolFsmState::Ready
 			}
-			(PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor, PenToolMessage::Undo) => {
+			(PenToolFsmState::DraggingHandle(..) | PenToolFsmState::PlacingAnchor, PenToolMessage::Undo) => {
 				if tool_data.point_index > 0 {
 					tool_data.point_index -= 1;
 					tool_data
@@ -906,16 +1143,40 @@ impl Fsm for PenToolFsmState {
 				HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Add Sharp Point"), HintInfo::mouse(MouseMotion::LmbDrag, "Add Smooth Point")]),
 				HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, ""), HintInfo::mouse(MouseMotion::LmbDrag, "Bend Prev. Point").prepend_slash()]),
 			]),
-			PenToolFsmState::DraggingHandle => HintData(vec![
-				HintGroup(vec![
+			PenToolFsmState::DraggingHandle(mode) => {
+				let mut dragging_hint_data = HintData(Vec::new());
+				dragging_hint_data.0.push(HintGroup(vec![
 					HintInfo::mouse(MouseMotion::Rmb, ""),
 					HintInfo::keys([Key::Escape], "").prepend_slash(),
 					HintInfo::keys([Key::Enter], "End Path").prepend_slash(),
-				]),
-				HintGroup(vec![HintInfo::keys([Key::Shift], "Snap 15°"), HintInfo::keys([Key::Control], "Lock Angle")]),
-				// TODO: Only show this if the handle being dragged is colinear, so don't show this when bending from the previous point (by clicking and dragging from the previously placed anchor)
-				HintGroup(vec![HintInfo::keys([Key::Alt], "Bend Handle")]),
-			]),
+				]));
+
+				let toggle_group = match mode {
+					HandleMode::Free => {
+						vec![HintInfo::keys([Key::KeyC], "Make Handles Colinear")]
+					}
+					HandleMode::ColinearLocked | HandleMode::ColinearEquidistant => {
+						vec![HintInfo::keys([Key::KeyC], "Break Colinear Handles")]
+					}
+				};
+
+				let mut common_hints = vec![HintInfo::keys([Key::Shift], "Snap 15°"), HintInfo::keys([Key::Control], "Lock Angle")];
+				let hold_group = match mode {
+					HandleMode::Free => common_hints,
+					HandleMode::ColinearLocked => {
+						common_hints.push(HintInfo::keys([Key::Alt], "Non-Equidistant Handles"));
+						common_hints
+					}
+					HandleMode::ColinearEquidistant => {
+						common_hints.push(HintInfo::keys([Key::Alt], "Equidistant Handles"));
+						common_hints
+					}
+				};
+
+				dragging_hint_data.0.push(HintGroup(toggle_group));
+				dragging_hint_data.0.push(HintGroup(hold_group));
+				dragging_hint_data
+			}
 		};
 
 		responses.add(FrontendMessage::UpdateInputHints { hint_data });


Test patch: