Skip to content
Open
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3fbcaf9
added feature to stop drawing when double clicked using pen tool
krVatsal Oct 26, 2025
04cf0eb
Merge branch 'master' into add-feature-to-stop-drawing-on-double-clic…
krVatsal Nov 16, 2025
5d25cac
Merge branch 'master' into add-feature-to-stop-drawing-on-double-clic…
krVatsal Nov 20, 2025
b92cf9f
fixing build issue
krVatsal Nov 30, 2025
4f09282
merged
krVatsal Nov 30, 2025
7375079
Merge branch 'master' into add-feature-to-stop-drawing-on-double-clic…
krVatsal Jan 10, 2026
f53d953
Merge branch 'master' into add-feature-to-stop-drawing-on-double-clic…
krVatsal Jan 12, 2026
d377141
fix: fixed arguments in pen tool
krVatsal Jan 12, 2026
e4a80c5
Merge branch 'master' into add-feature-to-stop-drawing-on-double-clic…
timon-schelling Jan 13, 2026
a704272
feat: close path on double click
krVatsal Jan 14, 2026
be1cc0c
Merge branch 'add-feature-to-stop-drawing-on-double-click-using-pen-t…
krVatsal Jan 14, 2026
a7f67b7
Merge branch 'master' into add-feature-to-stop-drawing-on-double-clic…
krVatsal Jan 14, 2026
ea2c5f6
Merge branch 'master' into add-feature-to-stop-drawing-on-double-clic…
krVatsal Jan 23, 2026
aa9da44
Merge branch 'master' into add-feature-to-stop-drawing-on-double-clic…
krVatsal Jan 25, 2026
fe87a39
not close path on double click and drag
krVatsal Jan 27, 2026
59bd6e7
Merge branch 'master' into add-feature-to-stop-drawing-on-double-clic…
krVatsal Jan 27, 2026
eec01d7
Merge branch 'master' into add-feature-to-stop-drawing-on-double-clic…
krVatsal Feb 14, 2026
c88b7ed
Merge branch 'master' into add-feature-to-stop-drawing-on-double-clic…
krVatsal Feb 17, 2026
9a477f8
change approach to use bfs to find endpoint
krVatsal Feb 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 102 additions & 3 deletions editor/src/messages/tool/tool_messages/pen_tool.rs
Original file line number Diff line number Diff line change
@@ -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_network_node_type;
use crate::messages::portfolio::document::overlays::utility_functions::path_overlays;
Expand Down Expand Up @@ -412,6 +412,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<u64>,
last_click_pos: Option<DVec2>,
pending_double_click_confirm: bool,

handle_mode: HandleMode,
prior_segment_layer: Option<LayerNodeIdentifier>,
Expand Down Expand Up @@ -448,6 +451,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`
Expand Down Expand Up @@ -1389,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<PointId> {
let start_point = self.latest_points.first()?.id;
let current_point = self.latest_point()?.id;
let mut visited: HashSet<PointId, NoHashBuilder> = HashSet::with_hasher(NoHashBuilder);
let mut stack = vec![start_point];
let mut endpoint: Option<PointId> = 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<SegmentId>) {
let anchor_position = vector.point_domain.position_from_id(anchor);

Expand Down Expand Up @@ -1805,6 +1853,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;

Expand All @@ -1827,6 +1877,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, viewport), &point, SnapTypeConfiguration::default());
let viewport_vec = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document);
Expand Down Expand Up @@ -1881,9 +1939,41 @@ impl Fsm for PenToolFsmState {
}
(PenToolFsmState::DraggingHandle(_), PenToolMessage::DragStop) => {
tool_data.cleanup_target_selections(shape_editor, layer, document, responses);
tool_data

// 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;

// 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));
}

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);
}
}

let next_state = tool_data
.finish_placing_handle(SnapData::new(document, input, viewport), transform, responses)
.unwrap_or(PenToolFsmState::PlacingAnchor)
.unwrap_or(PenToolFsmState::PlacingAnchor);

// 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
}
(
PenToolFsmState::DraggingHandle(_),
Expand All @@ -1903,6 +1993,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 {
Expand Down
Loading