Make the Pen tool use Ctrl to lock the angle of handles such that they maintain colinearity (#2284)

* pen tool feature and other minor fixes

* ctrl done,need to improve handle_modifications

* completes collinear and ctrl for all cases,need to refactor

* more bug fix need to refactor

* fixed minor issues and refactor done

* Code review

* minor bug fixes

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0SlowPoke0 2025-02-17 13:11:55 +05:30 committed by GitHub
parent 687744d999
commit e444785301
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 291 additions and 81 deletions

View File

@ -591,11 +591,14 @@ impl<'a> Selected<'a> {
responses.add(GraphOperationMessage::Vector { layer, modification_type });
if let Some((id, initial)) = handle.mirror {
let direction = viewspace.transform_vector2(new_pos_viewport - relative).try_normalize();
let length = viewspace.transform_vector2(initial - relative).length();
let new_relative = direction.map_or(initial - relative, |direction| viewspace.inverse().transform_vector2(-direction * length));
let modification_type = id.set_relative_position(new_relative);
responses.add(GraphOperationMessage::Vector { layer, modification_type });
// When the handle is scaled to zero, don't update the mirror handle
if (new_pos_viewport - relative).length_squared() > f64::EPSILON {
let direction = viewspace.transform_vector2(new_pos_viewport - relative).try_normalize();
let length = viewspace.transform_vector2(initial - relative).length();
let new_relative = direction.map_or(initial - relative, |direction| viewspace.inverse().transform_vector2(-direction * length));
let modification_type = id.set_relative_position(new_relative);
responses.add(GraphOperationMessage::Vector { layer, modification_type });
}
}
}
}

View File

