|
use super::select_tool::extend_lasso; |
|
use super::tool_prelude::*; |
|
use crate::consts::{ |
|
COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, DRAG_THRESHOLD, HANDLE_ROTATE_SNAP_ANGLE, SEGMENT_INSERTION_DISTANCE, |
|
SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD, SELECTION_TOLERANCE, |
|
}; |
|
use crate::messages::portfolio::document::overlays::utility_functions::{path_overlays, selected_segments}; |
|
use crate::messages::portfolio::document::overlays::utility_types::{DrawHandles, OverlayContext}; |
|
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; |
|
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface; |
|
use crate::messages::portfolio::document::utility_types::transformation::Axis; |
|
use crate::messages::preferences::SelectionMode; |
|
use crate::messages::tool::common_functionality::auto_panning::AutoPanning; |
|
use crate::messages::tool::common_functionality::shape_editor::{ |
|
ClosestSegment, ManipulatorAngle, OpposingHandleLengths, SelectedPointsInfo, SelectionChange, SelectionShape, SelectionShapeType, ShapeState, |
|
}; |
|
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager}; |
|
use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, find_two_param_best_approximate}; |
|
use bezier_rs::{Bezier, TValue}; |
|
use graphene_std::renderer::Quad; |
|
use graphene_std::vector::{HandleExt, HandleId, NoHashBuilder, SegmentId, VectorData}; |
|
use graphene_std::vector::{ManipulatorPointId, PointId, VectorModificationType}; |
|
use std::vec; |
|
|
|
#[derive(Default)] |
|
pub struct PathTool { |
|
fsm_state: PathToolFsmState, |
|
tool_data: PathToolData, |
|
options: PathToolOptions, |
|
} |
|
|
|
#[derive(Default)] |
|
pub struct PathToolOptions { |
|
path_overlay_mode: PathOverlayMode, |
|
path_editing_mode: PathEditingMode, |
|
} |
|
|
|
#[impl_message(Message, ToolMessage, Path)] |
|
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] |
|
pub enum PathToolMessage { |
|
|
|
Abort, |
|
Overlays(OverlayContext), |
|
SelectionChanged, |
|
|
|
|
|
BreakPath, |
|
DeselectAllPoints, |
|
Delete, |
|
DeleteAndBreakPath, |
|
DragStop { |
|
extend_selection: Key, |
|
shrink_selection: Key, |
|
}, |
|
Enter { |
|
extend_selection: Key, |
|
shrink_selection: Key, |
|
}, |
|
Escape, |
|
ClosePath, |
|
FlipSmoothSharp, |
|
GRS { |
|
|
|
key: Key, |
|
}, |
|
ManipulatorMakeHandlesFree, |
|
ManipulatorMakeHandlesColinear, |
|
MouseDown { |
|
extend_selection: Key, |
|
lasso_select: Key, |
|
handle_drag_from_anchor: Key, |
|
drag_restore_handle: Key, |
|
molding_in_segment_edit: Key, |
|
}, |
|
NudgeSelectedPoints { |
|
delta_x: f64, |
|
delta_y: f64, |
|
}, |
|
PointerMove { |
|
equidistant: Key, |
|
toggle_colinear: Key, |
|
move_anchor_with_handles: Key, |
|
snap_angle: Key, |
|
lock_angle: Key, |
|
delete_segment: Key, |
|
break_colinear_molding: Key, |
|
}, |
|
PointerOutsideViewport { |
|
equidistant: Key, |
|
toggle_colinear: Key, |
|
move_anchor_with_handles: Key, |
|
snap_angle: Key, |
|
lock_angle: Key, |
|
delete_segment: Key, |
|
break_colinear_molding: Key, |
|
}, |
|
RightClick, |
|
SelectAllAnchors, |
|
SelectedPointUpdated, |
|
SelectedPointXChanged { |
|
new_x: f64, |
|
}, |
|
SelectedPointYChanged { |
|
new_y: f64, |
|
}, |
|
SwapSelectedHandles, |
|
UpdateOptions(PathOptionsUpdate), |
|
UpdateSelectedPointsStatus { |
|
overlay_context: OverlayContext, |
|
}, |
|
} |
|
|
|
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, Default, serde::Serialize, serde::Deserialize, specta::Type)] |
|
pub enum PathOverlayMode { |
|
AllHandles = 0, |
|
#[default] |
|
SelectedPointHandles = 1, |
|
FrontierHandles = 2, |
|
} |
|
|
|
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)] |
|
pub struct PathEditingMode { |
|
point_editing_mode: bool, |
|
segment_editing_mode: bool, |
|
} |
|
|
|
impl Default for PathEditingMode { |
|
fn default() -> Self { |
|
Self { |
|
point_editing_mode: true, |
|
segment_editing_mode: false, |
|
} |
|
} |
|
} |
|
|
|
#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)] |
|
pub enum PathOptionsUpdate { |
|
OverlayModeType(PathOverlayMode), |
|
PointEditingMode { enabled: bool }, |
|
SegmentEditingMode { enabled: bool }, |
|
} |
|
|
|
impl ToolMetadata for PathTool { |
|
fn icon_name(&self) -> String { |
|
"VectorPathTool".into() |
|
} |
|
fn tooltip(&self) -> String { |
|
"Path Tool".into() |
|
} |
|
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType { |
|
ToolType::Path |
|
} |
|
} |
|
|
|
impl LayoutHolder for PathTool { |
|
fn layout(&self) -> Layout { |
|
let coordinates = self.tool_data.selection_status.as_one().as_ref().map(|point| point.coordinates); |
|
let (x, y) = coordinates.map(|point| (Some(point.x), Some(point.y))).unwrap_or((None, None)); |
|
|
|
let selection_status = &self.tool_data.selection_status; |
|
let manipulator_angle = selection_status.angle(); |
|
|
|
let x_location = NumberInput::new(x) |
|
.unit(" px") |
|
.label("X") |
|
.min_width(120) |
|
.disabled(x.is_none()) |
|
.min(-((1_u64 << f64::MANTISSA_DIGITS) as f64)) |
|
.max((1_u64 << f64::MANTISSA_DIGITS) as f64) |
|
.on_update(move |number_input: &NumberInput| { |
|
if let Some(new_x) = number_input.value.or(x) { |
|
PathToolMessage::SelectedPointXChanged { new_x }.into() |
|
} else { |
|
Message::NoOp |
|
} |
|
}) |
|
.widget_holder(); |
|
|
|
let y_location = NumberInput::new(y) |
|
.unit(" px") |
|
.label("Y") |
|
.min_width(120) |
|
.disabled(y.is_none()) |
|
.min(-((1_u64 << f64::MANTISSA_DIGITS) as f64)) |
|
.max((1_u64 << f64::MANTISSA_DIGITS) as f64) |
|
.on_update(move |number_input: &NumberInput| { |
|
if let Some(new_y) = number_input.value.or(y) { |
|
PathToolMessage::SelectedPointYChanged { new_y }.into() |
|
} else { |
|
Message::NoOp |
|
} |
|
}) |
|
.widget_holder(); |
|
|
|
let related_seperator = Separator::new(SeparatorType::Related).widget_holder(); |
|
let unrelated_seperator = Separator::new(SeparatorType::Unrelated).widget_holder(); |
|
|
|
let colinear_handles_tooltip = "Keep both handles unbent, each 180° apart, when moving either"; |
|
let colinear_handles_state = manipulator_angle.and_then(|angle| match angle { |
|
ManipulatorAngle::Colinear => Some(true), |
|
ManipulatorAngle::Free => Some(false), |
|
ManipulatorAngle::Mixed => None, |
|
}) |
|
|
|
.unwrap_or_default(); |
|
let mut checkbox_id = CheckboxId::default(); |
|
let colinear_handle_checkbox = CheckboxInput::new(colinear_handles_state) |
|
.disabled(!self.tool_data.can_toggle_colinearity) |
|
.on_update(|&CheckboxInput { checked, .. }| { |
|
if checked { |
|
PathToolMessage::ManipulatorMakeHandlesColinear.into() |
|
} else { |
|
PathToolMessage::ManipulatorMakeHandlesFree.into() |
|
} |
|
}) |
|
.tooltip(colinear_handles_tooltip) |
|
.for_label(checkbox_id.clone()) |
|
.widget_holder(); |
|
let colinear_handles_label = TextLabel::new("Colinear Handles") |
|
.disabled(!self.tool_data.can_toggle_colinearity) |
|
.tooltip(colinear_handles_tooltip) |
|
.for_checkbox(&mut checkbox_id) |
|
.widget_holder(); |
|
|
|
let point_editing_mode = CheckboxInput::new(self.options.path_editing_mode.point_editing_mode) |
|
|
|
.icon("Dot") |
|
.tooltip("Point Editing Mode") |
|
.on_update(|input| PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: input.checked }).into()) |
|
.widget_holder(); |
|
let segment_editing_mode = CheckboxInput::new(self.options.path_editing_mode.segment_editing_mode) |
|
|
|
.icon("Remove") |
|
.tooltip("Segment Editing Mode") |
|
.on_update(|input| PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: input.checked }).into()) |
|
.widget_holder(); |
|
|
|
let path_overlay_mode_widget = RadioInput::new(vec![ |
|
RadioEntryData::new("all") |
|
.icon("HandleVisibilityAll") |
|
.tooltip("Show all handles regardless of selection") |
|
.on_update(move |_| PathToolMessage::UpdateOptions(PathOptionsUpdate::OverlayModeType(PathOverlayMode::AllHandles)).into()), |
|
RadioEntryData::new("selected") |
|
.icon("HandleVisibilitySelected") |
|
.tooltip("Show only handles of the segments connected to selected points") |
|
.on_update(move |_| PathToolMessage::UpdateOptions(PathOptionsUpdate::OverlayModeType(PathOverlayMode::SelectedPointHandles)).into()), |
|
RadioEntryData::new("frontier") |
|
.icon("HandleVisibilityFrontier") |
|
.tooltip("Show only handles at the frontiers of the segments connected to selected points") |
|
.on_update(move |_| PathToolMessage::UpdateOptions(PathOptionsUpdate::OverlayModeType(PathOverlayMode::FrontierHandles)).into()), |
|
]) |
|
.selected_index(Some(self.options.path_overlay_mode as u32)) |
|
.widget_holder(); |
|
|
|
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { |
|
widgets: vec![ |
|
x_location, |
|
related_seperator.clone(), |
|
y_location, |
|
unrelated_seperator.clone(), |
|
colinear_handle_checkbox, |
|
related_seperator.clone(), |
|
colinear_handles_label, |
|
unrelated_seperator.clone(), |
|
point_editing_mode, |
|
related_seperator.clone(), |
|
segment_editing_mode, |
|
unrelated_seperator, |
|
path_overlay_mode_widget, |
|
], |
|
}])) |
|
} |
|
} |
|
|
|
impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathTool { |
|
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, tool_data: &mut ToolActionHandlerData<'a>) { |
|
let updating_point = message == ToolMessage::Path(PathToolMessage::SelectedPointUpdated); |
|
|
|
match message { |
|
ToolMessage::Path(PathToolMessage::UpdateOptions(action)) => match action { |
|
PathOptionsUpdate::OverlayModeType(overlay_mode_type) => { |
|
self.options.path_overlay_mode = overlay_mode_type; |
|
responses.add(OverlaysMessage::Draw); |
|
} |
|
PathOptionsUpdate::PointEditingMode { enabled } => { |
|
self.options.path_editing_mode.point_editing_mode = enabled; |
|
responses.add(OverlaysMessage::Draw); |
|
} |
|
PathOptionsUpdate::SegmentEditingMode { enabled } => { |
|
self.options.path_editing_mode.segment_editing_mode = enabled; |
|
responses.add(OverlaysMessage::Draw); |
|
} |
|
}, |
|
ToolMessage::Path(PathToolMessage::ClosePath) => { |
|
responses.add(DocumentMessage::AddTransaction); |
|
tool_data.shape_editor.close_selected_path(tool_data.document, responses); |
|
responses.add(DocumentMessage::EndTransaction); |
|
responses.add(OverlaysMessage::Draw); |
|
} |
|
ToolMessage::Path(PathToolMessage::SwapSelectedHandles) => { |
|
if tool_data.shape_editor.handle_with_pair_selected(&tool_data.document.network_interface) { |
|
tool_data.shape_editor.alternate_selected_handles(&tool_data.document.network_interface); |
|
responses.add(PathToolMessage::SelectedPointUpdated); |
|
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::None }); |
|
responses.add(OverlaysMessage::Draw); |
|
} |
|
} |
|
_ => { |
|
self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &self.options, responses, true); |
|
} |
|
} |
|
|
|
if updating_point { |
|
self.send_layout(responses, LayoutTarget::ToolOptions); |
|
} |
|
} |
|
|
|
|
|
fn actions(&self) -> ActionList { |
|
match self.fsm_state { |
|
PathToolFsmState::Ready => actions!(PathToolMessageDiscriminant; |
|
FlipSmoothSharp, |
|
MouseDown, |
|
Delete, |
|
NudgeSelectedPoints, |
|
Enter, |
|
SelectAllAnchors, |
|
DeselectAllPoints, |
|
BreakPath, |
|
DeleteAndBreakPath, |
|
ClosePath, |
|
PointerMove, |
|
), |
|
PathToolFsmState::Dragging(_) => actions!(PathToolMessageDiscriminant; |
|
Escape, |
|
RightClick, |
|
FlipSmoothSharp, |
|
DragStop, |
|
PointerMove, |
|
Delete, |
|
BreakPath, |
|
DeleteAndBreakPath, |
|
SwapSelectedHandles, |
|
), |
|
PathToolFsmState::Drawing { .. } => actions!(PathToolMessageDiscriminant; |
|
FlipSmoothSharp, |
|
DragStop, |
|
PointerMove, |
|
Delete, |
|
Enter, |
|
BreakPath, |
|
DeleteAndBreakPath, |
|
Escape, |
|
RightClick, |
|
), |
|
PathToolFsmState::SlidingPoint => actions!(PathToolMessageDiscriminant; |
|
PointerMove, |
|
DragStop, |
|
Escape, |
|
RightClick |
|
), |
|
PathToolFsmState::MoldingSegment => actions!(PathToolMessageDiscriminant; |
|
PointerMove, |
|
DragStop, |
|
RightClick, |
|
Escape, |
|
), |
|
} |
|
} |
|
} |
|
|
|
impl ToolTransition for PathTool { |
|
fn event_to_message_map(&self) -> EventToMessageMap { |
|
EventToMessageMap { |
|
tool_abort: Some(PathToolMessage::Abort.into()), |
|
selection_changed: Some(PathToolMessage::SelectionChanged.into()), |
|
overlay_provider: Some(|overlay_context| PathToolMessage::Overlays(overlay_context).into()), |
|
..Default::default() |
|
} |
|
} |
|
} |
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] |
|
pub struct DraggingState { |
|
point_select_state: PointSelectState, |
|
colinear: ManipulatorAngle, |
|
} |
|
|
|
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)] |
|
pub enum PointSelectState { |
|
HandleWithPair, |
|
#[default] |
|
HandleNoPair, |
|
Anchor, |
|
} |
|
|
|
#[derive(Clone, Copy)] |
|
pub struct SlidingSegmentData { |
|
segment_id: SegmentId, |
|
bezier: Bezier, |
|
start: PointId, |
|
} |
|
|
|
#[derive(Clone, Copy)] |
|
pub struct SlidingPointInfo { |
|
anchor: PointId, |
|
layer: LayerNodeIdentifier, |
|
connected_segments: [SlidingSegmentData; 2], |
|
} |
|
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] |
|
enum PathToolFsmState { |
|
#[default] |
|
Ready, |
|
Dragging(DraggingState), |
|
Drawing { |
|
selection_shape: SelectionShapeType, |
|
}, |
|
SlidingPoint, |
|
MoldingSegment, |
|
} |
|
|
|
#[derive(Default)] |
|
struct PathToolData { |
|
snap_manager: SnapManager, |
|
lasso_polygon: Vec<DVec2>, |
|
selection_mode: Option<SelectionMode>, |
|
drag_start_pos: DVec2, |
|
previous_mouse_position: DVec2, |
|
toggle_colinear_debounce: bool, |
|
opposing_handle_lengths: Option<OpposingHandleLengths>, |
|
|
|
|
|
|
|
selection_status: SelectionStatus, |
|
|
|
can_toggle_colinearity: bool, |
|
segment: Option<ClosestSegment>, |
|
snap_cache: SnapCache, |
|
double_click_handled: bool, |
|
delete_segment_pressed: bool, |
|
auto_panning: AutoPanning, |
|
saved_points_before_anchor_select_toggle: Vec<ManipulatorPointId>, |
|
select_anchor_toggled: bool, |
|
saved_points_before_handle_drag: Vec<ManipulatorPointId>, |
|
handle_drag_toggle: bool, |
|
saved_points_before_anchor_convert_smooth_sharp: HashSet<ManipulatorPointId>, |
|
last_click_time: u64, |
|
dragging_state: DraggingState, |
|
angle: f64, |
|
opposite_handle_position: Option<DVec2>, |
|
last_clicked_point_was_selected: bool, |
|
last_clicked_segment_was_selected: bool, |
|
snapping_axis: Option<Axis>, |
|
alt_clicked_on_anchor: bool, |
|
alt_dragging_from_anchor: bool, |
|
angle_locked: bool, |
|
temporary_colinear_handles: bool, |
|
molding_info: Option<(DVec2, DVec2)>, |
|
molding_segment: bool, |
|
temporary_adjacent_handles_while_molding: Option<[Option<HandleId>; 2]>, |
|
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>, |
|
adjacent_anchor_offset: Option<DVec2>, |
|
sliding_point_info: Option<SlidingPointInfo>, |
|
started_drawing_from_inside: bool, |
|
} |
|
|
|
impl PathToolData { |
|
fn save_points_before_anchor_toggle(&mut self, points: Vec<ManipulatorPointId>) -> PathToolFsmState { |
|
self.saved_points_before_anchor_select_toggle = points; |
|
PathToolFsmState::Dragging(self.dragging_state) |
|
} |
|
|
|
fn remove_saved_points(&mut self) { |
|
self.saved_points_before_anchor_select_toggle.clear(); |
|
} |
|
|
|
pub fn selection_quad(&self, metadata: &DocumentMetadata) -> Quad { |
|
let bbox = self.selection_box(metadata); |
|
Quad::from_box(bbox) |
|
} |
|
|
|
pub fn calculate_selection_mode_from_direction(&mut self, metadata: &DocumentMetadata) -> SelectionMode { |
|
let bbox = self.selection_box(metadata); |
|
let above_threshold = bbox[1].distance_squared(bbox[0]) > DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD.powi(2); |
|
|
|
if self.selection_mode.is_none() && above_threshold { |
|
let mode = if bbox[1].x < bbox[0].x { |
|
SelectionMode::Touched |
|
} else { |
|
|
|
SelectionMode::Enclosed |
|
}; |
|
self.selection_mode = Some(mode); |
|
} |
|
|
|
self.selection_mode.unwrap_or(SelectionMode::Touched) |
|
} |
|
|
|
pub fn selection_box(&self, metadata: &DocumentMetadata) -> [DVec2; 2] { |
|
|
|
let document_to_viewport = metadata.document_to_viewport; |
|
let previous_mouse = document_to_viewport.transform_point2(self.previous_mouse_position); |
|
if previous_mouse == self.drag_start_pos { |
|
let tolerance = DVec2::splat(SELECTION_TOLERANCE); |
|
[self.drag_start_pos - tolerance, self.drag_start_pos + tolerance] |
|
} else { |
|
[self.drag_start_pos, previous_mouse] |
|
} |
|
} |
|
|
|
fn update_selection_status(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler) { |
|
let selection_status = get_selection_status(&document.network_interface, shape_editor); |
|
|
|
self.can_toggle_colinearity = match &selection_status { |
|
SelectionStatus::None => false, |
|
SelectionStatus::One(single_selected_point) => { |
|
let vector_data = document.network_interface.compute_modified_vector(single_selected_point.layer).unwrap(); |
|
single_selected_point.id.get_handle_pair(&vector_data).is_some() |
|
} |
|
SelectionStatus::Multiple(_) => true, |
|
}; |
|
self.selection_status = selection_status; |
|
} |
|
|
|
|
|
#[allow(clippy::too_many_arguments)] |
|
fn mouse_down( |
|
&mut self, |
|
shape_editor: &mut ShapeState, |
|
document: &DocumentMessageHandler, |
|
input: &InputPreprocessorMessageHandler, |
|
responses: &mut VecDeque<Message>, |
|
extend_selection: bool, |
|
lasso_select: bool, |
|
handle_drag_from_anchor: bool, |
|
drag_zero_handle: bool, |
|
molding_in_segment_edit: bool, |
|
path_overlay_mode: PathOverlayMode, |
|
segment_editing_mode: bool, |
|
point_editing_mode: bool, |
|
) -> PathToolFsmState { |
|
self.double_click_handled = false; |
|
self.opposing_handle_lengths = None; |
|
|
|
self.drag_start_pos = input.mouse.position; |
|
|
|
if !self.saved_points_before_anchor_convert_smooth_sharp.is_empty() && (input.time - self.last_click_time > 500) { |
|
self.saved_points_before_anchor_convert_smooth_sharp.clear(); |
|
} |
|
|
|
self.last_click_time = input.time; |
|
|
|
let old_selection = shape_editor.selected_points().cloned().collect::<Vec<_>>(); |
|
|
|
|
|
|
|
if let Some((already_selected, mut selection_info)) = shape_editor.get_point_selection_state( |
|
&document.network_interface, |
|
input.mouse.position, |
|
SELECTION_THRESHOLD, |
|
path_overlay_mode, |
|
self.frontier_handles_info.clone(), |
|
point_editing_mode, |
|
) { |
|
responses.add(DocumentMessage::StartTransaction); |
|
|
|
self.last_clicked_point_was_selected = already_selected; |
|
|
|
|
|
|
|
if !(already_selected && extend_selection) { |
|
if let Some(updated_selection_info) = shape_editor.change_point_selection( |
|
&document.network_interface, |
|
input.mouse.position, |
|
SELECTION_THRESHOLD, |
|
extend_selection, |
|
path_overlay_mode, |
|
self.frontier_handles_info.clone(), |
|
) { |
|
selection_info = updated_selection_info; |
|
} |
|
} |
|
|
|
if let Some(selected_points) = selection_info { |
|
self.drag_start_pos = input.mouse.position; |
|
|
|
|
|
let mut dragging_only_handles = true; |
|
for point in &selected_points.points { |
|
if matches!(point.point_id, ManipulatorPointId::Anchor(_)) { |
|
dragging_only_handles = false; |
|
break; |
|
} |
|
} |
|
if dragging_only_handles && !self.handle_drag_toggle && !old_selection.is_empty() { |
|
self.saved_points_before_handle_drag = old_selection; |
|
} |
|
|
|
if handle_drag_from_anchor { |
|
if let Some((layer, point)) = shape_editor.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD) { |
|
|
|
if let (Some(point_id), Some(vector_data)) = (point.as_anchor(), document.network_interface.compute_modified_vector(layer)) { |
|
let handles = vector_data.all_connected(point_id).collect::<Vec<_>>(); |
|
self.alt_clicked_on_anchor = true; |
|
for handle in &handles { |
|
let modification_type = handle.set_relative_position(DVec2::ZERO); |
|
responses.add(GraphOperationMessage::Vector { layer, modification_type }); |
|
for &handles in &vector_data.colinear_manipulators { |
|
if handles.contains(handle) { |
|
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false }; |
|
responses.add(GraphOperationMessage::Vector { layer, modification_type }); |
|
} |
|
} |
|
} |
|
|
|
let manipulator_point_id = handles[0].to_manipulator_point(); |
|
shape_editor.deselect_all_points(); |
|
shape_editor.select_points_by_manipulator_id(&vec![manipulator_point_id]); |
|
responses.add(PathToolMessage::SelectedPointUpdated); |
|
} |
|
} |
|
} |
|
|
|
if let Some((Some(point), Some(vector_data))) = shape_editor |
|
.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD) |
|
.map(|(layer, point)| (point.as_anchor(), document.network_interface.compute_modified_vector(layer))) |
|
{ |
|
let handles = vector_data |
|
.all_connected(point) |
|
.filter(|handle| handle.length(&vector_data) < 1e-6) |
|
.map(|handle| handle.to_manipulator_point()) |
|
.collect::<Vec<_>>(); |
|
let endpoint = vector_data.extendable_points(false).any(|anchor| point == anchor); |
|
|
|
if drag_zero_handle && (handles.len() == 1 && !endpoint) { |
|
shape_editor.deselect_all_points(); |
|
shape_editor.select_points_by_manipulator_id(&handles); |
|
shape_editor.convert_selected_manipulators_to_colinear_handles(responses, document); |
|
} |
|
} |
|
|
|
self.start_dragging_point(selected_points, input, document, shape_editor); |
|
responses.add(OverlaysMessage::Draw); |
|
} |
|
PathToolFsmState::Dragging(self.dragging_state) |
|
} |
|
|
|
else if let Some(segment) = shape_editor.upper_closest_segment(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD) { |
|
responses.add(DocumentMessage::StartTransaction); |
|
|
|
if segment_editing_mode && !molding_in_segment_edit { |
|
let layer = segment.layer(); |
|
let segment_id = segment.segment(); |
|
let already_selected = shape_editor.selected_shape_state.get(&layer).is_some_and(|state| state.is_segment_selected(segment_id)); |
|
self.last_clicked_segment_was_selected = already_selected; |
|
|
|
if !(already_selected && extend_selection) { |
|
let retain_existing_selection = extend_selection || already_selected; |
|
if !retain_existing_selection { |
|
shape_editor.deselect_all_segments(); |
|
shape_editor.deselect_all_points(); |
|
} |
|
|
|
|
|
if let Some(selected_shape_state) = shape_editor.selected_shape_state.get_mut(&layer) { |
|
selected_shape_state.select_segment(segment_id); |
|
} |
|
} |
|
|
|
self.drag_start_pos = input.mouse.position; |
|
|
|
let viewport_to_document = document.metadata().document_to_viewport.inverse(); |
|
self.previous_mouse_position = viewport_to_document.transform_point2(input.mouse.position); |
|
|
|
responses.add(OverlaysMessage::Draw); |
|
PathToolFsmState::Dragging(self.dragging_state) |
|
} else { |
|
let handle1 = ManipulatorPointId::PrimaryHandle(segment.segment()); |
|
let handle2 = ManipulatorPointId::EndHandle(segment.segment()); |
|
if let Some(vector_data) = document.network_interface.compute_modified_vector(segment.layer()) { |
|
if let (Some(pos1), Some(pos2)) = (handle1.get_position(&vector_data), handle2.get_position(&vector_data)) { |
|
self.molding_info = Some((pos1, pos2)) |
|
} |
|
} |
|
PathToolFsmState::MoldingSegment |
|
} |
|
} |
|
|
|
else if let Some(layer) = document.click(input) { |
|
shape_editor.deselect_all_points(); |
|
shape_editor.deselect_all_segments(); |
|
if extend_selection { |
|
responses.add(NodeGraphMessage::SelectedNodesAdd { nodes: vec![layer.to_node()] }); |
|
} else { |
|
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }); |
|
} |
|
self.drag_start_pos = input.mouse.position; |
|
self.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position); |
|
|
|
self.started_drawing_from_inside = true; |
|
|
|
let selection_shape = if lasso_select { SelectionShapeType::Lasso } else { SelectionShapeType::Box }; |
|
PathToolFsmState::Drawing { selection_shape } |
|
} |
|
|
|
else { |
|
self.drag_start_pos = input.mouse.position; |
|
self.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position); |
|
|
|
let selection_shape = if lasso_select { SelectionShapeType::Lasso } else { SelectionShapeType::Box }; |
|
PathToolFsmState::Drawing { selection_shape } |
|
} |
|
} |
|
|
|
fn start_dragging_point(&mut self, selected_points: SelectedPointsInfo, input: &InputPreprocessorMessageHandler, document: &DocumentMessageHandler, shape_editor: &mut ShapeState) { |
|
let mut manipulators = HashMap::with_hasher(NoHashBuilder); |
|
let mut unselected = Vec::new(); |
|
for (&layer, state) in &shape_editor.selected_shape_state { |
|
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { |
|
continue; |
|
}; |
|
let transform = document.metadata().transform_to_document(layer); |
|
|
|
let mut layer_manipulators = HashSet::with_hasher(NoHashBuilder); |
|
for point in state.selected_points() { |
|
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 }; |
|
|
|
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) { |
|
continue; |
|
} |
|
unselected.push(SnapCandidatePoint::handle(transform.transform_point2(position))) |
|
} |
|
if !layer_manipulators.is_empty() { |
|
manipulators.insert(layer, layer_manipulators); |
|
} |
|
} |
|
self.snap_cache = SnapCache { manipulators, unselected }; |
|
|
|
let viewport_to_document = document.metadata().document_to_viewport.inverse(); |
|
self.previous_mouse_position = viewport_to_document.transform_point2(input.mouse.position - selected_points.offset); |
|
} |
|
|
|
fn update_colinear(&mut self, equidistant: bool, toggle_colinear: bool, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) -> bool { |
|
|
|
let is_colinear = self |
|
.selection_status |
|
.angle() |
|
.map(|angle| match angle { |
|
ManipulatorAngle::Colinear => true, |
|
ManipulatorAngle::Free | ManipulatorAngle::Mixed => false, |
|
}) |
|
.unwrap_or(false); |
|
|
|
|
|
if toggle_colinear && !self.toggle_colinear_debounce { |
|
self.opposing_handle_lengths = None; |
|
if is_colinear { |
|
shape_editor.disable_colinear_handles_state_on_selected(&document.network_interface, responses); |
|
} else { |
|
shape_editor.convert_selected_manipulators_to_colinear_handles(responses, document); |
|
} |
|
self.toggle_colinear_debounce = true; |
|
return true; |
|
} |
|
self.toggle_colinear_debounce = toggle_colinear; |
|
|
|
if equidistant && self.opposing_handle_lengths.is_none() { |
|
if !is_colinear { |
|
|
|
let Some((_, _, selected_handle_id)) = self.try_get_selected_handle_and_anchor(shape_editor, document) else { |
|
self.opposing_handle_lengths = Some(shape_editor.opposing_handle_lengths(document)); |
|
return false; |
|
}; |
|
|
|
let Some((layer, _)) = shape_editor.selected_shape_state.iter().next() else { |
|
self.opposing_handle_lengths = Some(shape_editor.opposing_handle_lengths(document)); |
|
return false; |
|
}; |
|
|
|
let Some(vector_data) = document.network_interface.compute_modified_vector(*layer) else { |
|
self.opposing_handle_lengths = Some(shape_editor.opposing_handle_lengths(document)); |
|
return false; |
|
}; |
|
|
|
|
|
if let Some(handle_pair) = selected_handle_id.get_handle_pair(&vector_data) { |
|
let opposite_handle_length = handle_pair.iter().filter(|&&h| h.to_manipulator_point() != selected_handle_id).find_map(|&h| { |
|
let opp_handle_pos = h.to_manipulator_point().get_position(&vector_data)?; |
|
let opp_anchor_id = h.to_manipulator_point().get_anchor(&vector_data)?; |
|
let opp_anchor_pos = vector_data.point_domain.position_from_id(opp_anchor_id)?; |
|
Some((opp_handle_pos - opp_anchor_pos).length()) |
|
}); |
|
|
|
|
|
if opposite_handle_length == Some(0.) { |
|
shape_editor.convert_selected_manipulators_to_colinear_handles(responses, document); |
|
return true; |
|
} |
|
} |
|
} |
|
self.opposing_handle_lengths = Some(shape_editor.opposing_handle_lengths(document)); |
|
} |
|
false |
|
} |
|
|
|
|
|
fn try_get_selected_handle_and_anchor(&self, shape_editor: &ShapeState, document: &DocumentMessageHandler) -> Option<(DVec2, DVec2, ManipulatorPointId)> { |
|
|
|
let (layer, selection) = shape_editor.selected_shape_state.iter().next()?; |
|
|
|
|
|
if selection.selected_points_count() != 1 { |
|
return None; |
|
} |
|
|
|
|
|
let selected_handle = selection.selected_points().next()?.as_handle()?; |
|
let handle_id = selected_handle.to_manipulator_point(); |
|
|
|
let layer_to_document = document.metadata().transform_to_document(*layer); |
|
let vector_data = document.network_interface.compute_modified_vector(*layer)?; |
|
|
|
let handle_position_local = selected_handle.to_manipulator_point().get_position(&vector_data)?; |
|
let anchor_id = selected_handle.to_manipulator_point().get_anchor(&vector_data)?; |
|
let anchor_position_local = vector_data.point_domain.position_from_id(anchor_id)?; |
|
|
|
let handle_position_document = layer_to_document.transform_point2(handle_position_local); |
|
let anchor_position_document = layer_to_document.transform_point2(anchor_position_local); |
|
|
|
Some((handle_position_document, anchor_position_document, handle_id)) |
|
} |
|
|
|
#[allow(clippy::too_many_arguments)] |
|
fn calculate_handle_angle( |
|
&mut self, |
|
shape_editor: &mut ShapeState, |
|
document: &DocumentMessageHandler, |
|
responses: &mut VecDeque<Message>, |
|
relative_vector: DVec2, |
|
handle_vector: DVec2, |
|
handle_id: ManipulatorPointId, |
|
lock_angle: bool, |
|
snap_angle: bool, |
|
tangent_to_neighboring_tangents: bool, |
|
) -> f64 { |
|
let current_angle = -handle_vector.angle_to(DVec2::X); |
|
|
|
if let Some((vector_data, layer)) = shape_editor |
|
.selected_shape_state |
|
.iter() |
|
.next() |
|
.and_then(|(layer, _)| document.network_interface.compute_modified_vector(*layer).map(|vector_data| (vector_data, layer))) |
|
{ |
|
let adjacent_anchor = check_handle_over_adjacent_anchor(handle_id, &vector_data); |
|
let mut required_angle = None; |
|
|
|
|
|
if adjacent_anchor.is_some() && lock_angle && !self.angle_locked { |
|
let anchor = handle_id.get_anchor(&vector_data); |
|
let (angle, anchor_position) = calculate_adjacent_anchor_tangent(handle_id, anchor, adjacent_anchor, &vector_data); |
|
|
|
let layer_to_document = document.metadata().transform_to_document(*layer); |
|
|
|
self.adjacent_anchor_offset = handle_id |
|
.get_anchor_position(&vector_data) |
|
.and_then(|handle_anchor| anchor_position.map(|adjacent_anchor| layer_to_document.transform_point2(adjacent_anchor) - layer_to_document.transform_point2(handle_anchor))); |
|
|
|
required_angle = angle; |
|
} |
|
|
|
|
|
if relative_vector.length() < 25. && lock_angle && !self.angle_locked { |
|
required_angle = calculate_lock_angle(self, shape_editor, responses, document, &vector_data, handle_id, tangent_to_neighboring_tangents); |
|
} |
|
|
|
|
|
if let Some(angle) = required_angle { |
|
self.angle = angle; |
|
self.angle_locked = true; |
|
return angle; |
|
} |
|
} |
|
|
|
if lock_angle && !self.angle_locked { |
|
self.angle_locked = true; |
|
self.angle = -relative_vector.angle_to(DVec2::X); |
|
return -relative_vector.angle_to(DVec2::X); |
|
} |
|
|
|
|
|
if self.angle_locked { |
|
return self.angle; |
|
} |
|
|
|
|
|
let mut handle_angle = current_angle; |
|
if snap_angle && !lock_angle { |
|
let snap_resolution = HANDLE_ROTATE_SNAP_ANGLE.to_radians(); |
|
handle_angle = (handle_angle / snap_resolution).round() * snap_resolution; |
|
} |
|
|
|
self.angle = handle_angle; |
|
|
|
handle_angle |
|
} |
|
|
|
#[allow(clippy::too_many_arguments)] |
|
fn apply_snapping( |
|
&mut self, |
|
handle_direction: DVec2, |
|
new_handle_position: DVec2, |
|
anchor_position: DVec2, |
|
using_angle_constraints: bool, |
|
handle_position: DVec2, |
|
document: &DocumentMessageHandler, |
|
input: &InputPreprocessorMessageHandler, |
|
) -> DVec2 { |
|
let snap_data = SnapData::new(document, input); |
|
let snap_point = SnapCandidatePoint::handle_neighbors(new_handle_position, [anchor_position]); |
|
|
|
let snap_result = match using_angle_constraints { |
|
true => { |
|
let snap_constraint = SnapConstraint::Line { |
|
origin: anchor_position, |
|
direction: handle_direction.normalize_or_zero(), |
|
}; |
|
|
|
self.snap_manager.constrained_snap(&snap_data, &snap_point, snap_constraint, Default::default()) |
|
} |
|
false => self.snap_manager.free_snap(&snap_data, &snap_point, Default::default()), |
|
}; |
|
|
|
self.snap_manager.update_indicator(snap_result.clone()); |
|
|
|
document.metadata().document_to_viewport.transform_vector2(snap_result.snapped_point_document - handle_position) |
|
} |
|
|
|
fn start_snap_along_axis(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) { |
|
|
|
let current_mouse = input.mouse.position; |
|
let drag_start = self.drag_start_pos; |
|
let opposite_delta = drag_start - current_mouse; |
|
|
|
shape_editor.move_selected_points_and_segments(None, document, opposite_delta, false, true, false, None, false, responses); |
|
|
|
|
|
let delta = current_mouse - drag_start; |
|
let axis = if delta.x.abs() >= delta.y.abs() { Axis::X } else { Axis::Y }; |
|
self.snapping_axis = Some(axis); |
|
let projected_delta = match axis { |
|
Axis::X => DVec2::new(delta.x, 0.), |
|
Axis::Y => DVec2::new(0., delta.y), |
|
_ => DVec2::new(delta.x, 0.), |
|
}; |
|
|
|
shape_editor.move_selected_points_and_segments(None, document, projected_delta, false, true, false, None, false, responses); |
|
} |
|
|
|
fn stop_snap_along_axis(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) { |
|
|
|
let current_mouse = input.mouse.position; |
|
let drag_start = self.drag_start_pos; |
|
|
|
let opposite_delta = drag_start - current_mouse; |
|
let Some(axis) = self.snapping_axis else { return }; |
|
let opposite_projected_delta = match axis { |
|
Axis::X => DVec2::new(opposite_delta.x, 0.), |
|
Axis::Y => DVec2::new(0., opposite_delta.y), |
|
_ => DVec2::new(opposite_delta.x, 0.), |
|
}; |
|
|
|
shape_editor.move_selected_points_and_segments(None, document, opposite_projected_delta, false, true, false, None, false, responses); |
|
|
|
|
|
let delta = current_mouse - drag_start; |
|
|
|
shape_editor.move_selected_points_and_segments(None, document, delta, false, true, false, None, false, responses); |
|
|
|
self.snapping_axis = None; |
|
} |
|
|
|
fn get_normalized_tangent(&mut self, point: PointId, segment: SegmentId, vector_data: &VectorData) -> Option<DVec2> { |
|
let other_point = vector_data.other_point(segment, point)?; |
|
let position = ManipulatorPointId::Anchor(point).get_position(vector_data)?; |
|
|
|
let mut handles = vector_data.all_connected(other_point); |
|
let other_handle = handles.find(|handle| handle.segment == segment)?; |
|
|
|
let target_position = if other_handle.length(vector_data) == 0. { |
|
ManipulatorPointId::Anchor(other_point).get_position(vector_data)? |
|
} else { |
|
other_handle.to_manipulator_point().get_position(vector_data)? |
|
}; |
|
|
|
let tangent_vector = target_position - position; |
|
tangent_vector.try_normalize() |
|
} |
|
|
|
fn update_closest_segment(&mut self, shape_editor: &mut ShapeState, position: DVec2, document: &DocumentMessageHandler, path_overlay_mode: PathOverlayMode) { |
|
|
|
if shape_editor |
|
.find_nearest_visible_point_indices(&document.network_interface, position, SELECTION_THRESHOLD, path_overlay_mode, self.frontier_handles_info.clone()) |
|
.is_some() |
|
{ |
|
self.segment = None; |
|
} |
|
|
|
else if let Some(closest_segment) = &mut self.segment { |
|
closest_segment.update_closest_point(document.metadata(), position); |
|
|
|
if closest_segment.too_far(position, SEGMENT_INSERTION_DISTANCE) { |
|
self.segment = None; |
|
} |
|
} |
|
|
|
else if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, position, SEGMENT_INSERTION_DISTANCE) { |
|
self.segment = Some(closest_segment); |
|
} |
|
} |
|
|
|
fn start_sliding_point(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler) -> bool { |
|
let single_anchor_selected = shape_editor.selected_points().count() == 1 && shape_editor.selected_points().any(|point| matches!(point, ManipulatorPointId::Anchor(_))); |
|
|
|
if single_anchor_selected { |
|
let Some(anchor) = shape_editor.selected_points().next() else { return false }; |
|
let Some(layer) = document.network_interface.selected_nodes().selected_layers(document.metadata()).next() else { |
|
return false; |
|
}; |
|
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { |
|
return false; |
|
}; |
|
|
|
|
|
if !vector_data.colinear(*anchor) { |
|
return false; |
|
}; |
|
|
|
let Some(point_id) = anchor.as_anchor() else { return false }; |
|
|
|
let mut connected_segments = [None, None]; |
|
for (segment, bezier, start, end) in vector_data.segment_bezier_iter() { |
|
if start == point_id || end == point_id { |
|
match (connected_segments[0], connected_segments[1]) { |
|
(None, None) => connected_segments[0] = Some(SlidingSegmentData { segment_id: segment, bezier, start }), |
|
(Some(_), None) => connected_segments[1] = Some(SlidingSegmentData { segment_id: segment, bezier, start }), |
|
_ => { |
|
warn!("more than two segments connected to the anchor point"); |
|
return false; |
|
} |
|
} |
|
} |
|
} |
|
let connected_segments = if let [Some(seg1), Some(seg2)] = connected_segments { |
|
[seg1, seg2] |
|
} else { |
|
warn!("expected exactly two connected segments"); |
|
return false; |
|
}; |
|
|
|
self.sliding_point_info = Some(SlidingPointInfo { |
|
anchor: point_id, |
|
layer, |
|
connected_segments, |
|
}); |
|
return true; |
|
} |
|
false |
|
} |
|
|
|
fn slide_point(&mut self, target_position: DVec2, responses: &mut VecDeque<Message>, network_interface: &NodeNetworkInterface, shape_editor: &ShapeState) { |
|
let Some(sliding_point_info) = self.sliding_point_info else { return }; |
|
let anchor = sliding_point_info.anchor; |
|
let layer = sliding_point_info.layer; |
|
|
|
let Some(vector_data) = network_interface.compute_modified_vector(layer) else { return }; |
|
let transform = network_interface.document_metadata().transform_to_viewport(layer); |
|
let layer_pos = transform.inverse().transform_point2(target_position); |
|
|
|
let segments = sliding_point_info.connected_segments; |
|
|
|
let t1 = segments[0].bezier.project(layer_pos); |
|
let position1 = segments[0].bezier.evaluate(TValue::Parametric(t1)); |
|
|
|
let t2 = segments[1].bezier.project(layer_pos); |
|
let position2 = segments[1].bezier.evaluate(TValue::Parametric(t2)); |
|
|
|
let (closer_segment, farther_segment, t_value, new_position) = if position2.distance(layer_pos) < position1.distance(layer_pos) { |
|
(segments[1], segments[0], t2, position2) |
|
} else { |
|
(segments[0], segments[1], t1, position1) |
|
}; |
|
|
|
|
|
let Some(current_position) = ManipulatorPointId::Anchor(anchor).get_position(&vector_data) else { |
|
return; |
|
}; |
|
let delta = new_position - current_position; |
|
|
|
shape_editor.move_anchor(anchor, &vector_data, delta, layer, None, responses); |
|
|
|
|
|
let [first, second] = closer_segment.bezier.split(TValue::Parametric(t_value)); |
|
let closer_segment_other_point = if anchor == closer_segment.start { closer_segment.bezier.end } else { closer_segment.bezier.start }; |
|
|
|
let (split_segment, other_segment) = if first.start == closer_segment_other_point { (first, second) } else { (second, first) }; |
|
|
|
|
|
let closer_primary_handle = HandleId::primary(closer_segment.segment_id); |
|
let Some(handle_position) = split_segment.handle_start() else { return }; |
|
let relative_position1 = handle_position - split_segment.start; |
|
let modification_type = closer_primary_handle.set_relative_position(relative_position1); |
|
responses.add(GraphOperationMessage::Vector { layer, modification_type }); |
|
|
|
let closer_secondary_handle = HandleId::end(closer_segment.segment_id); |
|
let Some(handle_position) = split_segment.handle_end() else { return }; |
|
let relative_position2 = handle_position - split_segment.end; |
|
let modification_type = closer_secondary_handle.set_relative_position(relative_position2); |
|
responses.add(GraphOperationMessage::Vector { layer, modification_type }); |
|
|
|
let end_handle_direction = if anchor == closer_segment.start { -relative_position1 } else { -relative_position2 }; |
|
|
|
let (farther_other_point, start_handle, end_handle, start_handle_pos) = if anchor == farther_segment.start { |
|
( |
|
farther_segment.bezier.end, |
|
HandleId::end(farther_segment.segment_id), |
|
HandleId::primary(farther_segment.segment_id), |
|
farther_segment.bezier.handle_end(), |
|
) |
|
} else { |
|
( |
|
farther_segment.bezier.start, |
|
HandleId::primary(farther_segment.segment_id), |
|
HandleId::end(farther_segment.segment_id), |
|
farther_segment.bezier.handle_start(), |
|
) |
|
}; |
|
let Some(start_handle_position) = start_handle_pos else { return }; |
|
let start_handle_direction = start_handle_position - farther_other_point; |
|
|
|
|
|
let d1 = start_handle_direction.try_normalize().unwrap_or({ |
|
if anchor == farther_segment.start { |
|
-farther_segment.bezier.tangent(TValue::Parametric(0.99)) |
|
} else { |
|
farther_segment.bezier.tangent(TValue::Parametric(0.01)) |
|
} |
|
}); |
|
|
|
let d2 = end_handle_direction.try_normalize().unwrap_or_default(); |
|
|
|
let min_len1 = start_handle_direction.length() * 0.4; |
|
let min_len2 = end_handle_direction.length() * 0.4; |
|
|
|
let (relative_pos1, relative_pos2) = find_two_param_best_approximate(farther_other_point, new_position, d1, d2, min_len1, min_len2, farther_segment.bezier, other_segment); |
|
|
|
|
|
let modification_type = start_handle.set_relative_position(relative_pos1); |
|
responses.add(GraphOperationMessage::Vector { layer, modification_type }); |
|
|
|
let modification_type = end_handle.set_relative_position(relative_pos2); |
|
responses.add(GraphOperationMessage::Vector { layer, modification_type }); |
|
} |
|
|
|
#[allow(clippy::too_many_arguments)] |
|
fn drag( |
|
&mut self, |
|
equidistant: bool, |
|
lock_angle: bool, |
|
snap_angle: bool, |
|
snap_axis: bool, |
|
shape_editor: &mut ShapeState, |
|
document: &DocumentMessageHandler, |
|
input: &InputPreprocessorMessageHandler, |
|
responses: &mut VecDeque<Message>, |
|
) { |
|
|
|
let selected_points = shape_editor.selected_points(); |
|
let single_handle_selected = selected_points.count() == 1 |
|
&& shape_editor |
|
.selected_points() |
|
.any(|point| matches!(point, ManipulatorPointId::EndHandle(_) | ManipulatorPointId::PrimaryHandle(_))); |
|
|
|
|
|
if snap_axis && self.snapping_axis.is_none() && !single_handle_selected { |
|
self.start_snap_along_axis(shape_editor, document, input, responses); |
|
} else if !snap_axis && self.snapping_axis.is_some() { |
|
self.stop_snap_along_axis(shape_editor, document, input, responses); |
|
} |
|
|
|
let document_to_viewport = document.metadata().document_to_viewport; |
|
let previous_mouse = document_to_viewport.transform_point2(self.previous_mouse_position); |
|
let current_mouse = input.mouse.position; |
|
let raw_delta = document_to_viewport.inverse().transform_vector2(current_mouse - previous_mouse); |
|
|
|
let snapped_delta = if let Some((handle_position, anchor_position, handle_id)) = self.try_get_selected_handle_and_anchor(shape_editor, document) { |
|
let cursor_position = handle_position + raw_delta; |
|
|
|
let handle_angle = self.calculate_handle_angle( |
|
shape_editor, |
|
document, |
|
responses, |
|
handle_position - anchor_position, |
|
cursor_position - anchor_position, |
|
handle_id, |
|
lock_angle, |
|
snap_angle, |
|
equidistant, |
|
); |
|
|
|
let adjacent_anchor_offset = self.adjacent_anchor_offset.unwrap_or(DVec2::ZERO); |
|
let constrained_direction = DVec2::new(handle_angle.cos(), handle_angle.sin()); |
|
let projected_length = (cursor_position - anchor_position - adjacent_anchor_offset).dot(constrained_direction); |
|
let constrained_target = anchor_position + adjacent_anchor_offset + constrained_direction * projected_length; |
|
let constrained_delta = constrained_target - handle_position; |
|
|
|
self.apply_snapping( |
|
constrained_direction, |
|
handle_position + constrained_delta, |
|
anchor_position + adjacent_anchor_offset, |
|
lock_angle || snap_angle, |
|
handle_position, |
|
document, |
|
input, |
|
) |
|
} else { |
|
shape_editor.snap(&mut self.snap_manager, &self.snap_cache, document, input, previous_mouse) |
|
}; |
|
|
|
let handle_lengths = if equidistant { None } else { self.opposing_handle_lengths.take() }; |
|
let opposite = if lock_angle { None } else { self.opposite_handle_position }; |
|
let unsnapped_delta = current_mouse - previous_mouse; |
|
let mut was_alt_dragging = false; |
|
|
|
if self.snapping_axis.is_none() { |
|
if self.alt_clicked_on_anchor && !self.alt_dragging_from_anchor && self.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD { |
|
|
|
self.alt_dragging_from_anchor = true; |
|
let Some(layer) = document.network_interface.selected_nodes().selected_layers(document.metadata()).next() else { |
|
return; |
|
}; |
|
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { return }; |
|
let Some(point_id) = shape_editor.selected_points().next().unwrap().get_anchor(&vector_data) else { |
|
return; |
|
}; |
|
|
|
if vector_data.connected_count(point_id) == 2 { |
|
let connected_segments: Vec<HandleId> = vector_data.all_connected(point_id).collect(); |
|
let segment1 = connected_segments[0]; |
|
let Some(tangent1) = self.get_normalized_tangent(point_id, segment1.segment, &vector_data) else { |
|
return; |
|
}; |
|
let segment2 = connected_segments[1]; |
|
let Some(tangent2) = self.get_normalized_tangent(point_id, segment2.segment, &vector_data) else { |
|
return; |
|
}; |
|
|
|
let delta = input.mouse.position - self.drag_start_pos; |
|
let handle = if delta.dot(tangent1) >= delta.dot(tangent2) { |
|
segment1.to_manipulator_point() |
|
} else { |
|
segment2.to_manipulator_point() |
|
}; |
|
|
|
|
|
shape_editor.deselect_all_points(); |
|
shape_editor.select_points_by_manipulator_id(&vec![handle]); |
|
responses.add(PathToolMessage::SelectionChanged); |
|
} |
|
} |
|
|
|
if self.alt_dragging_from_anchor && !equidistant && self.alt_clicked_on_anchor { |
|
was_alt_dragging = true; |
|
self.alt_dragging_from_anchor = false; |
|
self.alt_clicked_on_anchor = false; |
|
} |
|
|
|
let mut skip_opposite = false; |
|
if self.temporary_colinear_handles && !lock_angle { |
|
shape_editor.disable_colinear_handles_state_on_selected(&document.network_interface, responses); |
|
self.temporary_colinear_handles = false; |
|
skip_opposite = true; |
|
} |
|
shape_editor.move_selected_points_and_segments(handle_lengths, document, snapped_delta, equidistant, true, was_alt_dragging, opposite, skip_opposite, responses); |
|
self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(snapped_delta); |
|
} else { |
|
let Some(axis) = self.snapping_axis else { return }; |
|
let projected_delta = match axis { |
|
Axis::X => DVec2::new(unsnapped_delta.x, 0.), |
|
Axis::Y => DVec2::new(0., unsnapped_delta.y), |
|
_ => DVec2::new(unsnapped_delta.x, 0.), |
|
}; |
|
shape_editor.move_selected_points_and_segments(handle_lengths, document, projected_delta, equidistant, true, false, opposite, false, responses); |
|
self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(unsnapped_delta); |
|
} |
|
|
|
|
|
if snap_axis && self.snapping_axis.is_some() { |
|
let Some(current_axis) = self.snapping_axis else { return }; |
|
let total_delta = self.drag_start_pos - input.mouse.position; |
|
|
|
if (total_delta.x.abs() > total_delta.y.abs() && current_axis == Axis::Y) || (total_delta.y.abs() > total_delta.x.abs() && current_axis == Axis::X) { |
|
self.stop_snap_along_axis(shape_editor, document, input, responses); |
|
self.start_snap_along_axis(shape_editor, document, input, responses); |
|
} |
|
} |
|
} |
|
} |
|
|
|
impl Fsm for PathToolFsmState { |
|
type ToolData = PathToolData; |
|
type ToolOptions = PathToolOptions; |
|
|
|
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, input, shape_editor, .. } = tool_action_data; |
|
|
|
update_dynamic_hints(self, responses, shape_editor, document, tool_data, tool_options); |
|
|
|
let ToolMessage::Path(event) = event else { return self }; |
|
match (self, event) { |
|
(_, PathToolMessage::SelectionChanged) => { |
|
|
|
let target_layers = document.network_interface.selected_nodes().selected_layers(document.metadata()).collect(); |
|
shape_editor.set_selected_layers(target_layers); |
|
|
|
responses.add(OverlaysMessage::Draw); |
|
self |
|
} |
|
(_, PathToolMessage::UpdateSelectedPointsStatus { overlay_context }) => { |
|
let display_anchors = overlay_context.visibility_settings.anchors(); |
|
let display_handles = overlay_context.visibility_settings.handles(); |
|
|
|
shape_editor.update_selected_anchors_status(display_anchors); |
|
shape_editor.update_selected_handles_status(display_handles); |
|
|
|
self |
|
} |
|
(_, PathToolMessage::Overlays(mut overlay_context)) => { |
|
|
|
|
|
match tool_options.path_overlay_mode { |
|
PathOverlayMode::AllHandles => { |
|
path_overlays(document, DrawHandles::All, shape_editor, &mut overlay_context); |
|
tool_data.frontier_handles_info = None; |
|
} |
|
PathOverlayMode::SelectedPointHandles => { |
|
let selected_segments = selected_segments(&document.network_interface, shape_editor); |
|
|
|
path_overlays(document, DrawHandles::SelectedAnchors(selected_segments), shape_editor, &mut overlay_context); |
|
tool_data.frontier_handles_info = None; |
|
} |
|
PathOverlayMode::FrontierHandles => { |
|
let selected_segments = selected_segments(&document.network_interface, shape_editor); |
|
let selected_points = shape_editor.selected_points(); |
|
let selected_anchors = selected_points |
|
.filter_map(|point_id| if let ManipulatorPointId::Anchor(p) = point_id { Some(*p) } else { None }) |
|
.collect::<Vec<_>>(); |
|
|
|
|
|
if shape_editor.selected_points().count() == 1 { |
|
path_overlays(document, DrawHandles::SelectedAnchors(selected_segments), shape_editor, &mut overlay_context); |
|
} else { |
|
let mut segment_endpoints: HashMap<SegmentId, Vec<PointId>> = HashMap::new(); |
|
|
|
for layer in document.network_interface.selected_nodes().selected_layers(document.metadata()) { |
|
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue }; |
|
|
|
|
|
let mut selected_segments_by_point: HashMap<PointId, Vec<SegmentId>> = HashMap::new(); |
|
|
|
for (segment_id, _bezier, start, end) in vector_data.segment_bezier_iter() { |
|
if selected_segments.contains(&segment_id) { |
|
selected_segments_by_point.entry(start).or_default().push(segment_id); |
|
selected_segments_by_point.entry(end).or_default().push(segment_id); |
|
} |
|
} |
|
|
|
for (point, attached_segments) in selected_segments_by_point { |
|
if attached_segments.len() == 1 { |
|
segment_endpoints.entry(attached_segments[0]).or_default().push(point); |
|
} |
|
|
|
else if !selected_anchors.contains(&point) { |
|
segment_endpoints.entry(attached_segments[0]).or_default().push(point); |
|
segment_endpoints.entry(attached_segments[1]).or_default().push(point); |
|
} |
|
} |
|
} |
|
|
|
|
|
tool_data.frontier_handles_info = Some(segment_endpoints.clone()); |
|
|
|
|
|
path_overlays(document, DrawHandles::FrontierHandles(segment_endpoints), shape_editor, &mut overlay_context); |
|
} |
|
} |
|
} |
|
|
|
match self { |
|
Self::Ready => { |
|
tool_data.update_closest_segment(shape_editor, input.mouse.position, document, tool_options.path_overlay_mode); |
|
|
|
if let Some(closest_segment) = &tool_data.segment { |
|
if tool_options.path_editing_mode.segment_editing_mode { |
|
let transform = document.metadata().transform_to_viewport(closest_segment.layer()); |
|
|
|
overlay_context.outline_overlay_bezier(closest_segment.bezier(), transform); |
|
|
|
|
|
let display_anchors = overlay_context.visibility_settings.anchors(); |
|
if display_anchors { |
|
let start_pos = transform.transform_point2(closest_segment.bezier().start); |
|
let end_pos = transform.transform_point2(closest_segment.bezier().end); |
|
let start_id = closest_segment.points()[0]; |
|
let end_id = closest_segment.points()[1]; |
|
if let Some(shape_state) = shape_editor.selected_shape_state.get_mut(&closest_segment.layer()) { |
|
overlay_context.manipulator_anchor(start_pos, shape_state.is_point_selected(ManipulatorPointId::Anchor(start_id)), None); |
|
overlay_context.manipulator_anchor(end_pos, shape_state.is_point_selected(ManipulatorPointId::Anchor(end_id)), None); |
|
} |
|
} |
|
} else { |
|
let perp = closest_segment.calculate_perp(document); |
|
let point = closest_segment.closest_point(document.metadata()); |
|
|
|
|
|
if tool_data.delete_segment_pressed { |
|
let angle = 45_f64.to_radians(); |
|
let tilted_line = DVec2::from_angle(angle).rotate(perp); |
|
let tilted_perp = tilted_line.perp(); |
|
|
|
overlay_context.line(point - tilted_line * SEGMENT_OVERLAY_SIZE, point + tilted_line * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), None); |
|
overlay_context.line(point - tilted_perp * SEGMENT_OVERLAY_SIZE, point + tilted_perp * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), None); |
|
} |
|
|
|
else { |
|
overlay_context.line(point - perp * SEGMENT_OVERLAY_SIZE, point + perp * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), None); |
|
} |
|
} |
|
} |
|
} |
|
Self::Drawing { selection_shape } => { |
|
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, '#'); |
|
let fill_color = Some(fill_color.as_str()); |
|
|
|
let selection_mode = match tool_action_data.preferences.get_selection_mode() { |
|
SelectionMode::Directional => tool_data.calculate_selection_mode_from_direction(document.metadata()), |
|
selection_mode => selection_mode, |
|
}; |
|
|
|
let quad = tool_data.selection_quad(document.metadata()); |
|
let polygon = &tool_data.lasso_polygon; |
|
|
|
match (selection_shape, selection_mode, tool_data.started_drawing_from_inside) { |
|
|
|
(SelectionShapeType::Box, SelectionMode::Enclosed, false) => overlay_context.dashed_quad(quad, None, fill_color, Some(4.), Some(4.), Some(0.5)), |
|
(SelectionShapeType::Lasso, SelectionMode::Enclosed, _) => overlay_context.dashed_polygon(polygon, None, fill_color, Some(4.), Some(4.), Some(0.5)), |
|
(SelectionShapeType::Box, _, false) => overlay_context.quad(quad, None, fill_color), |
|
(SelectionShapeType::Lasso, _, _) => overlay_context.polygon(polygon, None, fill_color), |
|
(SelectionShapeType::Box, _, _) => {} |
|
} |
|
} |
|
Self::Dragging(_) => { |
|
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); |
|
|
|
|
|
if tool_data.snapping_axis.is_some() { |
|
let Some(axis) = tool_data.snapping_axis else { return self }; |
|
let origin = tool_data.drag_start_pos; |
|
let viewport_diagonal = input.viewport_bounds.size().length(); |
|
|
|
let faded = |color: &str| { |
|
let mut color = graphene_std::Color::from_rgb_str(color.strip_prefix('#').unwrap()).unwrap().with_alpha(0.25).to_rgba_hex_srgb(); |
|
color.insert(0, '#'); |
|
color |
|
}; |
|
match axis { |
|
Axis::Y => { |
|
overlay_context.line(origin - DVec2::Y * viewport_diagonal, origin + DVec2::Y * viewport_diagonal, Some(COLOR_OVERLAY_GREEN), None); |
|
overlay_context.line(origin - DVec2::X * viewport_diagonal, origin + DVec2::X * viewport_diagonal, Some(&faded(COLOR_OVERLAY_RED)), None); |
|
} |
|
Axis::X | Axis::Both => { |
|
overlay_context.line(origin - DVec2::X * viewport_diagonal, origin + DVec2::X * viewport_diagonal, Some(COLOR_OVERLAY_RED), None); |
|
overlay_context.line(origin - DVec2::Y * viewport_diagonal, origin + DVec2::Y * viewport_diagonal, Some(&faded(COLOR_OVERLAY_GREEN)), None); |
|
} |
|
} |
|
} |
|
} |
|
Self::SlidingPoint => {} |
|
Self::MoldingSegment => {} |
|
} |
|
|
|
responses.add(PathToolMessage::SelectedPointUpdated); |
|
responses.add(PathToolMessage::UpdateSelectedPointsStatus { overlay_context }); |
|
self |
|
} |
|
|
|
|
|
( |
|
_, |
|
PathToolMessage::MouseDown { |
|
extend_selection, |
|
lasso_select, |
|
handle_drag_from_anchor, |
|
drag_restore_handle, |
|
molding_in_segment_edit, |
|
}, |
|
) => { |
|
let extend_selection = input.keyboard.get(extend_selection as usize); |
|
let lasso_select = input.keyboard.get(lasso_select as usize); |
|
let handle_drag_from_anchor = input.keyboard.get(handle_drag_from_anchor as usize); |
|
let drag_zero_handle = input.keyboard.get(drag_restore_handle as usize); |
|
let molding_in_segment_edit = input.keyboard.get(molding_in_segment_edit as usize); |
|
|
|
tool_data.selection_mode = None; |
|
tool_data.lasso_polygon.clear(); |
|
|
|
tool_data.mouse_down( |
|
shape_editor, |
|
document, |
|
input, |
|
responses, |
|
extend_selection, |
|
lasso_select, |
|
handle_drag_from_anchor, |
|
drag_zero_handle, |
|
molding_in_segment_edit, |
|
tool_options.path_overlay_mode, |
|
tool_options.path_editing_mode.segment_editing_mode, |
|
tool_options.path_editing_mode.point_editing_mode, |
|
) |
|
} |
|
( |
|
PathToolFsmState::Drawing { selection_shape }, |
|
PathToolMessage::PointerMove { |
|
equidistant, |
|
toggle_colinear, |
|
move_anchor_with_handles, |
|
snap_angle, |
|
lock_angle, |
|
delete_segment, |
|
break_colinear_molding, |
|
}, |
|
) => { |
|
tool_data.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position); |
|
tool_data.started_drawing_from_inside = false; |
|
|
|
if selection_shape == SelectionShapeType::Lasso { |
|
extend_lasso(&mut tool_data.lasso_polygon, input.mouse.position); |
|
} |
|
|
|
responses.add(OverlaysMessage::Draw); |
|
|
|
|
|
let messages = [ |
|
PathToolMessage::PointerOutsideViewport { |
|
equidistant, |
|
toggle_colinear, |
|
move_anchor_with_handles, |
|
snap_angle, |
|
lock_angle, |
|
delete_segment, |
|
break_colinear_molding, |
|
} |
|
.into(), |
|
PathToolMessage::PointerMove { |
|
equidistant, |
|
toggle_colinear, |
|
move_anchor_with_handles, |
|
snap_angle, |
|
lock_angle, |
|
delete_segment, |
|
break_colinear_molding, |
|
} |
|
.into(), |
|
]; |
|
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); |
|
|
|
PathToolFsmState::Drawing { selection_shape } |
|
} |
|
( |
|
PathToolFsmState::Dragging(_), |
|
PathToolMessage::PointerMove { |
|
equidistant, |
|
toggle_colinear, |
|
move_anchor_with_handles, |
|
snap_angle, |
|
lock_angle, |
|
delete_segment, |
|
break_colinear_molding, |
|
}, |
|
) => { |
|
let mut selected_only_handles = true; |
|
|
|
let selected_points = shape_editor.selected_points(); |
|
|
|
for point in selected_points { |
|
if matches!(point, ManipulatorPointId::Anchor(_)) { |
|
selected_only_handles = false; |
|
break; |
|
} |
|
} |
|
|
|
if !tool_data.saved_points_before_handle_drag.is_empty() && (tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD) && (selected_only_handles) { |
|
tool_data.handle_drag_toggle = true; |
|
} |
|
|
|
let anchor_and_handle_toggled = input.keyboard.get(move_anchor_with_handles as usize); |
|
let initial_press = anchor_and_handle_toggled && !tool_data.select_anchor_toggled; |
|
let released_from_toggle = tool_data.select_anchor_toggled && !anchor_and_handle_toggled; |
|
|
|
if initial_press { |
|
responses.add(PathToolMessage::SelectedPointUpdated); |
|
tool_data.select_anchor_toggled = true; |
|
tool_data.save_points_before_anchor_toggle(shape_editor.selected_points().cloned().collect()); |
|
shape_editor.select_handles_and_anchor_connected_to_current_handle(&document.network_interface); |
|
} else if released_from_toggle { |
|
responses.add(PathToolMessage::SelectedPointUpdated); |
|
tool_data.select_anchor_toggled = false; |
|
shape_editor.deselect_all_points(); |
|
shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_anchor_select_toggle); |
|
tool_data.remove_saved_points(); |
|
} |
|
|
|
let toggle_colinear_state = input.keyboard.get(toggle_colinear as usize); |
|
let equidistant_state = input.keyboard.get(equidistant as usize); |
|
let lock_angle_state = input.keyboard.get(lock_angle as usize); |
|
let snap_angle_state = input.keyboard.get(snap_angle as usize); |
|
|
|
if !lock_angle_state { |
|
tool_data.angle_locked = false; |
|
tool_data.adjacent_anchor_offset = None; |
|
} |
|
|
|
if !tool_data.update_colinear(equidistant_state, toggle_colinear_state, tool_action_data.shape_editor, tool_action_data.document, responses) { |
|
if snap_angle_state && lock_angle_state && tool_data.start_sliding_point(tool_action_data.shape_editor, tool_action_data.document) { |
|
return PathToolFsmState::SlidingPoint; |
|
} |
|
|
|
tool_data.drag( |
|
equidistant_state, |
|
lock_angle_state, |
|
snap_angle_state, |
|
snap_angle_state, |
|
tool_action_data.shape_editor, |
|
tool_action_data.document, |
|
input, |
|
responses, |
|
); |
|
} |
|
|
|
|
|
let messages = [ |
|
PathToolMessage::PointerOutsideViewport { |
|
toggle_colinear, |
|
equidistant, |
|
move_anchor_with_handles, |
|
snap_angle, |
|
lock_angle, |
|
delete_segment, |
|
break_colinear_molding, |
|
} |
|
.into(), |
|
PathToolMessage::PointerMove { |
|
toggle_colinear, |
|
equidistant, |
|
move_anchor_with_handles, |
|
snap_angle, |
|
lock_angle, |
|
delete_segment, |
|
break_colinear_molding, |
|
} |
|
.into(), |
|
]; |
|
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); |
|
|
|
PathToolFsmState::Dragging(tool_data.dragging_state) |
|
} |
|
(PathToolFsmState::SlidingPoint, PathToolMessage::PointerMove { .. }) => { |
|
tool_data.slide_point(input.mouse.position, responses, &document.network_interface, shape_editor); |
|
PathToolFsmState::SlidingPoint |
|
} |
|
(PathToolFsmState::MoldingSegment, PathToolMessage::PointerMove { break_colinear_molding, .. }) => { |
|
if tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD { |
|
tool_data.molding_segment = true; |
|
} |
|
|
|
let break_colinear_molding = input.keyboard.get(break_colinear_molding as usize); |
|
|
|
|
|
if let Some(segment) = &mut tool_data.segment { |
|
if let Some(molding_segment_handles) = tool_data.molding_info { |
|
tool_data.temporary_adjacent_handles_while_molding = segment.mold_handle_positions( |
|
document, |
|
responses, |
|
molding_segment_handles, |
|
input.mouse.position, |
|
break_colinear_molding, |
|
tool_data.temporary_adjacent_handles_while_molding, |
|
); |
|
} |
|
} |
|
|
|
PathToolFsmState::MoldingSegment |
|
} |
|
(PathToolFsmState::Ready, PathToolMessage::PointerMove { delete_segment, .. }) => { |
|
tool_data.delete_segment_pressed = input.keyboard.get(delete_segment as usize); |
|
|
|
if !tool_data.saved_points_before_anchor_convert_smooth_sharp.is_empty() { |
|
tool_data.saved_points_before_anchor_convert_smooth_sharp.clear(); |
|
} |
|
|
|
if tool_data.adjacent_anchor_offset.is_some() { |
|
tool_data.adjacent_anchor_offset = None; |
|
} |
|
|
|
responses.add(OverlaysMessage::Draw); |
|
|
|
self |
|
} |
|
(PathToolFsmState::Drawing { selection_shape: selection_type }, PathToolMessage::PointerOutsideViewport { .. }) => { |
|
|
|
if let Some(offset) = tool_data.auto_panning.shift_viewport(input, responses) { |
|
tool_data.drag_start_pos += offset; |
|
} |
|
|
|
PathToolFsmState::Drawing { selection_shape: selection_type } |
|
} |
|
(PathToolFsmState::Dragging(dragging_state), PathToolMessage::PointerOutsideViewport { .. }) => { |
|
|
|
if let Some(offset) = tool_data.auto_panning.shift_viewport(input, responses) { |
|
tool_data.drag_start_pos += offset; |
|
} |
|
|
|
PathToolFsmState::Dragging(dragging_state) |
|
} |
|
( |
|
state, |
|
PathToolMessage::PointerOutsideViewport { |
|
equidistant, |
|
toggle_colinear, |
|
move_anchor_with_handles, |
|
snap_angle, |
|
lock_angle, |
|
delete_segment, |
|
break_colinear_molding, |
|
}, |
|
) => { |
|
|
|
let messages = [ |
|
PathToolMessage::PointerOutsideViewport { |
|
equidistant, |
|
toggle_colinear, |
|
move_anchor_with_handles, |
|
snap_angle, |
|
lock_angle, |
|
delete_segment, |
|
break_colinear_molding, |
|
} |
|
.into(), |
|
PathToolMessage::PointerMove { |
|
equidistant, |
|
toggle_colinear, |
|
move_anchor_with_handles, |
|
snap_angle, |
|
lock_angle, |
|
delete_segment, |
|
break_colinear_molding, |
|
} |
|
.into(), |
|
]; |
|
tool_data.auto_panning.stop(&messages, responses); |
|
|
|
state |
|
} |
|
(PathToolFsmState::Drawing { selection_shape }, PathToolMessage::Enter { extend_selection, shrink_selection }) => { |
|
let extend_selection = input.keyboard.get(extend_selection as usize); |
|
let shrink_selection = input.keyboard.get(shrink_selection as usize); |
|
|
|
let selection_change = if shrink_selection { |
|
SelectionChange::Shrink |
|
} else if extend_selection { |
|
SelectionChange::Extend |
|
} else { |
|
SelectionChange::Clear |
|
}; |
|
|
|
let document_to_viewport = document.metadata().document_to_viewport; |
|
let previous_mouse = document_to_viewport.transform_point2(tool_data.previous_mouse_position); |
|
if tool_data.drag_start_pos == previous_mouse { |
|
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] }); |
|
} else { |
|
let selection_mode = match tool_action_data.preferences.get_selection_mode() { |
|
SelectionMode::Directional => tool_data.calculate_selection_mode_from_direction(document.metadata()), |
|
selection_mode => selection_mode, |
|
}; |
|
|
|
match selection_shape { |
|
SelectionShapeType::Box => { |
|
let bbox = [tool_data.drag_start_pos, previous_mouse]; |
|
shape_editor.select_all_in_shape( |
|
&document.network_interface, |
|
SelectionShape::Box(bbox), |
|
selection_change, |
|
tool_options.path_overlay_mode, |
|
tool_data.frontier_handles_info.clone(), |
|
tool_options.path_editing_mode.segment_editing_mode, |
|
selection_mode, |
|
); |
|
} |
|
SelectionShapeType::Lasso => shape_editor.select_all_in_shape( |
|
&document.network_interface, |
|
SelectionShape::Lasso(&tool_data.lasso_polygon), |
|
selection_change, |
|
tool_options.path_overlay_mode, |
|
tool_data.frontier_handles_info.clone(), |
|
tool_options.path_editing_mode.segment_editing_mode, |
|
selection_mode, |
|
), |
|
} |
|
} |
|
|
|
responses.add(OverlaysMessage::Draw); |
|
|
|
PathToolFsmState::Ready |
|
} |
|
(PathToolFsmState::Dragging { .. }, PathToolMessage::Escape | PathToolMessage::RightClick) => { |
|
if tool_data.handle_drag_toggle && tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD { |
|
shape_editor.deselect_all_points(); |
|
shape_editor.deselect_all_segments(); |
|
shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_handle_drag); |
|
|
|
tool_data.saved_points_before_handle_drag.clear(); |
|
tool_data.handle_drag_toggle = false; |
|
} |
|
tool_data.angle_locked = false; |
|
responses.add(DocumentMessage::AbortTransaction); |
|
tool_data.snap_manager.cleanup(responses); |
|
PathToolFsmState::Ready |
|
} |
|
(PathToolFsmState::Drawing { .. }, PathToolMessage::Escape | PathToolMessage::RightClick) => { |
|
tool_data.snap_manager.cleanup(responses); |
|
PathToolFsmState::Ready |
|
} |
|
(PathToolFsmState::SlidingPoint, PathToolMessage::Escape | PathToolMessage::RightClick) => { |
|
tool_data.sliding_point_info = None; |
|
|
|
responses.add(DocumentMessage::AbortTransaction); |
|
tool_data.snap_manager.cleanup(responses); |
|
|
|
PathToolFsmState::Ready |
|
} |
|
(PathToolFsmState::MoldingSegment, PathToolMessage::Escape | PathToolMessage::RightClick) => { |
|
|
|
tool_data.molding_info = None; |
|
tool_data.molding_segment = false; |
|
tool_data.temporary_adjacent_handles_while_molding = None; |
|
|
|
responses.add(DocumentMessage::AbortTransaction); |
|
tool_data.snap_manager.cleanup(responses); |
|
|
|
PathToolFsmState::Ready |
|
} |
|
|
|
(PathToolFsmState::Drawing { selection_shape }, PathToolMessage::DragStop { extend_selection, shrink_selection }) => { |
|
let extend_selection = input.keyboard.get(extend_selection as usize); |
|
let shrink_selection = input.keyboard.get(shrink_selection as usize); |
|
|
|
let select_kind = if shrink_selection { |
|
SelectionChange::Shrink |
|
} else if extend_selection { |
|
SelectionChange::Extend |
|
} else { |
|
SelectionChange::Clear |
|
}; |
|
|
|
let document_to_viewport = document.metadata().document_to_viewport; |
|
let previous_mouse = document_to_viewport.transform_point2(tool_data.previous_mouse_position); |
|
|
|
let selection_mode = match tool_action_data.preferences.get_selection_mode() { |
|
SelectionMode::Directional => tool_data.calculate_selection_mode_from_direction(document.metadata()), |
|
selection_mode => selection_mode, |
|
}; |
|
|
|
if tool_data.drag_start_pos.distance(previous_mouse) < 1e-8 { |
|
|
|
if document.click(input).is_none() { |
|
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] }); |
|
} |
|
} else { |
|
match selection_shape { |
|
SelectionShapeType::Box => { |
|
let bbox = [tool_data.drag_start_pos, previous_mouse]; |
|
shape_editor.select_all_in_shape( |
|
&document.network_interface, |
|
SelectionShape::Box(bbox), |
|
select_kind, |
|
tool_options.path_overlay_mode, |
|
tool_data.frontier_handles_info.clone(), |
|
tool_options.path_editing_mode.segment_editing_mode, |
|
selection_mode, |
|
); |
|
} |
|
SelectionShapeType::Lasso => shape_editor.select_all_in_shape( |
|
&document.network_interface, |
|
SelectionShape::Lasso(&tool_data.lasso_polygon), |
|
select_kind, |
|
tool_options.path_overlay_mode, |
|
tool_data.frontier_handles_info.clone(), |
|
tool_options.path_editing_mode.segment_editing_mode, |
|
selection_mode, |
|
), |
|
} |
|
} |
|
responses.add(OverlaysMessage::Draw); |
|
responses.add(PathToolMessage::SelectedPointUpdated); |
|
|
|
PathToolFsmState::Ready |
|
} |
|
(_, PathToolMessage::DragStop { extend_selection, .. }) => { |
|
let extend_selection = input.keyboard.get(extend_selection as usize); |
|
let drag_occurred = tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD; |
|
|
|
let nearest_point = shape_editor.find_nearest_visible_point_indices( |
|
&document.network_interface, |
|
input.mouse.position, |
|
SELECTION_THRESHOLD, |
|
tool_options.path_overlay_mode, |
|
tool_data.frontier_handles_info.clone(), |
|
); |
|
|
|
let nearest_segment = tool_data.segment.clone(); |
|
|
|
if let Some(segment) = &mut tool_data.segment { |
|
let segment_mode = tool_options.path_editing_mode.segment_editing_mode; |
|
if !drag_occurred && !tool_data.molding_segment && !segment_mode { |
|
if tool_data.delete_segment_pressed { |
|
if let Some(vector_data) = document.network_interface.compute_modified_vector(segment.layer()) { |
|
shape_editor.dissolve_segment(responses, segment.layer(), &vector_data, segment.segment(), segment.points()); |
|
} |
|
} else { |
|
segment.adjusted_insert_and_select(shape_editor, responses, extend_selection); |
|
} |
|
} |
|
|
|
tool_data.segment = None; |
|
tool_data.molding_info = None; |
|
tool_data.molding_segment = false; |
|
tool_data.temporary_adjacent_handles_while_molding = None; |
|
} |
|
|
|
let segment_mode = tool_options.path_editing_mode.segment_editing_mode; |
|
|
|
if let Some((layer, nearest_point)) = nearest_point { |
|
let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == point); |
|
if !drag_occurred && extend_selection { |
|
if clicked_selected && tool_data.last_clicked_point_was_selected { |
|
shape_editor.selected_shape_state.entry(layer).or_default().deselect_point(nearest_point); |
|
} else { |
|
shape_editor.selected_shape_state.entry(layer).or_default().select_point(nearest_point); |
|
} |
|
responses.add(OverlaysMessage::Draw); |
|
} |
|
if !drag_occurred && !extend_selection && clicked_selected { |
|
if tool_data.saved_points_before_anchor_convert_smooth_sharp.is_empty() { |
|
tool_data.saved_points_before_anchor_convert_smooth_sharp = shape_editor.selected_points().copied().collect::<HashSet<_>>(); |
|
} |
|
|
|
shape_editor.deselect_all_points(); |
|
shape_editor.deselect_all_segments(); |
|
|
|
shape_editor.selected_shape_state.entry(layer).or_default().select_point(nearest_point); |
|
|
|
responses.add(OverlaysMessage::Draw); |
|
} |
|
} |
|
|
|
else if let Some(nearest_segment) = nearest_segment { |
|
if segment_mode { |
|
let clicked_selected = shape_editor.selected_segments().any(|&segment| segment == nearest_segment.segment()); |
|
if !drag_occurred && extend_selection { |
|
if clicked_selected && tool_data.last_clicked_segment_was_selected { |
|
shape_editor |
|
.selected_shape_state |
|
.entry(nearest_segment.layer()) |
|
.or_default() |
|
.deselect_segment(nearest_segment.segment()); |
|
} else { |
|
shape_editor.selected_shape_state.entry(nearest_segment.layer()).or_default().select_segment(nearest_segment.segment()); |
|
} |
|
|
|
responses.add(OverlaysMessage::Draw); |
|
} |
|
if !drag_occurred && !extend_selection && clicked_selected { |
|
shape_editor.deselect_all_segments(); |
|
shape_editor.deselect_all_points(); |
|
shape_editor.selected_shape_state.entry(nearest_segment.layer()).or_default().select_segment(nearest_segment.segment()); |
|
|
|
responses.add(OverlaysMessage::Draw); |
|
} |
|
} |
|
} |
|
|
|
else if tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD { |
|
shape_editor.deselect_all_points(); |
|
shape_editor.deselect_all_segments(); |
|
} |
|
|
|
if tool_data.temporary_colinear_handles { |
|
tool_data.temporary_colinear_handles = false; |
|
} |
|
|
|
if tool_data.handle_drag_toggle && drag_occurred { |
|
shape_editor.deselect_all_points(); |
|
shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_handle_drag); |
|
|
|
tool_data.saved_points_before_handle_drag.clear(); |
|
tool_data.handle_drag_toggle = false; |
|
} |
|
|
|
tool_data.alt_dragging_from_anchor = false; |
|
tool_data.alt_clicked_on_anchor = false; |
|
tool_data.angle_locked = false; |
|
|
|
if tool_data.select_anchor_toggled { |
|
shape_editor.deselect_all_points(); |
|
shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_anchor_select_toggle); |
|
tool_data.remove_saved_points(); |
|
tool_data.select_anchor_toggled = false; |
|
} |
|
|
|
tool_data.snapping_axis = None; |
|
tool_data.sliding_point_info = None; |
|
|
|
responses.add(DocumentMessage::EndTransaction); |
|
responses.add(PathToolMessage::SelectedPointUpdated); |
|
tool_data.snap_manager.cleanup(responses); |
|
tool_data.opposite_handle_position = None; |
|
|
|
PathToolFsmState::Ready |
|
} |
|
|
|
|
|
(_, PathToolMessage::Delete) => { |
|
|
|
responses.add(DocumentMessage::AddTransaction); |
|
shape_editor.delete_selected_segments(document, responses); |
|
shape_editor.delete_selected_points(document, responses); |
|
responses.add(PathToolMessage::SelectionChanged); |
|
|
|
PathToolFsmState::Ready |
|
} |
|
(_, PathToolMessage::BreakPath) => { |
|
shape_editor.break_path_at_selected_point(document, responses); |
|
PathToolFsmState::Ready |
|
} |
|
(_, PathToolMessage::DeleteAndBreakPath) => { |
|
shape_editor.delete_point_and_break_path(document, responses); |
|
PathToolFsmState::Ready |
|
} |
|
(_, PathToolMessage::FlipSmoothSharp) => { |
|
|
|
let nearest_point = shape_editor.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD); |
|
if nearest_point.is_some() { |
|
|
|
if !tool_data.double_click_handled && tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD { |
|
responses.add(DocumentMessage::StartTransaction); |
|
|
|
shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_anchor_convert_smooth_sharp.iter().copied().collect::<Vec<_>>()); |
|
shape_editor.flip_smooth_sharp(&document.network_interface, input.mouse.position, SELECTION_TOLERANCE, responses); |
|
tool_data.saved_points_before_anchor_convert_smooth_sharp.clear(); |
|
|
|
responses.add(DocumentMessage::EndTransaction); |
|
responses.add(PathToolMessage::SelectedPointUpdated); |
|
} |
|
|
|
return PathToolFsmState::Ready; |
|
} |
|
|
|
|
|
if let Some(layer) = document.click(input) { |
|
|
|
shape_editor.select_connected_anchors(document, layer, input.mouse.position); |
|
responses.add(OverlaysMessage::Draw); |
|
} |
|
|
|
PathToolFsmState::Ready |
|
} |
|
(_, PathToolMessage::Abort) => { |
|
responses.add(OverlaysMessage::Draw); |
|
PathToolFsmState::Ready |
|
} |
|
(_, PathToolMessage::NudgeSelectedPoints { delta_x, delta_y }) => { |
|
shape_editor.move_selected_points_and_segments( |
|
tool_data.opposing_handle_lengths.take(), |
|
document, |
|
(delta_x, delta_y).into(), |
|
true, |
|
false, |
|
false, |
|
tool_data.opposite_handle_position, |
|
false, |
|
responses, |
|
); |
|
|
|
PathToolFsmState::Ready |
|
} |
|
(_, PathToolMessage::SelectAllAnchors) => { |
|
shape_editor.select_all_anchors_in_selected_layers(document); |
|
responses.add(OverlaysMessage::Draw); |
|
PathToolFsmState::Ready |
|
} |
|
(_, PathToolMessage::DeselectAllPoints) => { |
|
shape_editor.deselect_all_points(); |
|
responses.add(OverlaysMessage::Draw); |
|
PathToolFsmState::Ready |
|
} |
|
(_, PathToolMessage::SelectedPointXChanged { new_x }) => { |
|
if let Some(&SingleSelectedPoint { coordinates, id, layer, .. }) = tool_data.selection_status.as_one() { |
|
shape_editor.reposition_control_point(&id, &document.network_interface, DVec2::new(new_x, coordinates.y), layer, responses); |
|
} |
|
PathToolFsmState::Ready |
|
} |
|
(_, PathToolMessage::SelectedPointYChanged { new_y }) => { |
|
if let Some(&SingleSelectedPoint { coordinates, id, layer, .. }) = tool_data.selection_status.as_one() { |
|
shape_editor.reposition_control_point(&id, &document.network_interface, DVec2::new(coordinates.x, new_y), layer, responses); |
|
} |
|
PathToolFsmState::Ready |
|
} |
|
(_, PathToolMessage::SelectedPointUpdated) => { |
|
let colinear = shape_editor.selected_manipulator_angles(&document.network_interface); |
|
tool_data.dragging_state = DraggingState { |
|
point_select_state: shape_editor.get_dragging_state(&document.network_interface), |
|
colinear, |
|
}; |
|
tool_data.update_selection_status(shape_editor, document); |
|
self |
|
} |
|
(_, PathToolMessage::ManipulatorMakeHandlesColinear) => { |
|
responses.add(DocumentMessage::StartTransaction); |
|
shape_editor.convert_selected_manipulators_to_colinear_handles(responses, document); |
|
responses.add(DocumentMessage::EndTransaction); |
|
responses.add(PathToolMessage::SelectionChanged); |
|
PathToolFsmState::Ready |
|
} |
|
(_, PathToolMessage::ManipulatorMakeHandlesFree) => { |
|
responses.add(DocumentMessage::StartTransaction); |
|
shape_editor.disable_colinear_handles_state_on_selected(&document.network_interface, responses); |
|
responses.add(DocumentMessage::EndTransaction); |
|
PathToolFsmState::Ready |
|
} |
|
(_, _) => PathToolFsmState::Ready, |
|
} |
|
} |
|
|
|
fn update_hints(&self, _responses: &mut VecDeque<Message>) { |
|
|
|
} |
|
|
|
fn update_cursor(&self, responses: &mut VecDeque<Message>) { |
|
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); |
|
} |
|
} |
|
|
|
#[derive(Debug, PartialEq, Default)] |
|
enum SelectionStatus { |
|
#[default] |
|
None, |
|
One(SingleSelectedPoint), |
|
Multiple(MultipleSelectedPoints), |
|
} |
|
|
|
impl SelectionStatus { |
|
fn as_one(&self) -> Option<&SingleSelectedPoint> { |
|
match self { |
|
SelectionStatus::One(one) => Some(one), |
|
_ => None, |
|
} |
|
} |
|
|
|
fn angle(&self) -> Option<ManipulatorAngle> { |
|
match self { |
|
Self::None => None, |
|
Self::One(one) => Some(one.manipulator_angle), |
|
Self::Multiple(one) => Some(one.manipulator_angle), |
|
} |
|
} |
|
} |
|
|
|
#[derive(Debug, PartialEq)] |
|
struct MultipleSelectedPoints { |
|
manipulator_angle: ManipulatorAngle, |
|
} |
|
|
|
#[derive(Debug, PartialEq)] |
|
struct SingleSelectedPoint { |
|
coordinates: DVec2, |
|
id: ManipulatorPointId, |
|
layer: LayerNodeIdentifier, |
|
manipulator_angle: ManipulatorAngle, |
|
} |
|
|
|
|
|
|
|
fn get_selection_status(network_interface: &NodeNetworkInterface, shape_state: &mut ShapeState) -> SelectionStatus { |
|
let mut selection_layers = shape_state.selected_shape_state.iter().map(|(k, v)| (*k, v.selected_points_count())); |
|
let total_selected_points = selection_layers.clone().map(|(_, v)| v).sum::<usize>(); |
|
|
|
|
|
if total_selected_points == 1 { |
|
let Some(layer) = selection_layers.find(|(_, v)| *v > 0).map(|(k, _)| k) else { |
|
return SelectionStatus::None; |
|
}; |
|
let Some(vector_data) = network_interface.compute_modified_vector(layer) else { |
|
return SelectionStatus::None; |
|
}; |
|
let Some(&point) = shape_state.selected_points().next() else { |
|
return SelectionStatus::None; |
|
}; |
|
let Some(local_position) = point.get_position(&vector_data) else { |
|
return SelectionStatus::None; |
|
}; |
|
|
|
let coordinates = network_interface.document_metadata().transform_to_document(layer).transform_point2(local_position); |
|
let manipulator_angle = if vector_data.colinear(point) { ManipulatorAngle::Colinear } else { ManipulatorAngle::Free }; |
|
|
|
return SelectionStatus::One(SingleSelectedPoint { |
|
coordinates, |
|
layer, |
|
id: point, |
|
manipulator_angle, |
|
}); |
|
}; |
|
|
|
|
|
if total_selected_points > 1 { |
|
return SelectionStatus::Multiple(MultipleSelectedPoints { |
|
manipulator_angle: shape_state.selected_manipulator_angles(network_interface), |
|
}); |
|
} |
|
|
|
SelectionStatus::None |
|
} |
|
|
|
fn calculate_lock_angle( |
|
tool_data: &mut PathToolData, |
|
shape_state: &mut ShapeState, |
|
responses: &mut VecDeque<Message>, |
|
document: &DocumentMessageHandler, |
|
vector_data: &VectorData, |
|
handle_id: ManipulatorPointId, |
|
tangent_to_neighboring_tangents: bool, |
|
) -> Option<f64> { |
|
let anchor = handle_id.get_anchor(vector_data)?; |
|
let anchor_position = vector_data.point_domain.position_from_id(anchor); |
|
let current_segment = handle_id.get_segment(); |
|
let points_connected = vector_data.connected_count(anchor); |
|
|
|
let (anchor_position, segment) = anchor_position.zip(current_segment)?; |
|
if points_connected == 1 { |
|
calculate_segment_angle(anchor, segment, vector_data, false) |
|
} else { |
|
let opposite_handle = handle_id |
|
.get_handle_pair(vector_data) |
|
.iter() |
|
.flatten() |
|
.find(|&h| h.to_manipulator_point() != handle_id) |
|
.copied() |
|
.map(|h| h.to_manipulator_point()); |
|
let opposite_handle_position = opposite_handle.and_then(|h| h.get_position(vector_data)).filter(|pos| (pos - anchor_position).length() > 1e-6); |
|
|
|
if let Some(opposite_pos) = opposite_handle_position { |
|
if !vector_data.colinear_manipulators.iter().flatten().map(|h| h.to_manipulator_point()).any(|h| h == handle_id) { |
|
shape_state.convert_selected_manipulators_to_colinear_handles(responses, document); |
|
tool_data.temporary_colinear_handles = true; |
|
} |
|
Some(-(opposite_pos - anchor_position).angle_to(DVec2::X)) |
|
} else { |
|
let angle_1 = vector_data |
|
.adjacent_segment(&handle_id) |
|
.and_then(|(_, adjacent_segment)| calculate_segment_angle(anchor, adjacent_segment, vector_data, false)); |
|
|
|
let angle_2 = calculate_segment_angle(anchor, segment, vector_data, false); |
|
|
|
match (angle_1, angle_2) { |
|
(Some(angle_1), Some(angle_2)) => { |
|
let angle = Some((angle_1 + angle_2) / 2.); |
|
if tangent_to_neighboring_tangents { |
|
angle.map(|angle| angle + std::f64::consts::FRAC_PI_2) |
|
} else { |
|
angle |
|
} |
|
} |
|
(Some(angle_1), None) => Some(angle_1), |
|
(None, Some(angle_2)) => Some(angle_2), |
|
(None, None) => None, |
|
} |
|
} |
|
} |
|
} |
|
|
|
fn check_handle_over_adjacent_anchor(handle_id: ManipulatorPointId, vector_data: &VectorData) -> Option<PointId> { |
|
let (anchor, handle_position) = handle_id.get_anchor(vector_data).zip(handle_id.get_position(vector_data))?; |
|
|
|
let check_if_close = |point_id: &PointId| { |
|
let Some(anchor_position) = vector_data.point_domain.position_from_id(*point_id) else { |
|
return false; |
|
}; |
|
(anchor_position - handle_position).length() < 10. |
|
}; |
|
|
|
vector_data.connected_points(anchor).find(check_if_close) |
|
} |
|
fn calculate_adjacent_anchor_tangent( |
|
currently_dragged_handle: ManipulatorPointId, |
|
anchor: Option<PointId>, |
|
adjacent_anchor: Option<PointId>, |
|
vector_data: &VectorData, |
|
) -> (Option<f64>, Option<DVec2>) { |
|
|
|
|
|
let Some((dragged_handle_anchor, adjacent_anchor)) = anchor.zip(adjacent_anchor) else { |
|
return (None, None); |
|
}; |
|
let adjacent_anchor_position = vector_data.point_domain.position_from_id(adjacent_anchor); |
|
|
|
let handles: Vec<_> = vector_data.all_connected(adjacent_anchor).filter(|handle| handle.length(vector_data) > 1e-6).collect(); |
|
|
|
match handles.len() { |
|
0 => { |
|
|
|
let non_shared_segment: Vec<_> = vector_data |
|
.segment_bezier_iter() |
|
.filter_map(|(segment_id, _, start, end)| { |
|
let touches_adjacent = start == adjacent_anchor || end == adjacent_anchor; |
|
let shares_with_dragged = start == dragged_handle_anchor || end == dragged_handle_anchor; |
|
|
|
if touches_adjacent && !shares_with_dragged { Some(segment_id) } else { None } |
|
}) |
|
.collect(); |
|
|
|
match non_shared_segment.first() { |
|
Some(&segment) => { |
|
let angle = calculate_segment_angle(adjacent_anchor, segment, vector_data, true); |
|
(angle, adjacent_anchor_position) |
|
} |
|
None => (None, None), |
|
} |
|
} |
|
|
|
1 => { |
|
let segment = handles[0].segment; |
|
let angle = calculate_segment_angle(adjacent_anchor, segment, vector_data, true); |
|
(angle, adjacent_anchor_position) |
|
} |
|
|
|
2 => { |
|
|
|
let Some(shared_segment_handle) = handles |
|
.iter() |
|
.find(|handle| handle.opposite().to_manipulator_point() == currently_dragged_handle) |
|
.map(|handle| handle.to_manipulator_point()) |
|
else { |
|
return (None, None); |
|
}; |
|
|
|
let angle = shared_segment_handle |
|
.get_position(vector_data) |
|
.zip(adjacent_anchor_position) |
|
.map(|(handle, anchor)| -(handle - anchor).angle_to(DVec2::X)); |
|
|
|
(angle, adjacent_anchor_position) |
|
} |
|
|
|
_ => (None, None), |
|
} |
|
} |
|
|
|
fn update_dynamic_hints( |
|
state: PathToolFsmState, |
|
responses: &mut VecDeque<Message>, |
|
shape_editor: &mut ShapeState, |
|
document: &DocumentMessageHandler, |
|
tool_data: &PathToolData, |
|
tool_options: &PathToolOptions, |
|
) { |
|
|
|
|
|
let hint_data = match state { |
|
PathToolFsmState::Ready => { |
|
|
|
let single_anchor_selected = shape_editor.selected_points().count() == 1 && shape_editor.selected_points().any(|point| matches!(point, ManipulatorPointId::Anchor(_))); |
|
let at_least_one_anchor_selected = shape_editor.selected_points().any(|point| matches!(point, ManipulatorPointId::Anchor(_))); |
|
let at_least_one_point_selected = shape_editor.selected_points().count() >= 1; |
|
|
|
let mut single_colinear_anchor_selected = false; |
|
if single_anchor_selected { |
|
if let (Some(anchor), Some(layer)) = ( |
|
shape_editor.selected_points().next(), |
|
document.network_interface.selected_nodes().selected_layers(document.metadata()).next(), |
|
) { |
|
if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { |
|
single_colinear_anchor_selected = vector_data.colinear(*anchor) |
|
} |
|
} |
|
} |
|
|
|
let mut drag_selected_hints = vec![HintInfo::mouse(MouseMotion::LmbDrag, "Drag Selected")]; |
|
let mut delete_selected_hints = vec![HintInfo::keys([Key::Delete], "Delete Selected")]; |
|
|
|
if at_least_one_anchor_selected { |
|
delete_selected_hints.push(HintInfo::keys([Key::Accel], "No Dissolve").prepend_plus()); |
|
delete_selected_hints.push(HintInfo::keys([Key::Shift], "Cut Anchor").prepend_plus()); |
|
} |
|
|
|
if single_colinear_anchor_selected { |
|
drag_selected_hints.push(HintInfo::multi_keys([[Key::Control], [Key::Shift]], "Slide").prepend_plus()); |
|
} |
|
|
|
let mut hint_data = match (tool_data.segment.is_some(), tool_options.path_editing_mode.segment_editing_mode) { |
|
(true, true) => { |
|
vec![ |
|
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Select Segment"), HintInfo::keys([Key::Shift], "Extend").prepend_plus()]), |
|
HintGroup(vec![HintInfo::keys_and_mouse([Key::KeyA], MouseMotion::Lmb, "Mold Segment")]), |
|
] |
|
} |
|
(true, false) => { |
|
vec![ |
|
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Insert Point on Segment")]), |
|
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Mold Segment")]), |
|
HintGroup(vec![HintInfo::keys_and_mouse([Key::Alt], MouseMotion::Lmb, "Delete Segment")]), |
|
] |
|
} |
|
(false, _) => { |
|
vec![ |
|
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Select Point"), HintInfo::keys([Key::Shift], "Extend").prepend_plus()]), |
|
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), HintInfo::keys([Key::Control], "Lasso").prepend_plus()]), |
|
] |
|
} |
|
}; |
|
|
|
if at_least_one_anchor_selected { |
|
|
|
hint_data.push(HintGroup(vec![ |
|
HintInfo::mouse(MouseMotion::LmbDouble, "Convert Anchor Point"), |
|
HintInfo::keys_and_mouse([Key::Alt], MouseMotion::Lmb, "To Sharp"), |
|
HintInfo::keys_and_mouse([Key::Alt], MouseMotion::LmbDrag, "To Smooth"), |
|
])); |
|
} |
|
|
|
if at_least_one_point_selected { |
|
let mut groups = vec![ |
|
HintGroup(drag_selected_hints), |
|
HintGroup(vec![HintInfo::multi_keys([[Key::KeyG], [Key::KeyR], [Key::KeyS]], "Grab/Rotate/Scale Selected")]), |
|
HintGroup(vec![HintInfo::arrow_keys("Nudge Selected"), HintInfo::keys([Key::Shift], "10x").prepend_plus()]), |
|
HintGroup(delete_selected_hints), |
|
]; |
|
hint_data.append(&mut groups); |
|
} |
|
|
|
HintData(hint_data) |
|
} |
|
PathToolFsmState::Dragging(dragging_state) => { |
|
let colinear = dragging_state.colinear; |
|
let mut dragging_hint_data = HintData(Vec::new()); |
|
dragging_hint_data |
|
.0 |
|
.push(HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])); |
|
|
|
let drag_anchor = HintInfo::keys([Key::Space], "Drag Anchor"); |
|
let toggle_group = match dragging_state.point_select_state { |
|
PointSelectState::HandleNoPair | PointSelectState::HandleWithPair => { |
|
let mut hints = vec![HintInfo::keys([Key::Tab], "Swap Dragged Handle")]; |
|
hints.push(HintInfo::keys( |
|
[Key::KeyC], |
|
if colinear == ManipulatorAngle::Colinear { |
|
"Break Colinear Handles" |
|
} else { |
|
"Make Handles Colinear" |
|
}, |
|
)); |
|
hints |
|
} |
|
PointSelectState::Anchor => Vec::new(), |
|
}; |
|
let hold_group = match dragging_state.point_select_state { |
|
PointSelectState::HandleNoPair => { |
|
let mut hints = vec![]; |
|
if colinear != ManipulatorAngle::Free { |
|
hints.push(HintInfo::keys([Key::Alt], "Equidistant Handles")); |
|
} |
|
hints.push(HintInfo::keys([Key::Shift], "15° Increments")); |
|
hints.push(HintInfo::keys([Key::Control], "Lock Angle")); |
|
hints.push(drag_anchor); |
|
hints |
|
} |
|
PointSelectState::HandleWithPair => { |
|
let mut hints = vec![]; |
|
if colinear != ManipulatorAngle::Free { |
|
hints.push(HintInfo::keys([Key::Alt], "Equidistant Handles")); |
|
} |
|
hints.push(HintInfo::keys([Key::Shift], "15° Increments")); |
|
hints.push(HintInfo::keys([Key::Control], "Lock Angle")); |
|
hints.push(drag_anchor); |
|
hints |
|
} |
|
PointSelectState::Anchor => Vec::new(), |
|
}; |
|
|
|
if !toggle_group.is_empty() { |
|
dragging_hint_data.0.push(HintGroup(toggle_group)); |
|
} |
|
|
|
if !hold_group.is_empty() { |
|
dragging_hint_data.0.push(HintGroup(hold_group)); |
|
} |
|
|
|
dragging_hint_data |
|
} |
|
PathToolFsmState::Drawing { .. } => HintData(vec![ |
|
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), |
|
HintGroup(vec![ |
|
HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), |
|
HintInfo::keys([Key::Shift], "Extend").prepend_plus(), |
|
HintInfo::keys([Key::Alt], "Subtract").prepend_plus(), |
|
]), |
|
]), |
|
PathToolFsmState::MoldingSegment => { |
|
let mut has_colinear_anchors = false; |
|
|
|
if let Some(segment) = &tool_data.segment { |
|
let handle1 = HandleId::primary(segment.segment()); |
|
let handle2 = HandleId::end(segment.segment()); |
|
|
|
if let Some(vector_data) = document.network_interface.compute_modified_vector(segment.layer()) { |
|
let other_handle1 = vector_data.other_colinear_handle(handle1); |
|
let other_handle2 = vector_data.other_colinear_handle(handle2); |
|
if other_handle1.is_some() || other_handle2.is_some() { |
|
has_colinear_anchors = true; |
|
} |
|
}; |
|
} |
|
|
|
let handles_stored = if let Some(other_handles) = tool_data.temporary_adjacent_handles_while_molding { |
|
other_handles[0].is_some() || other_handles[1].is_some() |
|
} else { |
|
false |
|
}; |
|
|
|
let molding_disable_possible = has_colinear_anchors || handles_stored; |
|
|
|
let mut molding_hints = vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]; |
|
|
|
if molding_disable_possible { |
|
molding_hints.push(HintGroup(vec![HintInfo::keys([Key::Alt], "Break Colinear Handles")])); |
|
} |
|
|
|
HintData(molding_hints) |
|
} |
|
PathToolFsmState::SlidingPoint => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]), |
|
}; |
|
responses.add(FrontendMessage::UpdateInputHints { hint_data }); |
|
} |
|
|