|
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::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; |
|
use crate::messages::portfolio::document::overlays::utility_types::{DrawHandles, OverlayContext}; |
|
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; |
|
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::shape_editor::ShapeState; |
|
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration}; |
|
use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, closest_point, should_extend}; |
|
use bezier_rs::{Bezier, BezierHandles}; |
|
use graph_craft::document::NodeId; |
|
use graphene_std::Color; |
|
use graphene_std::vector::{HandleId, ManipulatorPointId, NoHashBuilder, SegmentId, StrokeId, VectorData}; |
|
use graphene_std::vector::{PointId, VectorModificationType}; |
|
|
|
#[derive(Default)] |
|
pub struct PenTool { |
|
fsm_state: PenToolFsmState, |
|
tool_data: PenToolData, |
|
options: PenOptions, |
|
} |
|
|
|
pub struct PenOptions { |
|
line_weight: f64, |
|
fill: ToolColorOptions, |
|
stroke: ToolColorOptions, |
|
pen_overlay_mode: PenOverlayMode, |
|
} |
|
|
|
impl Default for PenOptions { |
|
fn default() -> Self { |
|
Self { |
|
line_weight: DEFAULT_STROKE_WIDTH, |
|
fill: ToolColorOptions::new_secondary(), |
|
stroke: ToolColorOptions::new_primary(), |
|
pen_overlay_mode: PenOverlayMode::FrontierHandles, |
|
} |
|
} |
|
} |
|
|
|
#[impl_message(Message, ToolMessage, Pen)] |
|
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] |
|
pub enum PenToolMessage { |
|
|
|
Abort, |
|
SelectionChanged, |
|
WorkingColorChanged, |
|
Overlays(OverlayContext), |
|
|
|
|
|
|
|
|
|
AddPointLayerPosition { |
|
layer: LayerNodeIdentifier, |
|
viewport: DVec2, |
|
}, |
|
Confirm, |
|
DragStart { |
|
append_to_selected: Key, |
|
}, |
|
DragStop, |
|
PointerMove { |
|
snap_angle: Key, |
|
break_handle: Key, |
|
lock_angle: Key, |
|
colinear: Key, |
|
move_anchor_with_handles: Key, |
|
}, |
|
PointerOutsideViewport { |
|
snap_angle: Key, |
|
break_handle: Key, |
|
lock_angle: Key, |
|
colinear: Key, |
|
move_anchor_with_handles: Key, |
|
}, |
|
Redo, |
|
Undo, |
|
UpdateOptions(PenOptionsUpdate), |
|
RecalculateLatestPointsPosition, |
|
RemovePreviousHandle, |
|
GRS { |
|
grab: Key, |
|
rotate: Key, |
|
scale: Key, |
|
}, |
|
FinalPosition { |
|
final_position: DVec2, |
|
}, |
|
SwapHandles, |
|
} |
|
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] |
|
enum PenToolFsmState { |
|
#[default] |
|
Ready, |
|
DraggingHandle(HandleMode), |
|
PlacingAnchor, |
|
GRSHandle, |
|
} |
|
|
|
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] |
|
pub enum PenOverlayMode { |
|
AllHandles = 0, |
|
FrontierHandles = 1, |
|
} |
|
|
|
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] |
|
pub enum PenOptionsUpdate { |
|
FillColor(Option<Color>), |
|
FillColorType(ToolColorType), |
|
LineWeight(f64), |
|
StrokeColor(Option<Color>), |
|
StrokeColorType(ToolColorType), |
|
WorkingColors(Option<Color>, Option<Color>), |
|
OverlayModeType(PenOverlayMode), |
|
} |
|
|
|
impl ToolMetadata for PenTool { |
|
fn icon_name(&self) -> String { |
|
"VectorPenTool".into() |
|
} |
|
fn tooltip(&self) -> String { |
|
"Pen Tool".into() |
|
} |
|
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType { |
|
ToolType::Pen |
|
} |
|
} |
|
|
|
fn create_weight_widget(line_weight: f64) -> WidgetHolder { |
|
NumberInput::new(Some(line_weight)) |
|
.unit(" px") |
|
.label("Weight") |
|
.min(0.) |
|
.max((1_u64 << f64::MANTISSA_DIGITS) as f64) |
|
.on_update(|number_input: &NumberInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::LineWeight(number_input.value.unwrap())).into()) |
|
.widget_holder() |
|
} |
|
|
|
impl LayoutHolder for PenTool { |
|
fn layout(&self) -> Layout { |
|
let mut widgets = self.options.fill.create_widgets( |
|
"Fill", |
|
true, |
|
|_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(None)).into(), |
|
|color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColorType(color_type.clone())).into()), |
|
|color: &ColorInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(), |
|
); |
|
|
|
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); |
|
|
|
widgets.append(&mut self.options.stroke.create_widgets( |
|
"Stroke", |
|
true, |
|
|_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(None)).into(), |
|
|color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColorType(color_type.clone())).into()), |
|
|color: &ColorInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(), |
|
)); |
|
|
|
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); |
|
|
|
widgets.push(create_weight_widget(self.options.line_weight)); |
|
|
|
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); |
|
|
|
widgets.push( |
|
RadioInput::new(vec![ |
|
RadioEntryData::new("all") |
|
.icon("HandleVisibilityAll") |
|
.tooltip("Show all handles regardless of selection") |
|
.on_update(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::OverlayModeType(PenOverlayMode::AllHandles)).into()), |
|
RadioEntryData::new("frontier") |
|
.icon("HandleVisibilityFrontier") |
|
.tooltip("Show only handles at the frontiers of the segments connected to selected points") |
|
.on_update(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::OverlayModeType(PenOverlayMode::FrontierHandles)).into()), |
|
]) |
|
.selected_index(Some(self.options.pen_overlay_mode as u32)) |
|
.widget_holder(), |
|
); |
|
|
|
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) |
|
} |
|
} |
|
|
|
impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PenTool { |
|
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, tool_data: &mut ToolActionHandlerData<'a>) { |
|
let ToolMessage::Pen(PenToolMessage::UpdateOptions(action)) = message else { |
|
self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &self.options, responses, true); |
|
return; |
|
}; |
|
|
|
match action { |
|
PenOptionsUpdate::OverlayModeType(overlay_mode_type) => { |
|
self.options.pen_overlay_mode = overlay_mode_type; |
|
responses.add(OverlaysMessage::Draw); |
|
} |
|
PenOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight, |
|
PenOptionsUpdate::FillColor(color) => { |
|
self.options.fill.custom_color = color; |
|
self.options.fill.color_type = ToolColorType::Custom; |
|
} |
|
PenOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type, |
|
PenOptionsUpdate::StrokeColor(color) => { |
|
self.options.stroke.custom_color = color; |
|
self.options.stroke.color_type = ToolColorType::Custom; |
|
} |
|
PenOptionsUpdate::StrokeColorType(color_type) => self.options.stroke.color_type = color_type, |
|
PenOptionsUpdate::WorkingColors(primary, secondary) => { |
|
self.options.stroke.primary_working_color = primary; |
|
self.options.stroke.secondary_working_color = secondary; |
|
self.options.fill.primary_working_color = primary; |
|
self.options.fill.secondary_working_color = secondary; |
|
} |
|
} |
|
|
|
self.send_layout(responses, LayoutTarget::ToolOptions); |
|
} |
|
|
|
fn actions(&self) -> ActionList { |
|
match self.fsm_state { |
|
PenToolFsmState::Ready | PenToolFsmState::GRSHandle => actions!(PenToolMessageDiscriminant; |
|
Undo, |
|
DragStart, |
|
DragStop, |
|
Confirm, |
|
Abort, |
|
PointerMove, |
|
FinalPosition |
|
), |
|
PenToolFsmState::DraggingHandle(_) | PenToolFsmState::PlacingAnchor => actions!(PenToolMessageDiscriminant; |
|
DragStart, |
|
DragStop, |
|
PointerMove, |
|
Confirm, |
|
Abort, |
|
RemovePreviousHandle, |
|
GRS, |
|
SwapHandles |
|
), |
|
} |
|
} |
|
} |
|
|
|
impl ToolTransition for PenTool { |
|
fn event_to_message_map(&self) -> EventToMessageMap { |
|
EventToMessageMap { |
|
tool_abort: Some(PenToolMessage::Abort.into()), |
|
selection_changed: Some(PenToolMessage::SelectionChanged.into()), |
|
working_color_changed: Some(PenToolMessage::WorkingColorChanged.into()), |
|
overlay_provider: Some(|overlay_context| PenToolMessage::Overlays(overlay_context).into()), |
|
..Default::default() |
|
} |
|
} |
|
} |
|
#[derive(Clone, Debug, Default)] |
|
struct ModifierState { |
|
snap_angle: bool, |
|
lock_angle: bool, |
|
break_handle: bool, |
|
colinear: bool, |
|
move_anchor_with_handles: bool, |
|
} |
|
#[derive(Clone, Debug)] |
|
struct LastPoint { |
|
id: PointId, |
|
pos: DVec2, |
|
in_segment: Option<SegmentId>, |
|
handle_start: DVec2, |
|
} |
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] |
|
enum HandleMode { |
|
|
|
Free, |
|
|
|
#[default] |
|
ColinearLocked, |
|
|
|
ColinearEquidistant, |
|
} |
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq, Copy)] |
|
enum TargetHandle { |
|
#[default] |
|
None, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
FuturePreviewOutHandle, |
|
|
|
|
|
|
|
|
|
PreviewInHandle, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
PriorOutHandle(SegmentId), |
|
|
|
|
|
|
|
|
|
|
|
PriorInHandle(SegmentId), |
|
} |
|
|
|
#[derive(Clone, Debug, Default)] |
|
struct PenToolData { |
|
snap_manager: SnapManager, |
|
latest_points: Vec<LastPoint>, |
|
point_index: usize, |
|
handle_end: Option<DVec2>, |
|
next_point: DVec2, |
|
next_handle_start: DVec2, |
|
|
|
g1_continuous: bool, |
|
|
|
angle: f64, |
|
auto_panning: AutoPanning, |
|
modifiers: ModifierState, |
|
|
|
buffering_merged_vector: bool, |
|
|
|
previous_handle_start_pos: DVec2, |
|
previous_handle_end_pos: Option<DVec2>, |
|
toggle_colinear_debounce: bool, |
|
colinear: bool, |
|
alt_pressed: bool, |
|
space_pressed: bool, |
|
|
|
|
|
switch_to_free_on_ctrl_release: bool, |
|
|
|
handle_swapped: bool, |
|
|
|
|
|
|
|
angle_locked: bool, |
|
path_closed: bool, |
|
|
|
handle_mode: HandleMode, |
|
prior_segment_layer: Option<LayerNodeIdentifier>, |
|
current_layer: Option<LayerNodeIdentifier>, |
|
prior_segment_endpoint: Option<PointId>, |
|
prior_segment: Option<SegmentId>, |
|
|
|
|
|
prior_segments: Option<Vec<SegmentId>>, |
|
handle_type: TargetHandle, |
|
handle_start_offset: Option<DVec2>, |
|
handle_end_offset: Option<DVec2>, |
|
|
|
snap_cache: SnapCache, |
|
} |
|
|
|
impl PenToolData { |
|
fn latest_point(&self) -> Option<&LastPoint> { |
|
self.latest_points.get(self.point_index) |
|
} |
|
|
|
fn latest_point_mut(&mut self) -> Option<&mut LastPoint> { |
|
self.latest_points.get_mut(self.point_index) |
|
} |
|
|
|
fn add_point(&mut self, point: LastPoint) { |
|
self.point_index = (self.point_index + 1).min(self.latest_points.len()); |
|
self.latest_points.truncate(self.point_index); |
|
self.latest_points.push(point); |
|
} |
|
|
|
fn cleanup(&mut self, responses: &mut VecDeque<Message>) { |
|
self.handle_end = None; |
|
self.latest_points.clear(); |
|
self.point_index = 0; |
|
self.snap_manager.cleanup(responses); |
|
} |
|
|
|
|
|
fn check_end_handle_type(&self, vector_data: &VectorData) -> TargetHandle { |
|
match (self.handle_end, self.prior_segment_endpoint, self.prior_segment, self.path_closed) { |
|
(Some(_), _, _, false) => TargetHandle::PreviewInHandle, |
|
(None, Some(point), Some(segment), false) | (Some(_), Some(point), Some(segment), true) => { |
|
if vector_data.segment_start_from_id(segment) == Some(point) { |
|
TargetHandle::PriorOutHandle(segment) |
|
} else { |
|
TargetHandle::PriorInHandle(segment) |
|
} |
|
} |
|
_ => TargetHandle::None, |
|
} |
|
} |
|
|
|
fn check_grs_end_handle(&self, vector_data: &VectorData) -> TargetHandle { |
|
let Some(point) = self.latest_point().map(|point| point.id) else { return TargetHandle::None }; |
|
let Some(segment) = self.prior_segment else { return TargetHandle::None }; |
|
|
|
if vector_data.segment_start_from_id(segment) == Some(point) { |
|
TargetHandle::PriorOutHandle(segment) |
|
} else { |
|
TargetHandle::PriorInHandle(segment) |
|
} |
|
} |
|
|
|
fn get_opposite_handle_type(&self, handle_type: TargetHandle, vector_data: &VectorData) -> TargetHandle { |
|
match handle_type { |
|
TargetHandle::FuturePreviewOutHandle => self.check_end_handle_type(vector_data), |
|
TargetHandle::PreviewInHandle => match (self.path_closed, self.prior_segment_endpoint, self.prior_segment) { |
|
(true, Some(point), Some(segment)) => { |
|
if vector_data.segment_start_from_id(segment) == Some(point) { |
|
TargetHandle::PriorOutHandle(segment) |
|
} else { |
|
TargetHandle::PriorInHandle(segment) |
|
} |
|
} |
|
(false, _, _) => TargetHandle::FuturePreviewOutHandle, |
|
_ => TargetHandle::None, |
|
}, |
|
_ => { |
|
if self.path_closed { |
|
TargetHandle::PreviewInHandle |
|
} else { |
|
TargetHandle::FuturePreviewOutHandle |
|
} |
|
} |
|
} |
|
} |
|
|
|
fn update_handle_type(&mut self, handle_type: TargetHandle) { |
|
self.handle_type = handle_type; |
|
} |
|
|
|
fn update_target_handle_pos(&mut self, handle_type: TargetHandle, anchor_pos: DVec2, responses: &mut VecDeque<Message>, delta: DVec2, layer: LayerNodeIdentifier) { |
|
match handle_type { |
|
TargetHandle::FuturePreviewOutHandle => { |
|
self.next_handle_start = delta; |
|
} |
|
TargetHandle::PreviewInHandle => { |
|
if let Some(handle) = self.handle_end.as_mut() { |
|
*handle = delta; |
|
} |
|
} |
|
TargetHandle::PriorInHandle(segment) => { |
|
let relative_position = delta - anchor_pos; |
|
let modification_type = VectorModificationType::SetEndHandle { segment, relative_position }; |
|
responses.add(GraphOperationMessage::Vector { layer, modification_type }); |
|
} |
|
TargetHandle::PriorOutHandle(segment) => { |
|
let relative_position = delta - anchor_pos; |
|
let modification_type = VectorModificationType::SetPrimaryHandle { segment, relative_position }; |
|
responses.add(GraphOperationMessage::Vector { layer, modification_type }); |
|
} |
|
TargetHandle::None => {} |
|
} |
|
} |
|
|
|
fn target_handle_position(&self, handle_type: TargetHandle, vector_data: &VectorData) -> Option<DVec2> { |
|
match handle_type { |
|
TargetHandle::PriorOutHandle(segment) => ManipulatorPointId::PrimaryHandle(segment).get_position(vector_data), |
|
TargetHandle::PriorInHandle(segment) => ManipulatorPointId::EndHandle(segment).get_position(vector_data), |
|
TargetHandle::PreviewInHandle => self.handle_end, |
|
TargetHandle::FuturePreviewOutHandle => Some(self.next_handle_start), |
|
TargetHandle::None => None, |
|
} |
|
} |
|
|
|
|
|
fn cleanup_target_selections(&self, shape_editor: &mut ShapeState, layer: Option<LayerNodeIdentifier>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) { |
|
let Some(shape_state) = layer.and_then(|layer| shape_editor.selected_shape_state.get_mut(&layer)) else { |
|
return; |
|
}; |
|
|
|
let Some(vector_data) = layer.and_then(|layer| document.network_interface.compute_modified_vector(layer)) else { |
|
return; |
|
}; |
|
|
|
match self.check_end_handle_type(&vector_data) { |
|
TargetHandle::PriorInHandle(segment) => shape_state.deselect_point(ManipulatorPointId::EndHandle(segment)), |
|
TargetHandle::PriorOutHandle(segment) => shape_state.deselect_point(ManipulatorPointId::PrimaryHandle(segment)), |
|
_ => {} |
|
} |
|
responses.add(OverlaysMessage::Draw); |
|
} |
|
|
|
|
|
fn add_target_selections(&self, shape_editor: &mut ShapeState, layer: Option<LayerNodeIdentifier>) { |
|
let Some(shape_state) = layer.and_then(|layer| shape_editor.selected_shape_state.get_mut(&layer)) else { |
|
return; |
|
}; |
|
|
|
match self.handle_type { |
|
TargetHandle::PriorInHandle(segment) => shape_state.select_point(ManipulatorPointId::EndHandle(segment)), |
|
TargetHandle::PriorOutHandle(segment) => shape_state.select_point(ManipulatorPointId::PrimaryHandle(segment)), |
|
_ => {} |
|
} |
|
} |
|
|
|
|
|
fn moving_start_point(&self) -> bool { |
|
self.latest_points.len() == 1 && self.latest_point().is_some_and(|point| point.pos == self.next_point) |
|
} |
|
|
|
|
|
fn recalculate_latest_points_position(&mut self, document: &DocumentMessageHandler) { |
|
let selected_nodes = document.network_interface.selected_nodes(); |
|
let mut selected_layers = selected_nodes.selected_layers(document.metadata()); |
|
if let (Some(layer), None) = (selected_layers.next(), selected_layers.next()) { |
|
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { |
|
return; |
|
}; |
|
for point in &mut self.latest_points { |
|
let Some(pos) = vector_data.point_domain.position_from_id(point.id) else { |
|
continue; |
|
}; |
|
point.pos = pos; |
|
point.handle_start = point.pos; |
|
} |
|
} |
|
} |
|
|
|
|
|
fn bend_from_previous_point( |
|
&mut self, |
|
snap_data: SnapData, |
|
transform: DAffine2, |
|
layer: LayerNodeIdentifier, |
|
preferences: &PreferencesMessageHandler, |
|
shape_editor: &mut ShapeState, |
|
responses: &mut VecDeque<Message>, |
|
) { |
|
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(); |
|
self.update_handle_type(TargetHandle::FuturePreviewOutHandle); |
|
self.handle_mode = HandleMode::ColinearLocked; |
|
|
|
|
|
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; |
|
|
|
self.store_clicked_endpoint(document, &transform, snap_data.input, preferences); |
|
|
|
if self.modifiers.lock_angle { |
|
self.set_lock_angle(&vector_data, id, self.prior_segment); |
|
let last_segment = self.prior_segment; |
|
let Some(point) = self.latest_point_mut() else { return }; |
|
point.in_segment = last_segment; |
|
self.switch_to_free_on_ctrl_release = true; |
|
return; |
|
} |
|
|
|
if let Some(point) = self.latest_point_mut() { |
|
point.in_segment = None; |
|
} |
|
} |
|
|
|
|
|
let closing_path_on_point = self.close_path_on_point(snap_data, &vector_data, document, preferences, id, &transform); |
|
if !closing_path_on_point && preferences.vector_meshes { |
|
|
|
let tolerance = crate::consts::SNAP_POINT_TOLERANCE; |
|
if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, transform.transform_point2(self.next_point), tolerance) { |
|
let (point, _) = closest_segment.adjusted_insert(responses); |
|
|
|
self.update_handle_type(TargetHandle::PreviewInHandle); |
|
self.handle_end_offset = None; |
|
self.path_closed = true; |
|
self.next_handle_start = self.next_point; |
|
|
|
self.prior_segment_endpoint = Some(point); |
|
self.prior_segment_layer = Some(closest_segment.layer()); |
|
self.prior_segments = None; |
|
self.prior_segment = None; |
|
|
|
|
|
|
|
self.handle_mode = HandleMode::Free; |
|
if let (true, Some(prior_endpoint)) = (self.modifiers.lock_angle, self.prior_segment_endpoint) { |
|
self.set_lock_angle(&vector_data, prior_endpoint, self.prior_segment); |
|
self.switch_to_free_on_ctrl_release = true; |
|
} |
|
} |
|
} |
|
} |
|
|
|
fn close_path_on_point( |
|
&mut self, |
|
snap_data: SnapData, |
|
vector_data: &VectorData, |
|
document: &DocumentMessageHandler, |
|
preferences: &PreferencesMessageHandler, |
|
id: PointId, |
|
transform: &DAffine2, |
|
) -> bool { |
|
for id in vector_data.extendable_points(preferences.vector_meshes).filter(|&point| point != id) { |
|
let Some(pos) = vector_data.point_domain.position_from_id(id) else { continue }; |
|
let transformed_distance_between_squared = transform.transform_point2(pos).distance_squared(transform.transform_point2(self.next_point)); |
|
let snap_point_tolerance_squared = crate::consts::SNAP_POINT_TOLERANCE.powi(2); |
|
|
|
if transformed_distance_between_squared < snap_point_tolerance_squared { |
|
self.update_handle_type(TargetHandle::PreviewInHandle); |
|
self.handle_end_offset = None; |
|
self.path_closed = true; |
|
self.next_handle_start = self.next_point; |
|
self.store_clicked_endpoint(document, transform, snap_data.input, preferences); |
|
self.handle_mode = HandleMode::Free; |
|
if let (true, Some(prior_endpoint)) = (self.modifiers.lock_angle, self.prior_segment_endpoint) { |
|
self.set_lock_angle(vector_data, prior_endpoint, self.prior_segment); |
|
self.switch_to_free_on_ctrl_release = true; |
|
} |
|
return true; |
|
} |
|
} |
|
false |
|
} |
|
|
|
fn finish_placing_handle(&mut self, snap_data: SnapData, transform: DAffine2, preferences: &PreferencesMessageHandler, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> { |
|
let document = snap_data.document; |
|
let next_handle_start = self.next_handle_start; |
|
let handle_start = self.latest_point()?.handle_start; |
|
let mouse = snap_data.input.mouse.position; |
|
self.handle_swapped = false; |
|
self.handle_end_offset = None; |
|
self.handle_start_offset = None; |
|
let Some(handle_end) = self.handle_end else { |
|
responses.add(DocumentMessage::EndTransaction); |
|
self.handle_end = Some(next_handle_start); |
|
self.place_anchor(snap_data, transform, mouse, preferences, responses); |
|
self.latest_point_mut()?.handle_start = next_handle_start; |
|
return None; |
|
}; |
|
let next_point = self.next_point; |
|
self.place_anchor(snap_data, transform, mouse, preferences, responses); |
|
let handles = [handle_start - self.latest_point()?.pos, handle_end - next_point].map(Some); |
|
|
|
|
|
let mut end = None; |
|
let selected_nodes = document.network_interface.selected_nodes(); |
|
let mut selected_layers = selected_nodes.selected_layers(document.metadata()); |
|
let layer = selected_layers.next().filter(|_| selected_layers.next().is_none()).or(self.current_layer)?; |
|
let vector_data = document.network_interface.compute_modified_vector(layer)?; |
|
let start = self.latest_point()?.id; |
|
let transform = document.metadata().document_to_viewport * transform; |
|
for id in vector_data.extendable_points(preferences.vector_meshes).filter(|&point| point != start) { |
|
let Some(pos) = vector_data.point_domain.position_from_id(id) else { continue }; |
|
let transformed_distance_between_squared = transform.transform_point2(pos).distance_squared(transform.transform_point2(next_point)); |
|
let snap_point_tolerance_squared = crate::consts::SNAP_POINT_TOLERANCE.powi(2); |
|
if transformed_distance_between_squared < snap_point_tolerance_squared { |
|
end = Some(id); |
|
} |
|
} |
|
let close_subpath = end.is_some(); |
|
|
|
|
|
let end = end.unwrap_or_else(|| { |
|
let end = PointId::generate(); |
|
let modification_type = VectorModificationType::InsertPoint { id: end, position: next_point }; |
|
responses.add(GraphOperationMessage::Vector { layer, modification_type }); |
|
|
|
end |
|
}); |
|
|
|
|
|
let id = SegmentId::generate(); |
|
if self.path_closed { |
|
if let Some((handles, handle1_pos)) = match self.get_opposite_handle_type(TargetHandle::PreviewInHandle, &vector_data) { |
|
TargetHandle::PriorOutHandle(segment) => { |
|
let handles = [HandleId::end(id), HandleId::primary(segment)]; |
|
let handle1_pos = handles[1].to_manipulator_point().get_position(&vector_data); |
|
handle1_pos.map(|pos| (handles, pos)) |
|
} |
|
TargetHandle::PriorInHandle(segment) => { |
|
let handles = [HandleId::end(id), HandleId::end(segment)]; |
|
let handle1_pos = handles[1].to_manipulator_point().get_position(&vector_data); |
|
handle1_pos.map(|pos| (handles, pos)) |
|
} |
|
_ => None, |
|
} { |
|
let angle = (handle_end - next_point).angle_to(handle1_pos - next_point); |
|
let pi = std::f64::consts::PI; |
|
let colinear = (angle - pi).abs() < 1e-6 || (angle + pi).abs() < 1e-6; |
|
responses.add(GraphOperationMessage::Vector { |
|
layer, |
|
modification_type: VectorModificationType::SetG1Continuous { handles, enabled: colinear }, |
|
}); |
|
self.cleanup(responses); |
|
} |
|
} |
|
|
|
self.prior_segment = Some(id); |
|
|
|
let points = [start, end]; |
|
let modification_type = VectorModificationType::InsertSegment { id, points, handles }; |
|
responses.add(GraphOperationMessage::Vector { layer, modification_type }); |
|
|
|
|
|
if let Some((last_segment, last_point)) = self.latest_point().and_then(|point| point.in_segment).zip(self.latest_point()) { |
|
let end = vector_data.segment_end_from_id(last_segment) == Some(last_point.id); |
|
let handles = if end { |
|
[HandleId::end(last_segment), HandleId::primary(id)] |
|
} else { |
|
[HandleId::primary(last_segment), HandleId::primary(id)] |
|
}; |
|
|
|
if let Some(h1) = handles[0].to_manipulator_point().get_position(&vector_data) { |
|
let angle = (h1 - last_point.pos).angle_to(last_point.handle_start - last_point.pos); |
|
let pi = std::f64::consts::PI; |
|
let colinear = (angle - pi).abs() < 1e-6 || (angle + pi).abs() < 1e-6; |
|
responses.add(GraphOperationMessage::Vector { |
|
layer, |
|
modification_type: VectorModificationType::SetG1Continuous { handles, enabled: colinear }, |
|
}); |
|
} |
|
} |
|
if !close_subpath { |
|
self.add_point(LastPoint { |
|
id: end, |
|
pos: next_point, |
|
in_segment: self.g1_continuous.then_some(id), |
|
handle_start: next_handle_start, |
|
}); |
|
} |
|
self.path_closed = false; |
|
self.prior_segment_endpoint = None; |
|
responses.add(DocumentMessage::EndTransaction); |
|
Some(if close_subpath { PenToolFsmState::Ready } else { PenToolFsmState::PlacingAnchor }) |
|
} |
|
|
|
#[allow(clippy::too_many_arguments)] |
|
|
|
fn space_anchor_handle_snap( |
|
&mut self, |
|
viewport_to_document: &DAffine2, |
|
transform: &DAffine2, |
|
snap_data: &SnapData<'_>, |
|
mouse: &DVec2, |
|
vector_data: &VectorData, |
|
input: &InputPreprocessorMessageHandler, |
|
) -> Option<DVec2> { |
|
let reference_handle = if self.path_closed { TargetHandle::PreviewInHandle } else { TargetHandle::FuturePreviewOutHandle }; |
|
let end_handle = self.get_opposite_handle_type(reference_handle, vector_data); |
|
let end_handle_pos = self.target_handle_position(end_handle, vector_data); |
|
let ref_pos = self.target_handle_position(reference_handle, vector_data)?; |
|
let snap = &mut self.snap_manager; |
|
let snap_data = SnapData::new_snap_cache(snap_data.document, input, &self.snap_cache); |
|
|
|
let handle_start_offset = self.handle_start_offset.unwrap_or(DVec2::ZERO); |
|
let document_pos = viewport_to_document.transform_point2(*mouse + handle_start_offset); |
|
|
|
let anchor_offset = transform.transform_point2(self.next_point - ref_pos); |
|
|
|
let handle_start = SnapCandidatePoint::handle(document_pos); |
|
let anchor = SnapCandidatePoint::handle(document_pos + anchor_offset); |
|
|
|
let snapped_near_handle_start = snap.free_snap(&snap_data, &handle_start, SnapTypeConfiguration::default()); |
|
let snapped_anchor = snap.free_snap(&snap_data, &anchor, SnapTypeConfiguration::default()); |
|
|
|
let handle_snap_option = end_handle_pos.and_then(|handle| match end_handle { |
|
TargetHandle::None => None, |
|
TargetHandle::FuturePreviewOutHandle => None, |
|
_ => { |
|
let handle_offset = transform.transform_point2(handle - ref_pos); |
|
let handle_snap = SnapCandidatePoint::handle(document_pos + handle_offset); |
|
Some((handle, handle_snap)) |
|
} |
|
}); |
|
|
|
let mut delta: DVec2; |
|
let best_snapped = if snapped_near_handle_start.other_snap_better(&snapped_anchor) { |
|
delta = snapped_anchor.snapped_point_document - transform.transform_point2(self.next_point); |
|
snapped_anchor |
|
} else { |
|
delta = snapped_near_handle_start.snapped_point_document - transform.transform_point2(ref_pos); |
|
snapped_near_handle_start |
|
}; |
|
|
|
let Some((handle, handle_snap)) = handle_snap_option else { |
|
snap.update_indicator(best_snapped); |
|
return Some(transform.inverse().transform_vector2(delta)); |
|
}; |
|
|
|
let snapped_handle = snap.free_snap(&snap_data, &handle_snap, SnapTypeConfiguration::default()); |
|
|
|
if best_snapped.other_snap_better(&snapped_handle) { |
|
delta = snapped_handle.snapped_point_document - transform.transform_point2(handle); |
|
snap.update_indicator(snapped_handle); |
|
} else { |
|
snap.update_indicator(best_snapped); |
|
} |
|
|
|
|
|
Some(transform.inverse().transform_vector2(delta)) |
|
} |
|
|
|
|
|
fn swap_handles( |
|
&mut self, |
|
layer: Option<LayerNodeIdentifier>, |
|
document: &DocumentMessageHandler, |
|
shape_editor: &mut ShapeState, |
|
input: &InputPreprocessorMessageHandler, |
|
responses: &mut VecDeque<Message>, |
|
) { |
|
|
|
let Some(vector_data) = layer.and_then(|layer| document.network_interface.compute_modified_vector(layer)) else { |
|
return; |
|
}; |
|
|
|
let Some(viewport) = layer.map(|layer| document.metadata().transform_to_viewport(layer)) else { |
|
return; |
|
}; |
|
|
|
|
|
let should_swap_to_opposite = self.path_closed && matches!(self.handle_type, TargetHandle::PreviewInHandle | TargetHandle::PriorOutHandle(..) | TargetHandle::PriorInHandle(..)) |
|
|| !self.path_closed && matches!(self.handle_type, TargetHandle::FuturePreviewOutHandle); |
|
|
|
|
|
let should_swap_to_start = !self.path_closed && !matches!(self.handle_type, TargetHandle::None | TargetHandle::FuturePreviewOutHandle); |
|
|
|
if should_swap_to_opposite { |
|
let opposite_type = self.get_opposite_handle_type(self.handle_type, &vector_data); |
|
|
|
let Some(handle_pos) = self.target_handle_position(opposite_type, &vector_data) else { |
|
self.handle_swapped = false; |
|
return; |
|
}; |
|
if (handle_pos - self.next_point).length() < 1e-6 { |
|
self.handle_swapped = false; |
|
return; |
|
} |
|
self.handle_end_offset = Some(viewport.transform_point2(handle_pos) - input.mouse.position); |
|
|
|
|
|
if self.path_closed { |
|
self.cleanup_target_selections(shape_editor, layer, document, responses); |
|
} |
|
self.update_handle_type(opposite_type); |
|
self.add_target_selections(shape_editor, layer); |
|
} else if should_swap_to_start { |
|
self.cleanup_target_selections(shape_editor, layer, document, responses); |
|
|
|
|
|
if let Some(layer_id) = layer { |
|
let transform = document.metadata().transform_to_viewport(layer_id); |
|
self.handle_start_offset = Some(transform.transform_point2(self.next_handle_start) - input.mouse.position); |
|
} |
|
|
|
self.update_handle_type(TargetHandle::FuturePreviewOutHandle); |
|
} |
|
|
|
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::None }); |
|
} |
|
|
|
|
|
fn handle_single_point_path_drag(&mut self, delta: DVec2, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> { |
|
self.next_handle_start += delta; |
|
self.next_point += delta; |
|
|
|
let Some(latest) = self.latest_point_mut() else { |
|
return Some(PenToolFsmState::DraggingHandle(self.handle_mode)); |
|
}; |
|
|
|
latest.pos += delta; |
|
|
|
let modification_type = VectorModificationType::ApplyPointDelta { point: latest.id, delta }; |
|
|
|
responses.add(GraphOperationMessage::Vector { layer, modification_type }); |
|
|
|
responses.add(OverlaysMessage::Draw); |
|
Some(PenToolFsmState::DraggingHandle(self.handle_mode)) |
|
} |
|
|
|
fn move_anchor_and_handles(&mut self, delta: DVec2, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>, vector_data: &VectorData) { |
|
if self.handle_end.is_none() { |
|
if let Some(latest_pt) = self.latest_point_mut() { |
|
latest_pt.pos += delta; |
|
} |
|
} |
|
|
|
let Some(end_point) = self.prior_segment_endpoint else { return }; |
|
|
|
let modification_type_anchor = VectorModificationType::ApplyPointDelta { point: end_point, delta }; |
|
responses.add(GraphOperationMessage::Vector { |
|
layer, |
|
modification_type: modification_type_anchor, |
|
}); |
|
|
|
let reference_handle = if self.path_closed { TargetHandle::PreviewInHandle } else { TargetHandle::FuturePreviewOutHandle }; |
|
|
|
|
|
let end_handle_type = self.get_opposite_handle_type(reference_handle, vector_data); |
|
match end_handle_type { |
|
TargetHandle::PriorInHandle(..) | TargetHandle::PriorOutHandle(..) => { |
|
let Some(handle_pos) = self.target_handle_position(end_handle_type, vector_data) else { return }; |
|
self.update_target_handle_pos(end_handle_type, self.next_point, responses, handle_pos + delta, layer); |
|
} |
|
_ => {} |
|
} |
|
} |
|
|
|
fn drag_handle( |
|
&mut self, |
|
snap_data: SnapData, |
|
transform: DAffine2, |
|
mouse: DVec2, |
|
responses: &mut VecDeque<Message>, |
|
layer: Option<LayerNodeIdentifier>, |
|
input: &InputPreprocessorMessageHandler, |
|
) -> 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; |
|
let Some(layer) = layer else { return Some(PenToolFsmState::DraggingHandle(self.handle_mode)) }; |
|
let vector_data = document.network_interface.compute_modified_vector(layer)?; |
|
let viewport_to_document = document.metadata().document_to_viewport.inverse(); |
|
let mut mouse_pos = mouse; |
|
|
|
|
|
if self.modifiers.move_anchor_with_handles { |
|
let Some(delta) = self.space_anchor_handle_snap(&viewport_to_document, &transform, &snap_data, &mouse, &vector_data, input) else { |
|
return Some(PenToolFsmState::DraggingHandle(self.handle_mode)); |
|
}; |
|
|
|
if self.moving_start_point() { |
|
return self.handle_single_point_path_drag(delta, layer, responses); |
|
} |
|
|
|
self.next_handle_start += delta; |
|
self.next_point += delta; |
|
|
|
if let Some(handle) = self.handle_end.as_mut() { |
|
*handle += delta; |
|
if !self.path_closed { |
|
responses.add(OverlaysMessage::Draw); |
|
return Some(PenToolFsmState::DraggingHandle(self.handle_mode)); |
|
}; |
|
} |
|
|
|
self.move_anchor_and_handles(delta, layer, responses, &vector_data); |
|
|
|
responses.add(OverlaysMessage::Draw); |
|
return Some(PenToolFsmState::DraggingHandle(self.handle_mode)); |
|
} |
|
|
|
match self.handle_type { |
|
TargetHandle::FuturePreviewOutHandle => { |
|
let offset = self.handle_start_offset.unwrap_or(DVec2::ZERO); |
|
mouse_pos += offset; |
|
self.next_handle_start = self.compute_snapped_angle(snap_data.clone(), transform, colinear, mouse_pos, Some(self.next_point), false); |
|
} |
|
_ => { |
|
mouse_pos += self.handle_end_offset.unwrap_or(DVec2::ZERO); |
|
let mouse_pos = self.compute_snapped_angle(snap_data.clone(), transform, colinear, mouse_pos, Some(self.next_point), false); |
|
self.update_target_handle_pos(self.handle_type, self.next_point, responses, mouse_pos, layer); |
|
} |
|
} |
|
|
|
let mouse_pos = viewport_to_document.transform_point2(mouse_pos); |
|
let anchor = transform.transform_point2(self.next_point); |
|
let distance = (mouse_pos - anchor).length(); |
|
|
|
if self.switch_to_free_on_ctrl_release && !self.modifiers.lock_angle { |
|
self.switch_to_free_on_ctrl_release = false; |
|
self.handle_mode = HandleMode::Free; |
|
} |
|
|
|
if distance > 20. && self.handle_mode == HandleMode::Free && self.modifiers.lock_angle && !self.angle_locked { |
|
self.angle_locked = true |
|
} |
|
|
|
match self.handle_mode { |
|
HandleMode::ColinearLocked | HandleMode::ColinearEquidistant => { |
|
self.g1_continuous = true; |
|
self.apply_colinear_constraint(responses, layer, self.next_point, &vector_data); |
|
self.adjust_handle_length(responses, layer, &vector_data); |
|
} |
|
HandleMode::Free => { |
|
self.g1_continuous = false; |
|
} |
|
} |
|
|
|
if distance < 20. && self.handle_mode == HandleMode::Free && self.modifiers.lock_angle && !self.angle_locked { |
|
let Some(endpoint) = self.prior_segment_endpoint else { |
|
return Some(PenToolFsmState::DraggingHandle(self.handle_mode)); |
|
}; |
|
self.set_lock_angle(&vector_data, endpoint, self.prior_segment); |
|
self.switch_to_free_on_ctrl_release = true; |
|
let last_segment = self.prior_segment; |
|
if let Some(latest) = self.latest_point_mut() { |
|
latest.in_segment = last_segment; |
|
} |
|
} |
|
|
|
responses.add(OverlaysMessage::Draw); |
|
|
|
Some(PenToolFsmState::DraggingHandle(self.handle_mode)) |
|
} |
|
|
|
|
|
fn adjust_handle_length(&mut self, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier, vector_data: &VectorData) { |
|
let opposite_handle_type = self.get_opposite_handle_type(self.handle_type, vector_data); |
|
match self.handle_mode { |
|
HandleMode::ColinearEquidistant => { |
|
if self.modifiers.break_handle { |
|
|
|
if !self.alt_pressed { |
|
self.previous_handle_end_pos = self.target_handle_position(opposite_handle_type, vector_data); |
|
self.alt_pressed = true; |
|
} |
|
|
|
|
|
let Some(new_position) = self.target_handle_position(self.handle_type, vector_data).map(|handle| self.next_point * 2. - handle) else { |
|
return; |
|
}; |
|
self.update_target_handle_pos(opposite_handle_type, self.next_point, responses, new_position, layer); |
|
} else if self.alt_pressed { |
|
|
|
if let Some(previous_handle) = self.previous_handle_end_pos { |
|
self.update_target_handle_pos(opposite_handle_type, self.next_point, responses, previous_handle, layer); |
|
} |
|
self.alt_pressed = false; |
|
self.previous_handle_end_pos = None; |
|
} |
|
} |
|
HandleMode::ColinearLocked => { |
|
if !self.modifiers.break_handle { |
|
let Some(new_position) = self.target_handle_position(self.handle_type, vector_data).map(|handle| self.next_point * 2. - handle) else { |
|
return; |
|
}; |
|
self.update_target_handle_pos(opposite_handle_type, self.next_point, responses, new_position, layer); |
|
} |
|
} |
|
HandleMode::Free => {} |
|
} |
|
} |
|
|
|
fn apply_colinear_constraint(&mut self, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier, anchor_pos: DVec2, vector_data: &VectorData) { |
|
let Some(handle) = self.target_handle_position(self.handle_type, vector_data) else { |
|
return; |
|
}; |
|
|
|
if (anchor_pos - handle).length() < 1e-6 && self.modifiers.lock_angle { |
|
return; |
|
} |
|
|
|
let Some(direction) = (anchor_pos - handle).try_normalize() else { |
|
return; |
|
}; |
|
let opposite_handle = self.get_opposite_handle_type(self.handle_type, vector_data); |
|
let Some(handle_offset) = self.target_handle_position(opposite_handle, vector_data).map(|handle| (handle - anchor_pos).length()) else { |
|
return; |
|
}; |
|
let new_handle_position = anchor_pos + handle_offset * direction; |
|
self.update_target_handle_pos(opposite_handle, self.next_point, responses, new_handle_position, layer); |
|
} |
|
|
|
fn place_anchor(&mut self, snap_data: SnapData, transform: DAffine2, mouse: DVec2, preferences: &PreferencesMessageHandler, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> { |
|
let document = snap_data.document; |
|
|
|
let relative = if self.path_closed { None } else { self.latest_point().map(|point| point.pos) }; |
|
self.next_point = self.compute_snapped_angle(snap_data, transform, false, mouse, relative, true); |
|
|
|
let selected_nodes = document.network_interface.selected_nodes(); |
|
let mut selected_layers = selected_nodes.selected_layers(document.metadata()); |
|
let layer = selected_layers.next().filter(|_| selected_layers.next().is_none()).or(self.current_layer)?; |
|
let vector_data = document.network_interface.compute_modified_vector(layer)?; |
|
let transform = document.metadata().document_to_viewport * transform; |
|
for point in vector_data.extendable_points(preferences.vector_meshes) { |
|
let Some(pos) = vector_data.point_domain.position_from_id(point) else { continue }; |
|
let transformed_distance_between_squared = transform.transform_point2(pos).distance_squared(transform.transform_point2(self.next_point)); |
|
let snap_point_tolerance_squared = crate::consts::SNAP_POINT_TOLERANCE.powi(2); |
|
if transformed_distance_between_squared < snap_point_tolerance_squared { |
|
self.next_point = pos; |
|
} |
|
} |
|
if let Some(handle_end) = self.handle_end.as_mut() { |
|
*handle_end = self.next_point; |
|
self.next_handle_start = self.next_point; |
|
} |
|
responses.add(OverlaysMessage::Draw); |
|
|
|
Some(PenToolFsmState::PlacingAnchor) |
|
} |
|
|
|
|
|
fn compute_snapped_angle(&mut self, snap_data: SnapData, transform: DAffine2, colinear: bool, mouse: DVec2, relative: Option<DVec2>, neighbor: bool) -> DVec2 { |
|
let ModifierState { snap_angle, lock_angle, .. } = self.modifiers; |
|
let document = snap_data.document; |
|
let mut document_pos = document.metadata().document_to_viewport.inverse().transform_point2(mouse); |
|
let snap = &mut self.snap_manager; |
|
|
|
let neighbors = relative.filter(|_| neighbor).map_or(Vec::new(), |neighbor| vec![neighbor]); |
|
|
|
let config = SnapTypeConfiguration::default(); |
|
if let Some(relative) = relative |
|
.map(|layer| transform.transform_point2(layer)) |
|
.filter(|&relative| (snap_angle || lock_angle) && (relative - document_pos).length_squared() > f64::EPSILON * 100.) |
|
{ |
|
let resolution = LINE_ROTATE_SNAP_ANGLE.to_radians(); |
|
|
|
let angle = if lock_angle { |
|
self.angle |
|
} else if (relative - document_pos) != DVec2::ZERO && !lock_angle { |
|
(-(relative - document_pos).angle_to(DVec2::X) / resolution).round() * resolution |
|
} else { |
|
self.angle |
|
}; |
|
document_pos = relative - (relative - document_pos).project_onto(DVec2::new(angle.cos(), angle.sin())); |
|
|
|
let constraint = SnapConstraint::Line { |
|
origin: relative, |
|
direction: document_pos - relative, |
|
}; |
|
let near_point = SnapCandidatePoint::handle_neighbors(document_pos, neighbors.clone()); |
|
let far_point = SnapCandidatePoint::handle_neighbors(2. * relative - document_pos, neighbors); |
|
if colinear { |
|
let snapped = snap.constrained_snap(&snap_data, &near_point, constraint, config); |
|
let snapped_far = snap.constrained_snap(&snap_data, &far_point, constraint, config); |
|
document_pos = if snapped_far.other_snap_better(&snapped) { |
|
snapped.snapped_point_document |
|
} else { |
|
2. * relative - snapped_far.snapped_point_document |
|
}; |
|
snap.update_indicator(if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far }); |
|
} else { |
|
let snapped = snap.constrained_snap(&snap_data, &near_point, constraint, config); |
|
document_pos = snapped.snapped_point_document; |
|
snap.update_indicator(snapped); |
|
} |
|
} else if let Some(relative) = relative.map(|layer| transform.transform_point2(layer)).filter(|_| colinear) { |
|
let snapped = snap.free_snap(&snap_data, &SnapCandidatePoint::handle_neighbors(document_pos, neighbors.clone()), config); |
|
let snapped_far = snap.free_snap(&snap_data, &SnapCandidatePoint::handle_neighbors(2. * relative - document_pos, neighbors), config); |
|
document_pos = if snapped_far.other_snap_better(&snapped) { |
|
snapped.snapped_point_document |
|
} else { |
|
2. * relative - snapped_far.snapped_point_document |
|
}; |
|
snap.update_indicator(if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far }); |
|
} else { |
|
let snapped = snap.free_snap(&snap_data, &SnapCandidatePoint::handle_neighbors(document_pos, neighbors), config); |
|
document_pos = snapped.snapped_point_document; |
|
snap.update_indicator(snapped); |
|
} |
|
|
|
if let Some(relative) = relative.map(|layer| transform.transform_point2(layer)) { |
|
if (relative - document_pos) != DVec2::ZERO && (relative - document_pos).length_squared() > f64::EPSILON * 100. { |
|
self.angle = -(relative - document_pos).angle_to(DVec2::X) |
|
} |
|
} |
|
|
|
transform.inverse().transform_point2(document_pos) |
|
} |
|
|
|
#[allow(clippy::too_many_arguments)] |
|
fn create_initial_point( |
|
&mut self, |
|
document: &DocumentMessageHandler, |
|
input: &InputPreprocessorMessageHandler, |
|
responses: &mut VecDeque<Message>, |
|
tool_options: &PenOptions, |
|
append: bool, |
|
preferences: &PreferencesMessageHandler, |
|
shape_editor: &mut ShapeState, |
|
) { |
|
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); |
|
self.handle_type = TargetHandle::FuturePreviewOutHandle; |
|
|
|
let selected_nodes = document.network_interface.selected_nodes(); |
|
self.handle_end = None; |
|
|
|
let tolerance = crate::consts::SNAP_POINT_TOLERANCE; |
|
let extension_choice = should_extend(document, viewport, tolerance, selected_nodes.selected_layers(document.metadata()), preferences); |
|
if let Some((layer, point, position)) = extension_choice { |
|
self.current_layer = Some(layer); |
|
self.extend_existing_path(document, layer, point, position); |
|
return; |
|
} else if preferences.vector_meshes { |
|
if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, viewport, tolerance) { |
|
let (point, segments) = closest_segment.adjusted_insert(responses); |
|
let layer = closest_segment.layer(); |
|
let position = closest_segment.closest_point_document(); |
|
|
|
|
|
self.prior_segment_endpoint = Some(point); |
|
self.prior_segment_layer = Some(layer); |
|
self.prior_segments = Some(segments.to_vec()); |
|
|
|
self.extend_existing_path(document, layer, point, position); |
|
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.switch_to_free_on_ctrl_release = true; |
|
} |
|
} |
|
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 { |
|
|
|
responses.add(PenToolMessage::AddPointLayerPosition { layer, viewport }); |
|
return; |
|
} |
|
} |
|
|
|
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); |
|
self.handle_mode = HandleMode::Free; |
|
if self.modifiers.lock_angle { |
|
self.set_lock_angle(&vector_data, point, segment); |
|
self.switch_to_free_on_ctrl_release = true; |
|
} |
|
} |
|
|
|
|
|
let node_type = resolve_document_node_type("Path").expect("Path node does not exist"); |
|
let nodes = vec![(NodeId(0), node_type.default_node_template())]; |
|
|
|
let parent = document.new_layer_bounding_artboard(input); |
|
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses); |
|
self.current_layer = Some(layer); |
|
tool_options.fill.apply_fill(layer, responses); |
|
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses); |
|
self.prior_segment = None; |
|
self.prior_segments = None; |
|
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }); |
|
|
|
|
|
responses.add(Message::StartBuffer); |
|
|
|
responses.add(PenToolMessage::AddPointLayerPosition { layer, viewport }); |
|
} |
|
|
|
|
|
fn extend_existing_path(&mut self, document: &DocumentMessageHandler, layer: LayerNodeIdentifier, point: PointId, position: DVec2) { |
|
let vector_data = document.network_interface.compute_modified_vector(layer); |
|
let (handle_start, in_segment) = if let Some(vector_data) = &vector_data { |
|
vector_data |
|
.segment_bezier_iter() |
|
.find_map(|(segment_id, bezier, start, end)| { |
|
let is_end = point == end; |
|
let is_start = point == start; |
|
if !is_end && !is_start { |
|
return None; |
|
} |
|
|
|
let handle = match bezier.handles { |
|
BezierHandles::Cubic { handle_start, handle_end, .. } => { |
|
if is_start { |
|
handle_start |
|
} else { |
|
handle_end |
|
} |
|
} |
|
BezierHandles::Quadratic { handle } => handle, |
|
_ => return None, |
|
}; |
|
Some((segment_id, is_end, handle)) |
|
}) |
|
.map(|(segment_id, is_end, handle)| { |
|
let mirrored_handle = position * 2. - handle; |
|
let in_segment = if is_end { Some(segment_id) } else { None }; |
|
(mirrored_handle, in_segment) |
|
}) |
|
.unwrap_or_else(|| (position, None)) |
|
} else { |
|
(position, None) |
|
}; |
|
|
|
let in_segment = if self.modifiers.lock_angle { self.prior_segment } else { in_segment }; |
|
|
|
self.add_point(LastPoint { |
|
id: point, |
|
pos: position, |
|
in_segment, |
|
handle_start, |
|
}); |
|
|
|
self.next_point = position; |
|
self.next_handle_start = handle_start; |
|
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.handle_mode = HandleMode::Free; |
|
|
|
if self.modifiers.lock_angle { |
|
self.set_lock_angle(&vector_data, point, segment); |
|
self.switch_to_free_on_ctrl_release = true; |
|
} |
|
} |
|
|
|
|
|
fn store_clicked_endpoint(&mut self, document: &DocumentMessageHandler, transform: &DAffine2, input: &InputPreprocessorMessageHandler, preferences: &PreferencesMessageHandler) { |
|
let mut manipulators = HashMap::with_hasher(NoHashBuilder); |
|
let mut unselected = Vec::new(); |
|
let mut layer_manipulators = HashSet::with_hasher(NoHashBuilder); |
|
|
|
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; |
|
self.prior_segment = None; |
|
self.prior_segment_endpoint = None; |
|
self.prior_segment_layer = None; |
|
self.prior_segments = None; |
|
|
|
if let Some((layer, point, _position)) = closest_point(document, viewport, tolerance, document.metadata().all_layers(), |_| false, preferences) { |
|
self.prior_segment_endpoint = Some(point); |
|
self.prior_segment_layer = Some(layer); |
|
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.prior_segment = segment; |
|
layer_manipulators.insert(point); |
|
for (&id, &position) in vector_data.point_domain.ids().iter().zip(vector_data.point_domain.positions()) { |
|
if id == point { |
|
continue; |
|
} |
|
unselected.push(SnapCandidatePoint::handle(transform.transform_point2(position))) |
|
} |
|
manipulators.insert(layer, layer_manipulators); |
|
self.snap_cache = SnapCache { manipulators, unselected } |
|
} |
|
} |
|
|
|
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; |
|
}; |
|
|
|
match (self.handle_type, self.path_closed) { |
|
(TargetHandle::FuturePreviewOutHandle, _) | (TargetHandle::PreviewInHandle, true) => { |
|
if let Some(required_handle) = calculate_segment_angle(anchor, segment, vector_data, true) { |
|
self.angle = required_handle; |
|
self.handle_mode = HandleMode::ColinearEquidistant; |
|
} |
|
} |
|
(TargetHandle::PriorInHandle(..) | TargetHandle::PriorOutHandle(..), true) => { |
|
self.angle = -(self.handle_end.unwrap() - anchor_position).angle_to(DVec2::X); |
|
self.handle_mode = HandleMode::ColinearEquidistant; |
|
} |
|
_ => { |
|
self.angle = -(self.next_handle_start - 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) { |
|
|
|
let id = PointId::generate(); |
|
let pos = document.metadata().transform_to_viewport(layer).inverse().transform_point2(viewport); |
|
let modification_type = VectorModificationType::InsertPoint { id, position: pos }; |
|
responses.add(GraphOperationMessage::Vector { layer, modification_type }); |
|
self.add_point(LastPoint { |
|
id, |
|
pos, |
|
in_segment: None, |
|
handle_start: pos, |
|
}); |
|
self.next_point = pos; |
|
self.next_handle_start = pos; |
|
self.handle_end = None; |
|
} |
|
} |
|
|
|
impl Fsm for PenToolFsmState { |
|
type ToolData = PenToolData; |
|
type ToolOptions = PenOptions; |
|
|
|
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque<Message>) -> Self { |
|
let ToolActionHandlerData { |
|
document, |
|
global_tool_data, |
|
input, |
|
shape_editor, |
|
preferences, |
|
.. |
|
} = tool_action_data; |
|
let selected_nodes = document.network_interface.selected_nodes(); |
|
let mut selected_layers = selected_nodes.selected_layers(document.metadata()); |
|
let layer = selected_layers.next().filter(|_| selected_layers.next().is_none()).or(tool_data.current_layer); |
|
|
|
let mut transform = layer.map(|layer| document.metadata().transform_to_document(layer)).unwrap_or_default(); |
|
|
|
if !transform.inverse().is_finite() { |
|
let parent_transform = layer.and_then(|layer| layer.parent(document.metadata())).map(|layer| document.metadata().transform_to_document(layer)); |
|
|
|
transform = parent_transform.unwrap_or(DAffine2::IDENTITY); |
|
} |
|
|
|
if !transform.inverse().is_finite() { |
|
transform = DAffine2::IDENTITY; |
|
} |
|
|
|
let ToolMessage::Pen(event) = event else { return self }; |
|
match (self, event) { |
|
(PenToolFsmState::PlacingAnchor | PenToolFsmState::GRSHandle, PenToolMessage::GRS { grab, rotate, scale }) => { |
|
let Some(layer) = layer else { return PenToolFsmState::PlacingAnchor }; |
|
|
|
let Some(latest) = tool_data.latest_point() else { return PenToolFsmState::PlacingAnchor }; |
|
if latest.handle_start == latest.pos { |
|
return PenToolFsmState::PlacingAnchor; |
|
} |
|
|
|
let latest_pos = latest.pos; |
|
let latest_handle_start = latest.handle_start; |
|
|
|
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 vector_data = document.network_interface.compute_modified_vector(layer).unwrap(); |
|
tool_data.previous_handle_start_pos = latest.handle_start; |
|
let opposite_handle = tool_data.check_grs_end_handle(&vector_data); |
|
tool_data.previous_handle_end_pos = tool_data.target_handle_position(opposite_handle, &vector_data); |
|
let handle1 = latest_handle_start - latest_pos; |
|
let Some(opposite_handle_pos) = tool_data.target_handle_position(opposite_handle, &vector_data) else { |
|
return PenToolFsmState::GRSHandle; |
|
}; |
|
let handle2 = opposite_handle_pos - latest_pos; |
|
let pi = std::f64::consts::PI; |
|
let angle = handle1.angle_to(handle2); |
|
tool_data.colinear = (angle - pi).abs() < 1e-6 || (angle + pi).abs() < 1e-6; |
|
PenToolFsmState::GRSHandle |
|
} |
|
(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_position); |
|
latest_pt.handle_start = final_pos; |
|
} |
|
|
|
responses.add(OverlaysMessage::Draw); |
|
let Some(latest) = tool_data.latest_point() else { |
|
return PenToolFsmState::GRSHandle; |
|
}; |
|
let opposite_handle = tool_data.check_grs_end_handle(&vector_data); |
|
let Some(opposite_handle_pos) = tool_data.target_handle_position(opposite_handle, &vector_data) else { |
|
return PenToolFsmState::GRSHandle; |
|
}; |
|
|
|
if tool_data.colinear { |
|
let Some(direction) = (latest.pos - latest.handle_start).try_normalize() else { |
|
return PenToolFsmState::GRSHandle; |
|
}; |
|
|
|
if (latest.pos - latest.handle_start).length_squared() < f64::EPSILON { |
|
return PenToolFsmState::GRSHandle; |
|
} |
|
let relative_distance = (opposite_handle_pos - latest.pos).length(); |
|
let relative_position = relative_distance * direction + latest.pos; |
|
tool_data.update_target_handle_pos(opposite_handle, latest.pos, responses, relative_position, layer); |
|
} |
|
|
|
responses.add(OverlaysMessage::Draw); |
|
|
|
PenToolFsmState::GRSHandle |
|
} |
|
(PenToolFsmState::GRSHandle, PenToolMessage::Confirm) => { |
|
tool_data.next_point = input.mouse.position; |
|
tool_data.next_handle_start = input.mouse.position; |
|
|
|
responses.add(OverlaysMessage::Draw); |
|
responses.add(PenToolMessage::PointerMove { |
|
snap_angle: Key::Control, |
|
break_handle: Key::Alt, |
|
lock_angle: Key::Shift, |
|
colinear: Key::KeyC, |
|
move_anchor_with_handles: Key::Space, |
|
}); |
|
|
|
PenToolFsmState::PlacingAnchor |
|
} |
|
(PenToolFsmState::GRSHandle, PenToolMessage::Abort) => { |
|
tool_data.next_point = input.mouse.position; |
|
tool_data.next_handle_start = input.mouse.position; |
|
|
|
let Some(layer) = layer else { return PenToolFsmState::GRSHandle }; |
|
let vector_data = document.network_interface.compute_modified_vector(layer).unwrap(); |
|
let opposite_handle = tool_data.check_grs_end_handle(&vector_data); |
|
|
|
let previous = tool_data.previous_handle_start_pos; |
|
if let Some(latest) = tool_data.latest_point_mut() { |
|
latest.handle_start = previous; |
|
} else { |
|
return PenToolFsmState::PlacingAnchor; |
|
} |
|
|
|
responses.add(OverlaysMessage::Draw); |
|
responses.add(PenToolMessage::PointerMove { |
|
snap_angle: Key::Control, |
|
break_handle: Key::Alt, |
|
lock_angle: Key::Shift, |
|
colinear: Key::KeyC, |
|
move_anchor_with_handles: Key::Space, |
|
}); |
|
|
|
let Some((previous_pos, latest)) = tool_data.previous_handle_end_pos.zip(tool_data.latest_point()) else { |
|
return PenToolFsmState::PlacingAnchor; |
|
}; |
|
tool_data.update_target_handle_pos(opposite_handle, latest.pos, responses, previous_pos, layer); |
|
|
|
PenToolFsmState::PlacingAnchor |
|
} |
|
(_, PenToolMessage::SelectionChanged) => { |
|
responses.add(OverlaysMessage::Draw); |
|
self |
|
} |
|
(PenToolFsmState::Ready, PenToolMessage::Overlays(mut overlay_context)) => { |
|
match tool_options.pen_overlay_mode { |
|
PenOverlayMode::AllHandles => { |
|
path_overlays(document, DrawHandles::All, shape_editor, &mut overlay_context); |
|
} |
|
PenOverlayMode::FrontierHandles => { |
|
path_overlays(document, DrawHandles::None, shape_editor, &mut overlay_context); |
|
} |
|
} |
|
|
|
|
|
let tolerance = crate::consts::SNAP_POINT_TOLERANCE; |
|
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); |
|
|
|
let close_to_point = closest_point(document, viewport, tolerance, document.metadata().all_layers(), |_| false, preferences).is_some(); |
|
if preferences.vector_meshes && !close_to_point { |
|
if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, viewport, tolerance) { |
|
let pos = closest_segment.closest_point_to_viewport(); |
|
let perp = closest_segment.calculate_perp(document); |
|
overlay_context.manipulator_anchor(pos, true, None); |
|
overlay_context.line(pos - perp * SEGMENT_OVERLAY_SIZE, pos + perp * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), None); |
|
} |
|
} |
|
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); |
|
self |
|
} |
|
(_, PenToolMessage::Overlays(mut overlay_context)) => { |
|
let display_anchors = overlay_context.visibility_settings.anchors(); |
|
let display_handles = overlay_context.visibility_settings.handles(); |
|
|
|
let valid = |point: DVec2, handle: DVec2| point.distance_squared(handle) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE; |
|
|
|
let transform = document.metadata().document_to_viewport * transform; |
|
|
|
|
|
let next_anchor = transform.transform_point2(tool_data.next_point); |
|
|
|
let next_handle_start = transform.transform_point2(tool_data.next_handle_start); |
|
|
|
|
|
let anchor_start = tool_data.latest_point().map(|point| transform.transform_point2(point.pos)); |
|
|
|
let handle_end = tool_data.handle_end.map(|point| transform.transform_point2(point)); |
|
|
|
let handle_start = tool_data.latest_point().map(|point| transform.transform_point2(point.handle_start)); |
|
|
|
if let (Some((start, handle_start)), Some(handle_end)) = (tool_data.latest_point().map(|point| (point.pos, point.handle_start)), tool_data.handle_end) { |
|
let handles = BezierHandles::Cubic { handle_start, handle_end }; |
|
let end = tool_data.next_point; |
|
let bezier = Bezier { start, handles, end }; |
|
if (end - start).length_squared() > f64::EPSILON { |
|
|
|
overlay_context.outline_bezier(bezier, transform); |
|
} |
|
} |
|
|
|
if display_handles { |
|
|
|
overlay_context.line(next_anchor, next_handle_start, None, 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.prior_segment { |
|
path_overlays(document, DrawHandles::SelectedAnchors(vec![latest_segment]), shape_editor, &mut overlay_context); |
|
} |
|
|
|
else if let Some(segments) = tool_data.prior_segments.clone() { |
|
if preferences.vector_meshes { |
|
path_overlays(document, DrawHandles::SelectedAnchors(segments), 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) { |
|
if display_handles { |
|
|
|
overlay_context.line(anchor_start, handle_start, None, None); |
|
|
|
|
|
overlay_context.line(next_anchor, handle_end, None, None); |
|
} |
|
|
|
if self == PenToolFsmState::PlacingAnchor && anchor_start != handle_start && tool_data.modifiers.lock_angle { |
|
|
|
overlay_context.dashed_line(anchor_start, next_anchor, None, None, Some(4.), Some(4.), Some(0.5)); |
|
} |
|
|
|
|
|
if self == PenToolFsmState::PlacingAnchor && anchor_start != handle_start && tool_data.modifiers.snap_angle { |
|
overlay_context.dashed_line(anchor_start, next_anchor, None, None, Some(4.), Some(4.), Some(0.5)); |
|
} |
|
|
|
if self == PenToolFsmState::DraggingHandle(tool_data.handle_mode) && valid(next_anchor, handle_end) && display_handles { |
|
|
|
let selected = tool_data.handle_type == TargetHandle::PreviewInHandle; |
|
if display_handles { |
|
overlay_context.manipulator_handle(handle_end, selected, None); |
|
overlay_context.manipulator_handle(handle_end, selected, None); |
|
} |
|
} |
|
|
|
if valid(anchor_start, handle_start) && display_handles { |
|
|
|
overlay_context.manipulator_handle(handle_start, false, None); |
|
} |
|
} else { |
|
|
|
match tool_options.pen_overlay_mode { |
|
PenOverlayMode::AllHandles => { |
|
path_overlays(document, DrawHandles::All, shape_editor, &mut overlay_context); |
|
} |
|
PenOverlayMode::FrontierHandles => { |
|
path_overlays(document, DrawHandles::None, shape_editor, &mut overlay_context); |
|
} |
|
} |
|
} |
|
|
|
if self == PenToolFsmState::DraggingHandle(tool_data.handle_mode) && valid(next_anchor, next_handle_start) && display_handles { |
|
|
|
let selected = tool_data.handle_type == TargetHandle::FuturePreviewOutHandle; |
|
overlay_context.manipulator_handle(next_handle_start, selected, None); |
|
} |
|
|
|
if self == PenToolFsmState::DraggingHandle(tool_data.handle_mode) && display_anchors { |
|
|
|
overlay_context.manipulator_anchor(next_anchor, false, None); |
|
} |
|
|
|
if self == PenToolFsmState::PlacingAnchor && preferences.vector_meshes { |
|
let tolerance = crate::consts::SNAP_POINT_TOLERANCE; |
|
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); |
|
let close_to_point = closest_point(document, viewport, tolerance, document.metadata().all_layers(), |_| false, preferences).is_some(); |
|
if !close_to_point { |
|
if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, viewport, tolerance) { |
|
let pos = closest_segment.closest_point_to_viewport(); |
|
let perp = closest_segment.calculate_perp(document); |
|
overlay_context.manipulator_anchor(pos, true, None); |
|
overlay_context.line(pos - perp * SEGMENT_OVERLAY_SIZE, pos + perp * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), None); |
|
} |
|
} |
|
} |
|
|
|
|
|
if let Some(latest_point) = tool_data.latest_point() { |
|
let handle_start = latest_point.handle_start; |
|
let handle_end = tool_data.handle_end.unwrap_or(tool_data.next_handle_start); |
|
let next_point = tool_data.next_point; |
|
let start = latest_point.id; |
|
|
|
if let Some(layer) = layer { |
|
let mut vector_data = document.network_interface.compute_modified_vector(layer).unwrap(); |
|
|
|
let closest_point = vector_data.extendable_points(preferences.vector_meshes).filter(|&id| id != start).find(|&id| { |
|
vector_data.point_domain.position_from_id(id).map_or(false, |pos| { |
|
let dist_sq = transform.transform_point2(pos).distance_squared(transform.transform_point2(next_point)); |
|
dist_sq < crate::consts::SNAP_POINT_TOLERANCE.powi(2) |
|
}) |
|
}); |
|
|
|
|
|
if let Some(end) = closest_point { |
|
let segment_id = SegmentId::generate(); |
|
vector_data.push(segment_id, start, end, BezierHandles::Cubic { handle_start, handle_end }, StrokeId::ZERO); |
|
|
|
let grouped_segments = vector_data.auto_join_paths(); |
|
let closed_paths = grouped_segments.iter().filter(|path| path.is_closed() && path.contains(segment_id)); |
|
|
|
let subpaths: Vec<_> = closed_paths |
|
.filter_map(|path| { |
|
let segments = path.edges.iter().filter_map(|edge| { |
|
vector_data |
|
.segment_domain |
|
.iter() |
|
.find(|(id, _, _, _)| id == &edge.id) |
|
.map(|(_, start, end, bezier)| if start == edge.start { (bezier, start, end) } else { (bezier.reversed(), end, start) }) |
|
}); |
|
vector_data.subpath_from_segments_ignore_discontinuities(segments) |
|
}) |
|
.collect(); |
|
|
|
let mut fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()) |
|
.unwrap() |
|
.with_alpha(0.05) |
|
.to_rgba_hex_srgb(); |
|
fill_color.insert(0, '#'); |
|
overlay_context.fill_path(subpaths.iter(), transform, fill_color.as_str()); |
|
} |
|
} |
|
} |
|
|
|
|
|
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); |
|
|
|
self |
|
} |
|
(_, PenToolMessage::WorkingColorChanged) => { |
|
responses.add(PenToolMessage::UpdateOptions(PenOptionsUpdate::WorkingColors( |
|
Some(global_tool_data.primary_color), |
|
Some(global_tool_data.secondary_color), |
|
))); |
|
self |
|
} |
|
(PenToolFsmState::Ready, PenToolMessage::DragStart { append_to_selected }) => { |
|
responses.add(DocumentMessage::StartTransaction); |
|
tool_data.handle_mode = HandleMode::Free; |
|
|
|
|
|
let append = input.keyboard.key(append_to_selected); |
|
|
|
tool_data.store_clicked_endpoint(document, &transform, input, preferences); |
|
tool_data.create_initial_point(document, input, responses, tool_options, append, preferences, shape_editor); |
|
|
|
|
|
PenToolFsmState::DraggingHandle(tool_data.handle_mode) |
|
} |
|
(_, PenToolMessage::AddPointLayerPosition { layer, viewport }) => { |
|
tool_data.add_point_layer_position(document, responses, layer, viewport); |
|
|
|
self |
|
} |
|
(state, PenToolMessage::RecalculateLatestPointsPosition) => { |
|
tool_data.recalculate_latest_points_position(document); |
|
state |
|
} |
|
(PenToolFsmState::PlacingAnchor, PenToolMessage::DragStart { append_to_selected }) => { |
|
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); |
|
|
|
|
|
if tool_data.buffering_merged_vector { |
|
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, shape_editor, responses); |
|
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() { |
|
responses.add(DocumentMessage::StartTransaction); |
|
} |
|
|
|
|
|
|
|
let layers = LayerNodeIdentifier::ROOT_PARENT |
|
.descendants(document.metadata()) |
|
.filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[])); |
|
if let Some((other_layer, _, _)) = should_extend(document, viewport, crate::consts::SNAP_POINT_TOLERANCE, layers, preferences) { |
|
let selected_nodes = document.network_interface.selected_nodes(); |
|
let mut selected_layers = selected_nodes.selected_layers(document.metadata()); |
|
if let Some(current_layer) = selected_layers |
|
.next() |
|
.filter(|current_layer| selected_layers.next().is_none() && *current_layer != other_layer) |
|
.or(tool_data.current_layer.filter(|layer| *layer != other_layer)) |
|
{ |
|
merge_layers(document, current_layer, other_layer, responses); |
|
} |
|
} |
|
|
|
|
|
tool_data.buffering_merged_vector = true; |
|
responses.add(PenToolMessage::DragStart { append_to_selected }); |
|
PenToolFsmState::PlacingAnchor |
|
} |
|
} |
|
(PenToolFsmState::PlacingAnchor, PenToolMessage::RemovePreviousHandle) => { |
|
if let Some(last_point) = tool_data.latest_points.last_mut() { |
|
last_point.handle_start = last_point.pos; |
|
responses.add(OverlaysMessage::Draw); |
|
} else { |
|
log::trace!("No latest point available to modify handle_start."); |
|
} |
|
self |
|
} |
|
(PenToolFsmState::DraggingHandle(_), PenToolMessage::DragStop) => { |
|
tool_data.cleanup_target_selections(shape_editor, layer, document, responses); |
|
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, |
|
colinear, |
|
move_anchor_with_handles, |
|
}, |
|
) => { |
|
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), |
|
move_anchor_with_handles: input.keyboard.key(move_anchor_with_handles), |
|
}; |
|
|
|
let snap_data = SnapData::new(document, input); |
|
if tool_data.modifiers.colinear && !tool_data.toggle_colinear_debounce { |
|
tool_data.handle_mode = match tool_data.handle_mode { |
|
HandleMode::Free => { |
|
let last_segment = tool_data.prior_segment; |
|
if let Some(latest) = tool_data.latest_point_mut() { |
|
latest.in_segment = last_segment; |
|
} |
|
HandleMode::ColinearEquidistant |
|
} |
|
HandleMode::ColinearEquidistant | HandleMode::ColinearLocked => HandleMode::Free, |
|
}; |
|
tool_data.toggle_colinear_debounce = true; |
|
} |
|
|
|
let Some(vector_data) = layer.and_then(|layer| document.network_interface.compute_modified_vector(layer)) else { |
|
return self; |
|
}; |
|
|
|
if tool_data.modifiers.move_anchor_with_handles && !tool_data.space_pressed { |
|
let reference_handle = if tool_data.path_closed { |
|
TargetHandle::PreviewInHandle |
|
} else { |
|
TargetHandle::FuturePreviewOutHandle |
|
}; |
|
let handle_start = layer.map(|layer| { |
|
document |
|
.metadata() |
|
.transform_to_viewport(layer) |
|
.transform_point2(tool_data.target_handle_position(reference_handle, &vector_data).unwrap()) |
|
}); |
|
tool_data.handle_start_offset = handle_start.map(|start| start - input.mouse.position); |
|
tool_data.space_pressed = true; |
|
} |
|
|
|
if !tool_data.modifiers.move_anchor_with_handles { |
|
tool_data.space_pressed = false; |
|
} |
|
|
|
if !tool_data.modifiers.colinear { |
|
tool_data.toggle_colinear_debounce = false; |
|
} |
|
|
|
if !tool_data.modifiers.lock_angle { |
|
tool_data.angle_locked = false; |
|
} |
|
|
|
let state = tool_data |
|
.drag_handle(snap_data, transform, input.mouse.position, responses, layer, input) |
|
.unwrap_or(PenToolFsmState::Ready); |
|
|
|
if tool_data.handle_swapped { |
|
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::None }); |
|
} |
|
|
|
|
|
let messages = [ |
|
PenToolMessage::PointerOutsideViewport { |
|
snap_angle, |
|
break_handle, |
|
lock_angle, |
|
colinear, |
|
move_anchor_with_handles, |
|
} |
|
.into(), |
|
PenToolMessage::PointerMove { |
|
snap_angle, |
|
break_handle, |
|
lock_angle, |
|
colinear, |
|
move_anchor_with_handles, |
|
} |
|
.into(), |
|
]; |
|
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); |
|
|
|
state |
|
} |
|
( |
|
PenToolFsmState::PlacingAnchor, |
|
PenToolMessage::PointerMove { |
|
snap_angle, |
|
break_handle, |
|
lock_angle, |
|
colinear, |
|
move_anchor_with_handles, |
|
}, |
|
) => { |
|
tool_data.switch_to_free_on_ctrl_release = false; |
|
tool_data.alt_pressed = 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), |
|
move_anchor_with_handles: input.keyboard.key(move_anchor_with_handles), |
|
}; |
|
let state = tool_data |
|
.place_anchor(SnapData::new(document, input), transform, input.mouse.position, preferences, responses) |
|
.unwrap_or(PenToolFsmState::Ready); |
|
|
|
|
|
let messages = [ |
|
PenToolMessage::PointerOutsideViewport { |
|
snap_angle, |
|
break_handle, |
|
lock_angle, |
|
colinear, |
|
move_anchor_with_handles, |
|
} |
|
.into(), |
|
PenToolMessage::PointerMove { |
|
snap_angle, |
|
break_handle, |
|
lock_angle, |
|
colinear, |
|
move_anchor_with_handles, |
|
} |
|
.into(), |
|
]; |
|
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); |
|
|
|
state |
|
} |
|
(PenToolFsmState::DraggingHandle(_), PenToolMessage::SwapHandles) => { |
|
if !tool_data.handle_swapped { |
|
tool_data.handle_swapped = true |
|
} |
|
tool_data.swap_handles(layer, document, shape_editor, input, responses); |
|
responses.add(OverlaysMessage::Draw); |
|
self |
|
} |
|
( |
|
PenToolFsmState::Ready, |
|
PenToolMessage::PointerMove { |
|
snap_angle, |
|
break_handle, |
|
lock_angle, |
|
colinear, |
|
move_anchor_with_handles, |
|
}, |
|
) => { |
|
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), |
|
move_anchor_with_handles: input.keyboard.key(move_anchor_with_handles), |
|
}; |
|
tool_data.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position); |
|
responses.add(OverlaysMessage::Draw); |
|
self |
|
} |
|
(PenToolFsmState::DraggingHandle(mode), PenToolMessage::PointerOutsideViewport { .. }) => { |
|
|
|
let _ = tool_data.auto_panning.shift_viewport(input, responses); |
|
|
|
PenToolFsmState::DraggingHandle(mode) |
|
} |
|
(PenToolFsmState::PlacingAnchor, PenToolMessage::PointerOutsideViewport { .. }) => { |
|
if !input.mouse.mouse_keys.contains(MouseKeys::LEFT) { |
|
return self; |
|
} |
|
|
|
let _ = tool_data.auto_panning.shift_viewport(input, responses); |
|
|
|
PenToolFsmState::PlacingAnchor |
|
} |
|
( |
|
state, |
|
PenToolMessage::PointerOutsideViewport { |
|
snap_angle, |
|
break_handle, |
|
lock_angle, |
|
colinear, |
|
move_anchor_with_handles, |
|
}, |
|
) => { |
|
|
|
let messages = [ |
|
PenToolMessage::PointerOutsideViewport { |
|
snap_angle, |
|
break_handle, |
|
lock_angle, |
|
colinear, |
|
move_anchor_with_handles, |
|
} |
|
.into(), |
|
PenToolMessage::PointerMove { |
|
snap_angle, |
|
break_handle, |
|
lock_angle, |
|
colinear, |
|
move_anchor_with_handles, |
|
} |
|
.into(), |
|
]; |
|
tool_data.auto_panning.stop(&messages, responses); |
|
|
|
state |
|
} |
|
(PenToolFsmState::DraggingHandle(..), PenToolMessage::Confirm) => { |
|
|
|
if let Some((vector_data, layer)) = layer.and_then(|layer| document.network_interface.compute_modified_vector(layer)).zip(layer) { |
|
let single_point_in_layer = vector_data.point_domain.ids().len() == 1; |
|
tool_data.finish_placing_handle(SnapData::new(document, input), transform, preferences, responses); |
|
let latest_points = tool_data.latest_points.len() == 1; |
|
|
|
if latest_points && single_point_in_layer { |
|
responses.add(NodeGraphMessage::DeleteNodes { |
|
node_ids: vec![layer.to_node()], |
|
delete_children: true, |
|
}); |
|
responses.add(NodeGraphMessage::RunDocumentGraph); |
|
} else if (latest_points && tool_data.prior_segment_endpoint.is_none()) |
|
|| (tool_data.prior_segment_endpoint.is_some() && tool_data.prior_segment_layer != Some(layer) && latest_points) |
|
{ |
|
let vector_modification = VectorModificationType::RemovePoint { |
|
id: tool_data.latest_point().unwrap().id, |
|
}; |
|
responses.add(GraphOperationMessage::Vector { |
|
layer, |
|
modification_type: vector_modification, |
|
}); |
|
responses.add(PenToolMessage::Abort); |
|
} else { |
|
responses.add(DocumentMessage::EndTransaction); |
|
} |
|
} |
|
tool_data.cleanup(responses); |
|
tool_data.cleanup_target_selections(shape_editor, layer, document, responses); |
|
|
|
responses.add(OverlaysMessage::Draw); |
|
|
|
PenToolFsmState::Ready |
|
} |
|
(PenToolFsmState::PlacingAnchor, PenToolMessage::Confirm) => { |
|
responses.add(DocumentMessage::EndTransaction); |
|
tool_data.cleanup(responses); |
|
tool_data.cleanup_target_selections(shape_editor, layer, document, responses); |
|
|
|
PenToolFsmState::Ready |
|
} |
|
(PenToolFsmState::DraggingHandle(..), PenToolMessage::Abort) => { |
|
responses.add(DocumentMessage::AbortTransaction); |
|
if tool_data.handle_end.is_none() { |
|
tool_data.cleanup(responses); |
|
tool_data.cleanup_target_selections(shape_editor, layer, document, responses); |
|
|
|
PenToolFsmState::Ready |
|
} else { |
|
tool_data |
|
.place_anchor(SnapData::new(document, input), transform, input.mouse.position, preferences, responses) |
|
.unwrap_or(PenToolFsmState::Ready) |
|
} |
|
} |
|
(PenToolFsmState::PlacingAnchor, PenToolMessage::Abort) => { |
|
let should_delete_layer = if let Some(vector_data) = layer.and_then(|layer| document.network_interface.compute_modified_vector(layer)) { |
|
vector_data.point_domain.ids().len() == 1 |
|
} else { |
|
false |
|
}; |
|
|
|
responses.add(DocumentMessage::AbortTransaction); |
|
tool_data.cleanup(responses); |
|
tool_data.cleanup_target_selections(shape_editor, layer, document, responses); |
|
|
|
if should_delete_layer { |
|
responses.add(NodeGraphMessage::DeleteNodes { |
|
node_ids: vec![layer.unwrap().to_node()], |
|
delete_children: true, |
|
}); |
|
responses.add(NodeGraphMessage::RunDocumentGraph); |
|
} |
|
responses.add(OverlaysMessage::Draw); |
|
|
|
PenToolFsmState::Ready |
|
} |
|
(_, PenToolMessage::Abort) => PenToolFsmState::Ready, |
|
(PenToolFsmState::DraggingHandle(..) | PenToolFsmState::PlacingAnchor, PenToolMessage::Undo) => { |
|
if tool_data.point_index > 0 { |
|
tool_data.point_index -= 1; |
|
tool_data |
|
.place_anchor(SnapData::new(document, input), transform, input.mouse.position, preferences, responses) |
|
.unwrap_or(PenToolFsmState::PlacingAnchor) |
|
} else { |
|
responses.add(PenToolMessage::Abort); |
|
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), transform, input.mouse.position, preferences, responses); |
|
match tool_data.point_index { |
|
0 => PenToolFsmState::Ready, |
|
_ => PenToolFsmState::PlacingAnchor, |
|
} |
|
} |
|
_ => self, |
|
} |
|
} |
|
|
|
fn update_hints(&self, responses: &mut VecDeque<Message>) { |
|
let hint_data = match self { |
|
PenToolFsmState::Ready | PenToolFsmState::GRSHandle => HintData(vec![HintGroup(vec![ |
|
HintInfo::mouse(MouseMotion::Lmb, "Draw Path"), |
|
|
|
HintInfo::keys([Key::Shift], "Append to Selected Layer").prepend_plus(), |
|
])]), |
|
PenToolFsmState::PlacingAnchor => HintData(vec![ |
|
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], "15° Increments"), HintInfo::keys([Key::Control], "Lock Angle")]), |
|
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(), |
|
HintInfo::keys([Key::Control], "Lock Angle").prepend_plus(), |
|
]), |
|
]), |
|
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], "Cancel Segment").prepend_slash(), |
|
HintInfo::keys([Key::Enter], "End Path"), |
|
])); |
|
|
|
let mut 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")] |
|
} |
|
}; |
|
toggle_group.push(HintInfo::keys([Key::Tab], "Swap Dragged Handle")); |
|
|
|
let mut common_hints = vec![HintInfo::keys([Key::Shift], "15° Increments"), HintInfo::keys([Key::Control], "Lock Angle")]; |
|
let mut 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 |
|
} |
|
}; |
|
hold_group.push(HintInfo::keys([Key::Space], "Drag Anchor")); |
|
|
|
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 }); |
|
} |
|
|
|
fn update_cursor(&self, responses: &mut VecDeque<Message>) { |
|
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); |
|
} |
|
} |
|
|