@ -733,14 +733,16 @@ impl ShapeState {
}
/// Move the selected points by dragging the mouse.
#[allow(clippy::too_many_arguments)]
pub fn move_selected_points(
&self,
handle_lengths: Option<OpposingHandleLengths>,
document: &DocumentMessageHandler,
delta: DVec2,
equidistant: bool,
responses: &mut VecDeque<Message>,
in_viewport_space: bool,
opposite_handle_position: Option<DVec2>,
responses: &mut VecDeque<Message>,
) {
for (&layer, state) in &self.selected_shape_state {
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue };
@ -791,6 +793,11 @@ impl ShapeState {
let new_relative = if equidistant {
-(handle_position - anchor_position)
}
// If the handle is very close to the anchor, return the original position
else if (handle_position - anchor_position).length_squared() < f64::EPSILON * 1e5 {
let Some(opposite_handle_position) = opposite_handle_position else { continue };
opposite_handle_position - anchor_position
} else {
// TODO: Is this equivalent to `transform_to_document_space`? If changed, the before and after should be tested.
let transform = document.metadata().document_to_viewport.inverse() * transform_to_viewport_space;

View File

@ -371,6 +371,7 @@ struct PathToolData {
dragging_state: DraggingState,
current_selected_handle_id: Option<ManipulatorPointId>,
angle: f64,
opposite_handle_position: Option<DVec2>,
}
impl PathToolData {
@ -551,6 +552,16 @@ impl PathToolData {
for point in state.selected() {
let Some(anchor) = point.get_anchor(&vector_data) else { continue };
layer_manipulators.insert(anchor);
let Some([handle1, handle2]) = point.get_handle_pair(&vector_data) else { continue };
let Some(handle) = point.as_handle() else { continue };
// Check which handle is selected and which is opposite
let opposite = if handle == handle1 { handle2 } else { handle1 };
self.opposite_handle_position = if self.opposite_handle_position.is_none() {
opposite.to_manipulator_point().get_position(&vector_data)
} else {
self.opposite_handle_position
};
}
for (&id, &position) in vector_data.point_domain.ids().iter().zip(vector_data.point_domain.positions()) {
if layer_manipulators.contains(&id) {
@ -742,7 +753,8 @@ impl PathToolData {
};
let handle_lengths = if equidistant { None } else { self.opposing_handle_lengths.take() };
shape_editor.move_selected_points(handle_lengths, document, snapped_delta, equidistant, responses, true);
let opposite = if lock_angle { None } else { self.opposite_handle_position };
shape_editor.move_selected_points(handle_lengths, document, snapped_delta, equidistant, true, opposite, responses);
self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(snapped_delta);
}
}
@ -1199,6 +1211,7 @@ impl Fsm for PathToolFsmState {
responses.add(DocumentMessage::EndTransaction);
responses.add(PathToolMessage::SelectedPointUpdated);
tool_data.snap_manager.cleanup(responses);
tool_data.opposite_handle_position = None;
PathToolFsmState::Ready
}
@ -1249,7 +1262,15 @@ impl Fsm for PathToolFsmState {
}
(_, PathToolMessage::PointerMove { .. }) => self,
(_, PathToolMessage::NudgeSelectedPoints { delta_x, delta_y }) => {
shape_editor.move_selected_points(tool_data.opposing_handle_lengths.take(), document, (delta_x, delta_y).into(), true, responses, false);
shape_editor.move_selected_points(
tool_data.opposing_handle_lengths.take(),
document,
(delta_x, delta_y).into(),
true,
false,
tool_data.opposite_handle_position,
responses,
);
PathToolFsmState::Ready
}

View File

@ -8,7 +8,7 @@ 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, merge_layers};
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
use crate::messages::tool::common_functionality::utility_functions::should_extend;
use crate::messages::tool::common_functionality::utility_functions::{closest_point, should_extend};
use bezier_rs::{Bezier, BezierHandles};
use graph_craft::document::NodeId;
@ -243,6 +243,14 @@ struct LastPoint {
in_segment: Option<SegmentId>,
handle_start: DVec2,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
enum DrawMode {
#[default]
/// Modifies the clicked endpoint segment, once you go to the ready mode you need to modify the handles of the next clicked endpoint segment
BreakPath,
/// Modifies the handle_end
ContinuePath,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
enum HandleMode {
@ -267,8 +275,6 @@ struct PenToolData {
g1_continuous: bool,
toggle_colinear_debounce: bool,
segment_end_before_bent: Option<SegmentId>,
angle: f64,
auto_panning: AutoPanning,
modifiers: ModifierState,
@ -280,6 +286,10 @@ struct PenToolData {
alt_press: bool,
handle_mode: HandleMode,
/// The point that is being dragged
end_point: Option<PointId>,
end_point_segment: Option<SegmentId>,
draw_mode: DrawMode,
}
impl PenToolData {
fn latest_point(&self) -> Option<&LastPoint> {
@ -315,25 +325,35 @@ 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, layer: LayerNodeIdentifier) {
fn bend_from_previous_point(&mut self, snap_data: SnapData, transform: DAffine2, layer: LayerNodeIdentifier, preferences: &PreferencesMessageHandler) {
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 Some((last_pos, id)) = self.latest_point().map(|point| (point.pos, point.id)) 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 {
self.handle_end = None;
self.handle_mode = HandleMode::Free;
// Update `end_point_segment` that was clicked on
self.store_clicked_endpoint(document, snap_data.input, preferences);
if self.modifiers.lock_angle {
self.set_lock_angle(&vector_data, id, self.end_point_segment);
let last_segment = self.end_point_segment;
let Some(point) = self.latest_point_mut() else { return };
point.in_segment = last_segment;
return;
}
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;
}
}
@ -379,9 +399,11 @@ impl PenToolData {
end
});
let points = [start, end];
// Store the segment
let id = SegmentId::generate();
self.segment_end_before_bent = Some(id);
self.end_point_segment = Some(id);
let points = [start, end];
let modification_type = VectorModificationType::InsertSegment { id, points, handles };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
@ -413,12 +435,17 @@ impl PenToolData {
self.next_handle_start = self.compute_snapped_angle(snap_data, transform, colinear, mouse, Some(self.next_point), false);
let Some(layer) = layer else { return Some(PenToolFsmState::DraggingHandle(self.handle_mode)) };
let vector_data = document.network_interface.compute_modified_vector(layer)?;
// Check if the handle is the start of the segment
let mut is_start = false;
if let Some((anchor, segment)) = self.end_point.zip(self.end_point_segment) {
is_start = vector_data.segment_start_from_id(segment) == Some(anchor);
}
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);
self.colinear(responses, layer, self.next_handle_start, self.next_point, &vector_data, is_start);
self.adjust_handle_length(responses, layer, &vector_data, is_start);
}
HandleMode::Free => {
self.g1_continuous = false;
@ -431,83 +458,122 @@ impl PenToolData {
}
/// Makes the opposite handle equidistant or locks its length.
fn adjust_handle_length(&mut self, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier, vector_data: &VectorData) {
fn adjust_handle_length(&mut self, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier, vector_data: &VectorData, is_start: bool) {
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::ColinearEquidistant => self.adjust_equidistant_handle(anchor_pos, responses, layer, vector_data, is_start),
HandleMode::ColinearLocked => self.adjust_locked_length_handle(anchor_pos, responses, layer, is_start),
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 {
fn colinear(&mut self, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier, handle_start: DVec2, anchor_pos: DVec2, vector_data: &VectorData, is_start: bool) {
let Some(direction) = (anchor_pos - 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);
let Some(handle_offset) = self.get_handle_offset(anchor_pos, vector_data, is_start) else { return };
let new_handle_position = anchor_pos + handle_offset * direction;
self.update_handle_position(new_handle_position, anchor_pos, responses, layer, is_start);
}
fn adjust_equidistant_handle(&mut self, anchor_pos: DVec2, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier, vector_data: &VectorData) {
fn get_handle_offset(&self, anchor_pos: DVec2, vector_data: &VectorData, is_start: bool) -> Option<f64> {
if is_start {
let segment = self.end_point_segment?;
let handle = ManipulatorPointId::PrimaryHandle(segment).get_position(vector_data)?;
return Some((handle - anchor_pos).length());
}
if self.draw_mode == DrawMode::ContinuePath {
return self.handle_end.map(|handle| (handle - anchor_pos).length()).or_else(|| {
self.end_point_segment
.and_then(|segment| Some((ManipulatorPointId::EndHandle(segment).get_position(vector_data)? - anchor_pos).length()))
});
}
let handle = ManipulatorPointId::EndHandle(self.end_point_segment?).get_position(vector_data);
if let Some(handle) = handle {
return Some((handle - anchor_pos).length());
}
None
}
fn adjust_equidistant_handle(&mut self, anchor_pos: DVec2, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier, vector_data: &VectorData, is_start: bool) {
if self.modifiers.break_handle {
self.store_handle(vector_data);
self.store_handle(vector_data, is_start);
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);
self.update_handle_position(new_position, anchor_pos, responses, layer, is_start);
} else {
self.restore_previous_handle(anchor_pos, responses, layer);
self.restore_previous_handle(anchor_pos, responses, layer, is_start);
}
}
fn adjust_locked_length_handle(&mut self, anchor_pos: DVec2, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier) {
fn adjust_locked_length_handle(&mut self, anchor_pos: DVec2, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier, is_start: bool) {
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);
self.update_handle_position(new_position, anchor_pos, responses, layer, is_start);
}
}
/// Temporarily stores the opposite handle position to revert back when Alt is released in equidistant mode.
fn store_handle(&mut self, vector_data: &VectorData) {
fn store_handle(&mut self, vector_data: &VectorData, is_start: bool) {
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)
});
self.previous_handle_end_pos = if is_start {
let Some(segment) = self.end_point_segment else { return };
ManipulatorPointId::PrimaryHandle(segment).get_position(vector_data)
} else if self.draw_mode == DrawMode::ContinuePath {
self.handle_end.or_else(|| {
let segment = self.end_point_segment?;
ManipulatorPointId::EndHandle(segment).get_position(vector_data)
})
} else {
let Some(segment) = self.end_point_segment else { return };
let end_handle = ManipulatorPointId::EndHandle(segment);
end_handle.get_position(vector_data)
};
}
}
fn restore_previous_handle(&mut self, anchor_pos: DVec2, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier) {
fn restore_previous_handle(&mut self, anchor_pos: DVec2, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier, is_start: bool) {
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.update_handle_position(previous_handle, anchor_pos, responses, layer, is_start);
}
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 };
fn update_handle_position(&mut self, new_position: DVec2, anchor_pos: DVec2, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier, is_start: bool) {
let relative_position = new_position - anchor_pos;
if self.draw_mode == DrawMode::ContinuePath {
if let Some(handle) = self.handle_end.as_mut() {
*handle = new_position;
return;
}
let Some(segment) = self.end_point_segment else { return };
let modification_type = VectorModificationType::SetEndHandle { segment, relative_position };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
return;
}
let Some(segment) = self.end_point_segment else { return };
if is_start {
let modification_type = VectorModificationType::SetPrimaryHandle { segment, relative_position };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
return;
}
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> {
@ -626,20 +692,36 @@ impl PenToolData {
let tolerance = crate::consts::SNAP_POINT_TOLERANCE;
if let Some((layer, point, position)) = should_extend(document, viewport, tolerance, selected_nodes.selected_layers(document.metadata()), preferences) {
// Perform extension of an existing path
let in_segment = if self.modifiers.lock_angle { self.end_point_segment } else { None };
self.add_point(LastPoint {
id: point,
pos: position,
in_segment: None,
in_segment,
handle_start: position,
});
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] });
self.next_point = position;
self.next_handle_start = position;
let vector_data = document.network_interface.compute_modified_vector(layer).unwrap();
let segment = vector_data.all_connected(point).collect::<Vec<_>>().first().map(|s| s.segment);
if self.modifiers.lock_angle {
self.set_lock_angle(&vector_data, point, segment);
}
return;
}
if append {
if let Some((layer, point, _)) = closest_point(document, viewport, tolerance, document.metadata().all_layers(), |_| false, preferences) {
let vector_data = document.network_interface.compute_modified_vector(layer).unwrap();
let segment = vector_data.all_connected(point).collect::<Vec<_>>().first().map(|s| s.segment);
if self.modifiers.lock_angle {
self.set_lock_angle(&vector_data, point, segment);
}
}
self.end_point_segment = None;
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 {
@ -649,6 +731,16 @@ impl PenToolData {
}
}
if let Some((layer, point, _position)) = closest_point(document, viewport, tolerance, document.metadata().all_layers(), |_| false, preferences) {
let vector_data = document.network_interface.compute_modified_vector(layer).unwrap();
let segment = vector_data.all_connected(point).collect::<Vec<_>>().first().map(|s| s.segment);
if self.modifiers.lock_angle {
self.set_lock_angle(&vector_data, point, segment);
self.handle_mode = HandleMode::Free;
}
}
// New path layer
let node_type = resolve_document_node_type("Path").expect("Path node does not exist");
let nodes = vec![(NodeId(0), node_type.default_node_template())];
@ -657,6 +749,8 @@ impl PenToolData {
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_options.line_weight, layer, responses);
self.end_point_segment = None;
self.draw_mode = DrawMode::ContinuePath;
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] });
// This causes the following message to be run only after the next graph evaluation runs and the transforms are updated
@ -665,6 +759,61 @@ impl PenToolData {
responses.add(PenToolMessage::AddPointLayerPosition { layer, viewport });
}
// Stores the segment and point ID of the clicked endpoint
fn store_clicked_endpoint(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, preferences: &PreferencesMessageHandler) {
let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position));
let snapped = self.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default());
let viewport = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document);
let tolerance = crate::consts::SNAP_POINT_TOLERANCE;
if let Some((layer, point, _position)) = closest_point(document, viewport, tolerance, document.metadata().all_layers(), |_| false, preferences) {
self.end_point = Some(point);
let vector_data = document.network_interface.compute_modified_vector(layer).unwrap();
let segment = vector_data.all_connected(point).collect::<Vec<_>>().first().map(|s| s.segment);
self.end_point_segment = segment;
}
}
fn set_lock_angle(&mut self, vector_data: &VectorData, anchor: PointId, segment: Option<SegmentId>) {
let anchor_position = vector_data.point_domain.position_from_id(anchor);
let Some((anchor_position, segment)) = anchor_position.zip(segment) else {
self.handle_mode = HandleMode::Free;
return;
};
// Closure to check if a point is the start or end of a segment
let is_start = |point: PointId, segment: SegmentId| vector_data.segment_start_from_id(segment) == Some(point);
let end_handle = ManipulatorPointId::EndHandle(segment).get_position(vector_data);
let start_handle = ManipulatorPointId::PrimaryHandle(segment).get_position(vector_data);
let start_point = if is_start(anchor, segment) {
vector_data.segment_end_from_id(segment).and_then(|id| vector_data.point_domain.position_from_id(id))
} else {
vector_data.segment_start_from_id(segment).and_then(|id| vector_data.point_domain.position_from_id(id))
};
let required_handle = if is_start(anchor, segment) {
start_handle
.filter(|&handle| handle != anchor_position)
.or(end_handle.filter(|&handle| Some(handle) != start_point))
.or(start_point)
} else {
end_handle
.filter(|&handle| handle != anchor_position)
.or(start_handle.filter(|&handle| Some(handle) != start_point))
.or(start_point)
};
if let Some(required_handle) = required_handle {
self.angle = -(required_handle - anchor_position).angle_to(DVec2::X);
self.handle_mode = HandleMode::ColinearEquidistant;
}
}
fn add_point_layer_position(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier, viewport: DVec2) {
// Add the first point
let id = PointId::generate();
@ -737,7 +886,7 @@ impl Fsm for PenToolFsmState {
tool_data.previous_handle_start_pos = latest.handle_start;
// Store the handle_end position
let segment = tool_data.segment_end_before_bent;
let segment = tool_data.end_point_segment;
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);
@ -759,7 +908,7 @@ impl Fsm for PenToolFsmState {
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) {
if let Some((latest, segment)) = tool_data.latest_point().zip(tool_data.end_point_segment) {
let handle = ManipulatorPointId::EndHandle(segment).get_position(&vector_data);
let Some(handle) = handle else { return PenToolFsmState::GRSHandle };
@ -814,7 +963,7 @@ impl Fsm for PenToolFsmState {
});
// 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) {
if let Some(((latest, segment), handle_end)) = tool_data.latest_point().zip(tool_data.end_point_segment).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 });
@ -866,6 +1015,19 @@ impl Fsm for PenToolFsmState {
// Draw the line between the currently-being-placed anchor and its currently-being-dragged-out outgoing handle (opposite the one currently being dragged out)
overlay_context.line(next_anchor, next_handle_start, None);
match tool_options.pen_overlay_mode {
PenOverlayMode::AllHandles => {
path_overlays(document, DrawHandles::All, shape_editor, &mut overlay_context);
}
PenOverlayMode::FrontierHandles => {
if let Some(latest_segment) = tool_data.end_point_segment {
path_overlays(document, DrawHandles::SelectedAnchors(vec![latest_segment]), shape_editor, &mut overlay_context);
} else {
path_overlays(document, DrawHandles::None, shape_editor, &mut overlay_context);
};
}
}
if let (Some(anchor_start), Some(handle_start), Some(handle_end)) = (anchor_start, handle_start, handle_end) {
// Draw the line between the most recently placed anchor and its outgoing handle (which is currently influencing the currently-being-placed segment)
overlay_context.line(anchor_start, handle_start, None);
@ -878,18 +1040,9 @@ impl Fsm for PenToolFsmState {
overlay_context.dashed_line(anchor_start, next_anchor, None, Some(4.), Some(4.), Some(0.5));
}
match tool_options.pen_overlay_mode {
PenOverlayMode::AllHandles => {
path_overlays(document, DrawHandles::All, shape_editor, &mut overlay_context);
}
PenOverlayMode::FrontierHandles => {
// Find the last segment ID to have its handles drawn
if let Some(latest_segment) = tool_data.latest_point().and_then(|point| point.in_segment) {
path_overlays(document, DrawHandles::SelectedAnchors(vec![latest_segment]), shape_editor, &mut overlay_context);
} else {
path_overlays(document, DrawHandles::None, shape_editor, &mut overlay_context);
};
}
// Draw the line between the currently-being-placed anchor and last-placed point (Lock angle bent overlays)
if self == PenToolFsmState::PlacingAnchor && anchor_start != handle_start && tool_data.modifiers.snap_angle {
overlay_context.dashed_line(anchor_start, next_anchor, None, Some(4.), Some(4.), Some(0.5));
}
if self == PenToolFsmState::DraggingHandle(tool_data.handle_mode) && valid(next_anchor, handle_end) {
@ -938,6 +1091,9 @@ impl Fsm for PenToolFsmState {
(PenToolFsmState::Ready, PenToolMessage::DragStart { append_to_selected }) => {
responses.add(DocumentMessage::StartTransaction);
tool_data.handle_mode = HandleMode::Free;
// Get the closest point and the segment it is on
tool_data.store_clicked_endpoint(document, input, preferences);
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
@ -959,11 +1115,13 @@ impl Fsm for PenToolFsmState {
// 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.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;
if let Some(layer) = layer {
tool_data.buffering_merged_vector = false;
tool_data.handle_mode = HandleMode::ColinearLocked;
tool_data.bend_from_previous_point(SnapData::new(document, input), transform, layer, preferences);
tool_data.place_anchor(SnapData::new(document, input), transform, input.mouse.position, preferences, responses);
tool_data.buffering_merged_vector = false;
}
PenToolFsmState::DraggingHandle(tool_data.handle_mode)
} else {
if tool_data.handle_end.is_some() {
@ -982,6 +1140,7 @@ impl Fsm for PenToolFsmState {
merge_layers(document, current_layer, other_layer, responses);
}
}
// Even if no buffer was started, the message still has to be run again in order to call bend_from_previous_point
tool_data.buffering_merged_vector = true;
responses.add(PenToolMessage::DragStart { append_to_selected });
@ -997,9 +1156,13 @@ impl Fsm for PenToolFsmState {
}
self
}
(PenToolFsmState::DraggingHandle(_), PenToolMessage::DragStop) => tool_data
.finish_placing_handle(SnapData::new(document, input), transform, preferences, responses)
.unwrap_or(PenToolFsmState::PlacingAnchor),
(PenToolFsmState::DraggingHandle(_), PenToolMessage::DragStop) => {
tool_data.end_point = None;
tool_data.draw_mode = DrawMode::ContinuePath;
tool_data
.finish_placing_handle(SnapData::new(document, input), transform, preferences, responses)
.unwrap_or(PenToolFsmState::PlacingAnchor)
}
(
PenToolFsmState::DraggingHandle(_),
PenToolMessage::PointerMove {
@ -1093,7 +1256,21 @@ impl Fsm for PenToolFsmState {
state
}
(PenToolFsmState::Ready, PenToolMessage::PointerMove { .. }) => {
(
PenToolFsmState::Ready,
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),
};
tool_data.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position);
responses.add(OverlaysMessage::Draw);
self
@ -1143,6 +1320,7 @@ impl Fsm for PenToolFsmState {
(PenToolFsmState::DraggingHandle(..) | PenToolFsmState::PlacingAnchor, PenToolMessage::Confirm) => {
responses.add(DocumentMessage::EndTransaction);
tool_data.handle_end = None;
tool_data.draw_mode = DrawMode::BreakPath;
tool_data.latest_points.clear();
tool_data.point_index = 0;
tool_data.snap_manager.cleanup(responses);
@ -1154,6 +1332,7 @@ impl Fsm for PenToolFsmState {
tool_data.handle_end = None;
tool_data.latest_points.clear();
tool_data.point_index = 0;
tool_data.draw_mode = DrawMode::BreakPath;
tool_data.snap_manager.cleanup(responses);
responses.add(OverlaysMessage::Draw);