From 3fbcaf9093e15f8f18400809ad644004b46a7575 Mon Sep 17 00:00:00 2001 From: krVatsal Date: Sun, 26 Oct 2025 17:30:21 +0530 Subject: [PATCH 1/8] added feature to stop drawing when double clicked using pen tool Signed-off-by: krVatsal --- .../messages/tool/tool_messages/pen_tool.rs | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 0abab93f48..b5acb44813 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1,5 +1,5 @@ use super::tool_prelude::*; -use crate::consts::{COLOR_OVERLAY_BLUE, DEFAULT_STROKE_WIDTH, HIDE_HANDLE_DISTANCE, LINE_ROTATE_SNAP_ANGLE, SEGMENT_OVERLAY_SIZE}; +use crate::consts::{COLOR_OVERLAY_BLUE, DEFAULT_STROKE_WIDTH, DOUBLE_CLICK_MILLISECONDS, DRAG_THRESHOLD, HIDE_HANDLE_DISTANCE, LINE_ROTATE_SNAP_ANGLE, SEGMENT_OVERLAY_SIZE}; use crate::messages::input_mapper::utility_types::input_mouse::MouseKeys; use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; use crate::messages::portfolio::document::overlays::utility_functions::path_overlays; @@ -410,6 +410,9 @@ struct PenToolData { /// and Ctrl is pressed near the anchor to make it colinear with its opposite handle. angle_locked: bool, path_closed: bool, + last_click_time: Option, + last_click_pos: Option, + pending_double_click_confirm: bool, handle_mode: HandleMode, prior_segment_layer: Option, @@ -446,6 +449,18 @@ impl PenToolData { self.latest_points.clear(); self.point_index = 0; self.snap_manager.cleanup(responses); + self.pending_double_click_confirm = false; + self.last_click_time = None; + self.last_click_pos = None; + } + + fn update_click_timing(&mut self, time: u64, position: DVec2) -> bool { + let within_time = self.last_click_time.map(|last_time| time.saturating_sub(last_time) <= DOUBLE_CLICK_MILLISECONDS).unwrap_or(false); + let within_distance = self.last_click_pos.map(|last_pos| last_pos.distance(position) <= DRAG_THRESHOLD).unwrap_or(false); + let is_double_click = within_time && within_distance; + self.last_click_time = Some(time); + self.last_click_pos = Some(position); + is_double_click } /// Check whether target handle is primary, end, or `self.handle_end` @@ -1816,6 +1831,8 @@ impl Fsm for PenToolFsmState { self } (PenToolFsmState::Ready, PenToolMessage::DragStart { append_to_selected }) => { + tool_data.pending_double_click_confirm = false; + let _ = tool_data.update_click_timing(input.time, input.mouse.position); responses.add(DocumentMessage::StartTransaction); tool_data.handle_mode = HandleMode::Free; @@ -1838,6 +1855,14 @@ impl Fsm for PenToolFsmState { state } (PenToolFsmState::PlacingAnchor, PenToolMessage::DragStart { append_to_selected }) => { + let double_click = if tool_data.buffering_merged_vector { + false + } else { + tool_data.update_click_timing(input.time, input.mouse.position) + }; + if !tool_data.buffering_merged_vector { + tool_data.pending_double_click_confirm = double_click; + } 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); @@ -1892,9 +1917,16 @@ impl Fsm for PenToolFsmState { } (PenToolFsmState::DraggingHandle(_), PenToolMessage::DragStop) => { tool_data.cleanup_target_selections(shape_editor, layer, document, responses); - tool_data + let next_state = tool_data .finish_placing_handle(SnapData::new(document, input), transform, preferences, responses) - .unwrap_or(PenToolFsmState::PlacingAnchor) + .unwrap_or(PenToolFsmState::PlacingAnchor); + if tool_data.pending_double_click_confirm && matches!(next_state, PenToolFsmState::PlacingAnchor) { + tool_data.pending_double_click_confirm = false; + responses.add(PenToolMessage::Confirm); + } else { + tool_data.pending_double_click_confirm = false; + } + next_state } ( PenToolFsmState::DraggingHandle(_), From b92cf9f45b491f443ede3fbdae76dec4f46affee Mon Sep 17 00:00:00 2001 From: krVatsal Date: Sun, 30 Nov 2025 18:31:48 +0530 Subject: [PATCH 2/8] fixing build issue --- Cargo.lock | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ddb7df6e62..b2461ffe3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -724,9 +724,9 @@ dependencies = [ [[package]] name = "cef" -version = "140.3.1+140.1.14" +version = "140.3.6+140.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951391894583c255fd60b11f9a80397c9f8e1273d5b3a05531fcfa1365259059" +checksum = "c40c067add496f9b9b39a03df10d016640f8535dedcc8f04b8b0e9230dafa15b" dependencies = [ "cef-dll-sys", "libloading", @@ -735,9 +735,9 @@ dependencies = [ [[package]] name = "cef-dll-sys" -version = "140.3.1+140.1.14" +version = "140.3.6+140.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "208b1aa960cedcde7ac7590cb882a107caca0804242ac4060c488db233eef222" +checksum = "e593b55c861429d4264fad567a41ffef96ebb43b631e681f595c29b9a226258a" dependencies = [ "anyhow", "cmake", @@ -876,9 +876,9 @@ dependencies = [ [[package]] name = "color" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ae467d04a8a8aea5d9a49018a6ade2e4221d92968e8ce55a48c0b1164e5f698" +checksum = "a18ef4657441fb193b65f34dc39b3781f0dfec23d3bd94d0eeb4e88cde421edb" [[package]] name = "color_quant" @@ -3687,9 +3687,9 @@ dependencies = [ [[package]] name = "objc2-app-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.9.3", "block2", @@ -3700,9 +3700,9 @@ dependencies = [ [[package]] name = "objc2-core-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.9.3", "block2", @@ -3712,9 +3712,9 @@ dependencies = [ [[package]] name = "objc2-core-graphics" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ "bitflags 2.9.3", "libc", @@ -3733,9 +3733,9 @@ dependencies = [ [[package]] name = "objc2-core-video" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1989c3e76c7e978cab0ba9e6f4961cd00ed14ca21121444cc26877403bfb6303" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" dependencies = [ "bitflags 2.9.3", "objc2-core-foundation", @@ -3750,9 +3750,9 @@ checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "objc2-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.9.3", "block2", @@ -3762,9 +3762,9 @@ dependencies = [ [[package]] name = "objc2-ui-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ "bitflags 2.9.3", "objc2", @@ -4192,9 +4192,9 @@ checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" [[package]] name = "poly-cool" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8370a553db72b99cd797d84e98f4f3a43bc7b57a3d9f0c9f095cbb24683694c1" +checksum = "cb79f376772fbca123c950f4c2a74558bae8f8fd7d35037c093d5d53d45b46c2" dependencies = [ "arrayvec", ] From d3771415e4cedd79ed5e838e404a4ee98f8e2151 Mon Sep 17 00:00:00 2001 From: krVatsal Date: Mon, 12 Jan 2026 14:24:41 +0530 Subject: [PATCH 3/8] fix: fixed arguments in pen tool Signed-off-by: krVatsal --- editor/src/messages/tool/tool_messages/pen_tool.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index aa01d99746..bcc54ffe60 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1907,7 +1907,7 @@ impl Fsm for PenToolFsmState { (PenToolFsmState::DraggingHandle(_), PenToolMessage::DragStop) => { tool_data.cleanup_target_selections(shape_editor, layer, document, responses); let next_state = tool_data - .finish_placing_handle(SnapData::new(document, input), transform, preferences, responses) + .finish_placing_handle(SnapData::new(document, input, viewport), transform, responses) .unwrap_or(PenToolFsmState::PlacingAnchor); if tool_data.pending_double_click_confirm && matches!(next_state, PenToolFsmState::PlacingAnchor) { tool_data.pending_double_click_confirm = false; From a704272f1224b3370b8147ac0f1158024efa95cf Mon Sep 17 00:00:00 2001 From: krVatsal Date: Wed, 14 Jan 2026 22:06:07 +0530 Subject: [PATCH 4/8] feat: close path on double click Signed-off-by: krVatsal --- .../messages/tool/tool_messages/pen_tool.rs | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index bcc54ffe60..c2e27d4f96 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1906,15 +1906,33 @@ impl Fsm for PenToolFsmState { } (PenToolFsmState::DraggingHandle(_), PenToolMessage::DragStop) => { tool_data.cleanup_target_selections(shape_editor, layer, document, responses); + + // Handle double-click to close path by connecting to first point + let is_double_click = tool_data.pending_double_click_confirm; + if is_double_click { + tool_data.pending_double_click_confirm = false; + + // Set next_point to first point's position of the CURRENT shape to close the path + if let Some(first_point) = tool_data.latest_points.first() { + tool_data.next_point = first_point.pos; + tool_data.next_handle_start = first_point.pos; + + // Also set handle_end to ensure proper closing + if tool_data.handle_end.is_none() { + tool_data.handle_end = Some(first_point.pos); + } + } + } + let next_state = tool_data .finish_placing_handle(SnapData::new(document, input, viewport), transform, responses) .unwrap_or(PenToolFsmState::PlacingAnchor); - if tool_data.pending_double_click_confirm && matches!(next_state, PenToolFsmState::PlacingAnchor) { - tool_data.pending_double_click_confirm = false; - responses.add(PenToolMessage::Confirm); - } else { - tool_data.pending_double_click_confirm = false; + + // If double-click occurred and path closed, ensure we clean up properly + if is_double_click && next_state == PenToolFsmState::Ready { + tool_data.cleanup(responses); } + next_state } ( From fe87a391146ec157739d28f1d3972b0defdef950 Mon Sep 17 00:00:00 2001 From: krVatsal Date: Tue, 27 Jan 2026 14:26:37 +0530 Subject: [PATCH 5/8] not close path on double click and drag --- editor/src/messages/tool/tool_messages/pen_tool.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 7d7918cede..bad8c7d0a5 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1953,6 +1953,15 @@ impl Fsm for PenToolFsmState { move_anchor_with_handles: input.keyboard.key(move_anchor_with_handles), }; + // If the user drags the mouse beyond the threshold, we should not close the path on release + if tool_data.pending_double_click_confirm { + if let Some(last_pos) = tool_data.last_click_pos { + if last_pos.distance(input.mouse.position) > DRAG_THRESHOLD { + tool_data.pending_double_click_confirm = false; + } + } + } + let snap_data = SnapData::new(document, input, viewport); if tool_data.modifiers.colinear && !tool_data.toggle_colinear_debounce { tool_data.handle_mode = match tool_data.handle_mode { From 9a477f879c610b3cd16f7ecc63cfd087759ba281 Mon Sep 17 00:00:00 2001 From: krVatsal Date: Tue, 17 Feb 2026 15:35:19 +0530 Subject: [PATCH 6/8] change approach to use bfs to find endpoint --- .../messages/tool/tool_messages/pen_tool.rs | 58 ++++++++++++++++--- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index bad8c7d0a5..8e77a08534 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1404,6 +1404,39 @@ impl PenToolData { } } + /// Walk the connected component of the current subpath and return its single open endpoint if unambiguous. + /// Excludes the path's starting point and the currently active anchor to avoid returning the same point we are on. + fn unambiguous_subpath_endpoint(&self, vector: &Vector) -> Option { + let start_point = self.latest_points.first()?.id; + let current_point = self.latest_point()?.id; + let mut visited: HashSet = HashSet::with_hasher(NoHashBuilder); + let mut stack = vec![start_point]; + let mut endpoint: Option = None; + + while let Some(point) = stack.pop() { + if !visited.insert(point) { + continue; + } + + let is_endpoint = vector.connected_count(point) == 1 && point != start_point && point != current_point; + if is_endpoint { + // More than one open endpoint makes the target ambiguous. + if endpoint.is_some() { + return None; + } + endpoint = Some(point); + } + + for neighbor in vector.connected_points(point) { + if !visited.contains(&neighbor) { + stack.push(neighbor); + } + } + } + + endpoint + } + fn set_lock_angle(&mut self, vector: &Vector, anchor: PointId, segment: Option) { let anchor_position = vector.point_domain.position_from_id(anchor); @@ -1907,20 +1940,27 @@ impl Fsm for PenToolFsmState { (PenToolFsmState::DraggingHandle(_), PenToolMessage::DragStop) => { tool_data.cleanup_target_selections(shape_editor, layer, document, responses); - // Handle double-click to close path by connecting to first point + // Handle double-click to close path by connecting to a clear endpoint when possible let is_double_click = tool_data.pending_double_click_confirm; if is_double_click { tool_data.pending_double_click_confirm = false; - // Set next_point to first point's position of the CURRENT shape to close the path - if let Some(first_point) = tool_data.latest_points.first() { - tool_data.next_point = first_point.pos; - tool_data.next_handle_start = first_point.pos; + // Prefer a clear endpoint on the current subpath; fall back to the session start point. + let mut closing_position = None; + if let Some(layer) = layer + && let Some(vector) = document.network_interface.compute_modified_vector(layer) + { + closing_position = tool_data.unambiguous_subpath_endpoint(&vector).and_then(|endpoint| vector.point_domain.position_from_id(endpoint)); + } - // Also set handle_end to ensure proper closing - if tool_data.handle_end.is_none() { - tool_data.handle_end = Some(first_point.pos); - } + if closing_position.is_none() { + closing_position = tool_data.latest_points.first().map(|first| first.pos); + } + + if let Some(position) = closing_position { + tool_data.next_point = position; + tool_data.next_handle_start = position; + tool_data.handle_end.get_or_insert(position); } } From d735f7edb14294fd241153a053bee2cc5dbcd831 Mon Sep 17 00:00:00 2001 From: krVatsal Date: Wed, 18 Feb 2026 19:02:41 +0530 Subject: [PATCH 7/8] add feature to undo closed path and continue drawing --- .../messages/tool/tool_messages/pen_tool.rs | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 8e77a08534..4bc96e8b1f 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -402,6 +402,8 @@ struct PenToolData { colinear: bool, alt_pressed: bool, space_pressed: bool, + /// If a double-click closed the path, defer cleanup so Undo can restore the drawing state. + cleanup_after_double_click: bool, /// Tracks whether to switch from `HandleMode::ColinearEquidistant` to `HandleMode::Free` /// after releasing Ctrl, specifically when Ctrl was held before the handle was dragged from the anchor. switch_to_free_on_ctrl_release: bool, @@ -454,6 +456,7 @@ impl PenToolData { self.pending_double_click_confirm = false; self.last_click_time = None; self.last_click_pos = None; + self.cleanup_after_double_click = false; } fn update_click_timing(&mut self, time: u64, position: DVec2) -> bool { @@ -1862,7 +1865,12 @@ impl Fsm for PenToolFsmState { let append = input.keyboard.key(append_to_selected); tool_data.store_clicked_endpoint(document, &transform, input, viewport); + // If a previous double-click closed a path and we never resumed drawing, clear stale tool state now. + if tool_data.cleanup_after_double_click { + tool_data.cleanup(responses); + } tool_data.create_initial_point(document, input, viewport, responses, tool_options, append, shape_editor); + tool_data.cleanup_after_double_click = false; // Enter the dragging handle state while the mouse is held down, allowing the user to move the mouse and position the handle PenToolFsmState::DraggingHandle(tool_data.handle_mode) @@ -1968,9 +1976,9 @@ impl Fsm for PenToolFsmState { .finish_placing_handle(SnapData::new(document, input, viewport), transform, responses) .unwrap_or(PenToolFsmState::PlacingAnchor); - // If double-click occurred and path closed, ensure we clean up properly + // If double-click closed the path, defer cleanup so Undo can restore the drawing state. Cleanup will run when starting a fresh path. if is_double_click && next_state == PenToolFsmState::Ready { - tool_data.cleanup(responses); + tool_data.cleanup_after_double_click = true; } next_state @@ -2284,6 +2292,7 @@ impl Fsm for PenToolFsmState { (PenToolFsmState::DraggingHandle(..) | PenToolFsmState::PlacingAnchor, PenToolMessage::Undo) => { if tool_data.point_index > 0 { tool_data.point_index -= 1; + tool_data.cleanup_after_double_click = false; tool_data .place_anchor(SnapData::new(document, input, viewport), transform, input.mouse.position, responses) .unwrap_or(PenToolFsmState::PlacingAnchor) @@ -2292,6 +2301,17 @@ impl Fsm for PenToolFsmState { self } } + (PenToolFsmState::Ready, PenToolMessage::Undo) => { + if tool_data.cleanup_after_double_click && !tool_data.latest_points.is_empty() { + tool_data.cleanup_after_double_click = false; + tool_data.pending_double_click_confirm = false; + tool_data.point_index = tool_data.latest_points.len().saturating_sub(1); + tool_data.handle_end = None; + PenToolFsmState::PlacingAnchor + } else { + self + } + } (_, PenToolMessage::Redo) => { tool_data.point_index = (tool_data.point_index + 1).min(tool_data.latest_points.len().saturating_sub(1)); tool_data.place_anchor(SnapData::new(document, input, viewport), transform, input.mouse.position, responses); From c992a341492b5bb8bda13315e02725d897deb96a Mon Sep 17 00:00:00 2001 From: krVatsal Date: Thu, 19 Feb 2026 16:59:26 +0530 Subject: [PATCH 8/8] fixed ghost line to be at appropriate node after undo --- editor/src/messages/tool/tool_messages/pen_tool.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 4bc96e8b1f..1b77ba77da 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -2293,6 +2293,11 @@ impl Fsm for PenToolFsmState { if tool_data.point_index > 0 { tool_data.point_index -= 1; tool_data.cleanup_after_double_click = false; + if let Some(prev_point) = tool_data.latest_points.get(tool_data.point_index) { + tool_data.next_point = prev_point.pos; + tool_data.next_handle_start = prev_point.handle_start; + tool_data.handle_end = None; + } tool_data .place_anchor(SnapData::new(document, input, viewport), transform, input.mouse.position, responses) .unwrap_or(PenToolFsmState::PlacingAnchor)