2705 lines
103 KiB
Rust
2705 lines
103 KiB
Rust
use super::select_tool::extend_lasso;
|
|
use super::tool_prelude::*;
|
|
use crate::consts::{
|
|
COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, DOUBLE_CLICK_MILLISECONDS, 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::pivot::{PivotGizmo, PivotGizmoType, PivotToolSource, pin_pivot_widget, pivot_gizmo_type_widget, pivot_reference_point_widget};
|
|
use crate::messages::tool::common_functionality::shape_editor::{
|
|
ClosestSegment, ManipulatorAngle, OpposingHandleLengths, SelectedLayerState, 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, BezierHandles, TValue};
|
|
use graphene_std::renderer::Quad;
|
|
use graphene_std::transform::ReferencePoint;
|
|
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 {
|
|
// Standard messages
|
|
Abort,
|
|
Overlays(OverlayContext),
|
|
SelectionChanged,
|
|
|
|
// Tool-specific messages
|
|
BreakPath,
|
|
DeselectAllPoints,
|
|
Delete,
|
|
DeleteAndBreakPath,
|
|
DragStop {
|
|
extend_selection: Key,
|
|
shrink_selection: Key,
|
|
},
|
|
Enter {
|
|
extend_selection: Key,
|
|
shrink_selection: Key,
|
|
},
|
|
Escape,
|
|
ClosePath,
|
|
DoubleClick {
|
|
extend_selection: Key,
|
|
shrink_selection: Key,
|
|
},
|
|
GRS {
|
|
// Should be `Key::KeyG` (Grab), `Key::KeyR` (Rotate), or `Key::KeyS` (Scale)
|
|
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,
|
|
},
|
|
SetPivot {
|
|
position: ReferencePoint,
|
|
},
|
|
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 },
|
|
PivotGizmoType(PivotGizmoType),
|
|
TogglePivotGizmoType(bool),
|
|
TogglePivotPinned,
|
|
}
|
|
|
|
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,
|
|
})
|
|
// TODO: Remove `unwrap_or_default` once checkboxes are capable of displaying a mixed state
|
|
.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)
|
|
// TODO(Keavon): Replace with a real icon
|
|
.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)
|
|
// TODO(Keavon): Replace with a real icon
|
|
.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();
|
|
|
|
let [_checkbox, _dropdown] = {
|
|
let pivot_gizmo_type_widget = pivot_gizmo_type_widget(self.tool_data.pivot_gizmo.state, PivotToolSource::Path);
|
|
[pivot_gizmo_type_widget[0].clone(), pivot_gizmo_type_widget[2].clone()]
|
|
};
|
|
|
|
let has_something = !self.tool_data.saved_points_before_anchor_convert_smooth_sharp.is_empty();
|
|
let _pivot_reference = pivot_reference_point_widget(
|
|
has_something || !self.tool_data.pivot_gizmo.state.is_pivot(),
|
|
self.tool_data.pivot_gizmo.pivot.to_pivot_position(),
|
|
PivotToolSource::Path,
|
|
);
|
|
|
|
let _pin_pivot = pin_pivot_widget(self.tool_data.pivot_gizmo.pin_active(), false, PivotToolSource::Path);
|
|
|
|
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.clone(),
|
|
path_overlay_mode_widget,
|
|
unrelated_seperator.clone(),
|
|
// checkbox.clone(),
|
|
// related_seperator.clone(),
|
|
// dropdown.clone(),
|
|
// unrelated_seperator,
|
|
// pivot_reference,
|
|
// related_seperator.clone(),
|
|
// pin_pivot,
|
|
],
|
|
}]))
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
PathOptionsUpdate::PivotGizmoType(gizmo_type) => {
|
|
if !self.tool_data.pivot_gizmo.state.disabled {
|
|
self.tool_data.pivot_gizmo.state.gizmo_type = gizmo_type;
|
|
responses.add(ToolMessage::UpdateHints);
|
|
let pivot_gizmo = self.tool_data.pivot_gizmo();
|
|
responses.add(TransformLayerMessage::SetPivotGizmo { pivot_gizmo });
|
|
responses.add(NodeGraphMessage::RunDocumentGraph);
|
|
self.send_layout(responses, LayoutTarget::ToolOptions);
|
|
}
|
|
}
|
|
PathOptionsUpdate::TogglePivotGizmoType(state) => {
|
|
self.tool_data.pivot_gizmo.state.disabled = !state;
|
|
responses.add(ToolMessage::UpdateHints);
|
|
responses.add(NodeGraphMessage::RunDocumentGraph);
|
|
self.send_layout(responses, LayoutTarget::ToolOptions);
|
|
}
|
|
|
|
PathOptionsUpdate::TogglePivotPinned => {
|
|
self.tool_data.pivot_gizmo.pivot.pinned = !self.tool_data.pivot_gizmo.pivot.pinned;
|
|
responses.add(ToolMessage::UpdateHints);
|
|
responses.add(NodeGraphMessage::RunDocumentGraph);
|
|
self.send_layout(responses, LayoutTarget::ToolOptions);
|
|
}
|
|
},
|
|
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);
|
|
}
|
|
}
|
|
|
|
// Different actions depending on state may be wanted:
|
|
fn actions(&self) -> ActionList {
|
|
match self.fsm_state {
|
|
PathToolFsmState::Ready => actions!(PathToolMessageDiscriminant;
|
|
DoubleClick,
|
|
MouseDown,
|
|
Delete,
|
|
NudgeSelectedPoints,
|
|
Enter,
|
|
SelectAllAnchors,
|
|
DeselectAllPoints,
|
|
BreakPath,
|
|
DeleteAndBreakPath,
|
|
ClosePath,
|
|
PointerMove,
|
|
),
|
|
PathToolFsmState::Dragging(_) => actions!(PathToolMessageDiscriminant;
|
|
Escape,
|
|
RightClick,
|
|
DoubleClick,
|
|
DragStop,
|
|
PointerMove,
|
|
Delete,
|
|
BreakPath,
|
|
DeleteAndBreakPath,
|
|
SwapSelectedHandles,
|
|
),
|
|
PathToolFsmState::Drawing { .. } => actions!(PathToolMessageDiscriminant;
|
|
DoubleClick,
|
|
DragStop,
|
|
PointerMove,
|
|
Delete,
|
|
Enter,
|
|
BreakPath,
|
|
DeleteAndBreakPath,
|
|
Escape,
|
|
RightClick,
|
|
),
|
|
PathToolFsmState::SlidingPoint => actions!(PathToolMessageDiscriminant;
|
|
PointerMove,
|
|
DragStop,
|
|
Escape,
|
|
RightClick
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
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,
|
|
}
|
|
|
|
#[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>,
|
|
/// Describes information about the selected point(s), if any, across one or multiple shapes and manipulator point types (anchor or handle).
|
|
/// The available information varies depending on whether `None`, `One`, or `Multiple` points are currently selected.
|
|
/// NOTE: It must be updated using `update_selection_status` to ensure `can_toggle_colinearity` stays synchronized with the current selection.
|
|
selection_status: SelectionStatus,
|
|
/// `true` if we can change the current selection to colinear or not.
|
|
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,
|
|
pivot_gizmo: PivotGizmo,
|
|
ordered_points: Vec<ManipulatorPointId>,
|
|
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,
|
|
first_selected_with_single_click: bool,
|
|
stored_selection: Option<HashMap<LayerNodeIdentifier, SelectedLayerState>>,
|
|
}
|
|
|
|
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 {
|
|
// This also covers the case where they're equal: the area is zero, so we use `Enclosed` to ensure the selection ends up empty, as nothing will be enclosed by an empty area
|
|
SelectionMode::Enclosed
|
|
};
|
|
self.selection_mode = Some(mode);
|
|
}
|
|
|
|
self.selection_mode.unwrap_or(SelectionMode::Touched)
|
|
}
|
|
|
|
pub fn selection_box(&self, metadata: &DocumentMetadata) -> [DVec2; 2] {
|
|
// Convert previous mouse position to viewport space first
|
|
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;
|
|
}
|
|
|
|
// TODO: This function is for basic point select mode. We definitely need to make a new one for the segment select mode.
|
|
#[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 input.time - self.last_click_time > DOUBLE_CLICK_MILLISECONDS {
|
|
self.saved_points_before_anchor_convert_smooth_sharp.clear();
|
|
self.stored_selection = None;
|
|
}
|
|
|
|
self.last_click_time = input.time;
|
|
|
|
let old_selection = shape_editor.selected_points().cloned().collect::<Vec<_>>();
|
|
|
|
// Check if the point is already selected; if not, select the first point within the threshold (in pixels)
|
|
// Don't select the points which are not shown currently in PathOverlayMode
|
|
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 the point is already selected and shift (`extend_selection`) is used, keep the selection unchanged.
|
|
// Otherwise, select the first point within the threshold.
|
|
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;
|
|
|
|
// If selected points contain only handles and there was some selection before, then it is stored and becomes restored upon release
|
|
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) {
|
|
// Check that selected point is an anchor
|
|
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)
|
|
}
|
|
// We didn't find a point nearby, so we will see if there is a segment to select or insert a point on
|
|
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();
|
|
}
|
|
|
|
// Add to selected segments
|
|
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 start_pos = segment.bezier().start;
|
|
let end_pos = segment.bezier().end;
|
|
|
|
let [pos1, pos2] = match segment.bezier().handles {
|
|
BezierHandles::Cubic { handle_start, handle_end } => [handle_start, handle_end],
|
|
BezierHandles::Quadratic { handle } => [handle, end_pos],
|
|
BezierHandles::Linear => [start_pos + (end_pos - start_pos) / 3., end_pos + (start_pos - end_pos) / 3.],
|
|
};
|
|
self.molding_info = Some((pos1, pos2));
|
|
PathToolFsmState::Dragging(self.dragging_state)
|
|
}
|
|
}
|
|
// If no other layers are selected and this is a single-click, then also select the layer (exception)
|
|
else if let Some(layer) = document.click(input) {
|
|
if shape_editor.selected_shape_state.is_empty() {
|
|
self.first_selected_with_single_click = true;
|
|
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] });
|
|
}
|
|
|
|
self.started_drawing_from_inside = true;
|
|
|
|
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 }
|
|
}
|
|
// Start drawing
|
|
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_if_feeds(layer, &document.network_interface);
|
|
|
|
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 };
|
|
// Check which handle is selected and which is opposite
|
|
let opposite = if handle == handle1 { handle2 } else { handle1 };
|
|
|
|
self.opposite_handle_position = if self.opposite_handle_position.is_none() {
|
|
opposite.to_manipulator_point().get_position(&vector_data)
|
|
} else {
|
|
self.opposite_handle_position
|
|
};
|
|
}
|
|
for (&id, &position) in vector_data.point_domain.ids().iter().zip(vector_data.point_domain.positions()) {
|
|
if layer_manipulators.contains(&id) {
|
|
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 {
|
|
// Check handle colinear state
|
|
let is_colinear = self
|
|
.selection_status
|
|
.angle()
|
|
.map(|angle| match angle {
|
|
ManipulatorAngle::Colinear => true,
|
|
ManipulatorAngle::Free | ManipulatorAngle::Mixed => false,
|
|
})
|
|
.unwrap_or(false);
|
|
|
|
// Check if the toggle_colinear key has just been pressed
|
|
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 {
|
|
// Try to get selected handle info
|
|
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;
|
|
};
|
|
|
|
// Check if handle has a pair (to ignore handles of edges of open paths)
|
|
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())
|
|
});
|
|
|
|
// Make handles colinear if opposite handle is zero 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
|
|
}
|
|
|
|
/// Attempts to get a single selected handle. Also retrieves the position of the anchor it is connected to. Used for the purpose of snapping the angle.
|
|
fn try_get_selected_handle_and_anchor(&self, shape_editor: &ShapeState, document: &DocumentMessageHandler) -> Option<(DVec2, DVec2, ManipulatorPointId)> {
|
|
// Only count selections of a single layer
|
|
let (layer, selection) = shape_editor.selected_shape_state.iter().next()?;
|
|
|
|
// Do not allow selections of multiple points to count
|
|
if selection.selected_points_count() != 1 {
|
|
return None;
|
|
}
|
|
|
|
// Only count selected handles
|
|
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_if_feeds(*layer, &document.network_interface);
|
|
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 the handle is dragged over one of its adjacent anchors while holding down the Ctrl key, compute the angle based on the tangent formed with the neighboring anchor points.
|
|
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_if_feeds(*layer, &document.network_interface);
|
|
|
|
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 the handle is dragged near its adjacent anchors while holding down the Ctrl key, compute the angle using the tangent direction of neighboring segments.
|
|
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);
|
|
}
|
|
|
|
// Finalize and apply angle locking if a valid target angle was determined.
|
|
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);
|
|
}
|
|
|
|
// When the angle is locked we use the old angle
|
|
if self.angle_locked {
|
|
return self.angle;
|
|
}
|
|
|
|
// Round the angle to the closest increment
|
|
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>) {
|
|
// Find the negative delta to take the point to the drag start position
|
|
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);
|
|
|
|
// Calculate the projected delta and shift the points along that delta
|
|
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>) {
|
|
// Calculate the negative delta of the selection and move it back to the drag start
|
|
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);
|
|
|
|
// Calculate what actually would have been the original delta for the point, and apply that
|
|
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) {
|
|
// Check if there is no point nearby
|
|
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;
|
|
}
|
|
// If already hovering on a segment, then recalculate its closest point
|
|
else if let Some(closest_segment) = &mut self.segment {
|
|
closest_segment.update_closest_point(document.metadata(), &document.network_interface, position);
|
|
|
|
if closest_segment.too_far(position, SEGMENT_INSERTION_DISTANCE) {
|
|
self.segment = None;
|
|
}
|
|
}
|
|
// If not, check that if there is some closest segment or not
|
|
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;
|
|
};
|
|
|
|
// Check that the handles of anchor point are also colinear
|
|
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_if_feeds(layer, network_interface);
|
|
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)
|
|
};
|
|
|
|
// Move the anchor to the new position
|
|
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);
|
|
|
|
// Make a split at the t_value
|
|
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) };
|
|
|
|
// Primary handle maps to primary handle and secondary maps to secondary
|
|
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;
|
|
|
|
// Get normalized direction vectors, if cubic handle is zero then we consider corresponding tangent
|
|
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);
|
|
|
|
// Now set those handles to these handle lengths keeping the directions d1, d2
|
|
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>,
|
|
) {
|
|
// First check if selection is not just a single handle point
|
|
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(_)));
|
|
|
|
// This is where it starts snapping along axis
|
|
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 {
|
|
// Checking which direction the dragging begins
|
|
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()
|
|
};
|
|
|
|
// Now change the selection to this handle
|
|
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);
|
|
}
|
|
|
|
// Constantly checking and changing the snapping axis based on current mouse position
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn pivot_gizmo(&self) -> PivotGizmo {
|
|
self.pivot_gizmo.clone()
|
|
}
|
|
|
|
fn sync_history(&mut self, points: &[ManipulatorPointId]) {
|
|
self.ordered_points.retain(|layer| points.contains(layer));
|
|
self.ordered_points.extend(points.iter().find(|&layer| !self.ordered_points.contains(layer)));
|
|
self.pivot_gizmo.point = self.ordered_points.last().copied()
|
|
}
|
|
}
|
|
|
|
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 };
|
|
|
|
// TODO(mTvare6): Remove once gizmos are implemented for path_tool
|
|
tool_data.pivot_gizmo.state.disabled = true;
|
|
|
|
match (self, event) {
|
|
(_, PathToolMessage::SelectionChanged) => {
|
|
// Set the newly targeted layers to visible
|
|
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);
|
|
|
|
let new_points = shape_editor.selected_points().copied().collect::<Vec<_>>();
|
|
tool_data.sync_history(&new_points);
|
|
|
|
self
|
|
}
|
|
(_, PathToolMessage::Overlays(mut overlay_context)) => {
|
|
// TODO: find the segment ids of which the selected points are a part of
|
|
|
|
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<_>>();
|
|
|
|
// Match the behavior of `PathOverlayMode::SelectedPointHandles` when only one point is selected
|
|
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 };
|
|
|
|
// The points which are part of only one segment will be rendered
|
|
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);
|
|
}
|
|
// Handle the edge case where a point, although not explicitly selected, is shared by two segments.
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Caching segment endpoints for use in point selection logic
|
|
tool_data.frontier_handles_info = Some(segment_endpoints.clone());
|
|
|
|
// Now frontier anchors can be sent for rendering overlays
|
|
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_if_feeds(closest_segment.layer(), &document.network_interface);
|
|
|
|
overlay_context.outline_overlay_bezier(closest_segment.bezier(), transform);
|
|
|
|
// Draw the anchors again
|
|
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(), &document.network_interface);
|
|
|
|
// Draw an X on the segment
|
|
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);
|
|
}
|
|
// Draw a line on the segment
|
|
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) {
|
|
// Don't draw box if it is from inside a shape and selection just began
|
|
(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);
|
|
|
|
// Draw the snapping axis lines
|
|
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 => {}
|
|
}
|
|
|
|
responses.add(PathToolMessage::SelectedPointUpdated);
|
|
responses.add(PathToolMessage::UpdateSelectedPointsStatus { overlay_context });
|
|
self
|
|
}
|
|
|
|
// Mouse down
|
|
(
|
|
_,
|
|
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;
|
|
tool_data.stored_selection = None;
|
|
|
|
if selection_shape == SelectionShapeType::Lasso {
|
|
extend_lasso(&mut tool_data.lasso_polygon, input.mouse.position);
|
|
}
|
|
|
|
responses.add(OverlaysMessage::Draw);
|
|
|
|
// Auto-panning
|
|
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 selected_only_handles = !shape_editor.selected_points().any(|point| matches!(point, ManipulatorPointId::Anchor(_)));
|
|
tool_data.stored_selection = None;
|
|
|
|
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;
|
|
}
|
|
|
|
if tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD {
|
|
tool_data.molding_segment = true;
|
|
}
|
|
|
|
let break_molding = input.keyboard.get(break_colinear_molding as usize);
|
|
|
|
// Logic for molding segment
|
|
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_molding,
|
|
tool_data.temporary_adjacent_handles_while_molding,
|
|
);
|
|
}
|
|
|
|
return PathToolFsmState::Dragging(tool_data.dragging_state);
|
|
}
|
|
|
|
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,
|
|
);
|
|
}
|
|
|
|
// Auto-panning
|
|
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::Ready, PathToolMessage::PointerMove { delete_segment, .. }) => {
|
|
tool_data.delete_segment_pressed = input.keyboard.get(delete_segment as usize);
|
|
tool_data.saved_points_before_anchor_convert_smooth_sharp.clear();
|
|
tool_data.adjacent_anchor_offset = None;
|
|
tool_data.stored_selection = None;
|
|
|
|
responses.add(OverlaysMessage::Draw);
|
|
|
|
self
|
|
}
|
|
(PathToolFsmState::Drawing { selection_shape: selection_type }, PathToolMessage::PointerOutsideViewport { .. }) => {
|
|
// Auto-panning
|
|
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 { .. }) => {
|
|
// Auto-panning
|
|
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,
|
|
},
|
|
) => {
|
|
// Auto-panning
|
|
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.molding_info = None;
|
|
tool_data.molding_segment = false;
|
|
tool_data.temporary_adjacent_handles_while_molding = None;
|
|
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
|
|
}
|
|
// Mouse up
|
|
(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,
|
|
};
|
|
tool_data.started_drawing_from_inside = false;
|
|
|
|
if tool_data.drag_start_pos.distance(previous_mouse) < 1e-8 {
|
|
// Clicked inside or outside the shape then deselect all of the points/segments
|
|
if document.click(input).is_some() && tool_data.stored_selection.is_none() {
|
|
tool_data.stored_selection = Some(shape_editor.selected_shape_state.clone());
|
|
}
|
|
|
|
shape_editor.deselect_all_points();
|
|
shape_editor.deselect_all_segments();
|
|
} 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);
|
|
}
|
|
}
|
|
// Segment editing mode
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
// Deselect all points if the user clicks the filled region of the shape
|
|
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
|
|
}
|
|
|
|
// Delete key
|
|
(_, PathToolMessage::Delete) => {
|
|
// Delete the selected points and clean up overlays
|
|
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::DoubleClick { extend_selection, shrink_selection }) => {
|
|
// Double-clicked on a point (flip smooth/sharp behavior)
|
|
let nearest_point = shape_editor.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD);
|
|
if nearest_point.is_some() {
|
|
// Flip the selected point between smooth and sharp
|
|
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;
|
|
}
|
|
// Double-clicked on a filled region
|
|
else if let Some(layer) = document.click(input) {
|
|
let extend_selection = input.keyboard.get(extend_selection as usize);
|
|
let shrink_selection = input.keyboard.get(shrink_selection as usize);
|
|
|
|
if shape_editor.is_selected_layer(layer) {
|
|
if extend_selection && !tool_data.first_selected_with_single_click {
|
|
responses.add(NodeGraphMessage::SelectedNodesRemove { nodes: vec![layer.to_node()] });
|
|
|
|
if let Some(selection) = &tool_data.stored_selection {
|
|
let mut selection = selection.clone();
|
|
selection.remove(&layer);
|
|
shape_editor.selected_shape_state = selection;
|
|
tool_data.stored_selection = None;
|
|
}
|
|
} else if shrink_selection && !tool_data.first_selected_with_single_click {
|
|
// Only deselect all the points of the double clicked layer
|
|
if let Some(selection) = &tool_data.stored_selection {
|
|
let selection = selection.clone();
|
|
shape_editor.selected_shape_state = selection;
|
|
tool_data.stored_selection = None;
|
|
}
|
|
|
|
let state = shape_editor.selected_shape_state.get_mut(&layer).expect("No state for selected layer");
|
|
state.deselect_all_points_in_layer();
|
|
state.deselect_all_segments_in_layer();
|
|
} else if !tool_data.first_selected_with_single_click {
|
|
// Select according to the selected editing mode
|
|
let point_editing_mode = tool_options.path_editing_mode.point_editing_mode;
|
|
let segment_editing_mode = tool_options.path_editing_mode.segment_editing_mode;
|
|
shape_editor.select_connected(document, layer, input.mouse.position, point_editing_mode, segment_editing_mode);
|
|
|
|
// Select all the other layers back again
|
|
if let Some(selection) = &tool_data.stored_selection {
|
|
let mut selection = selection.clone();
|
|
selection.remove(&layer);
|
|
|
|
for (layer, state) in selection {
|
|
shape_editor.selected_shape_state.insert(layer, state);
|
|
}
|
|
tool_data.stored_selection = None;
|
|
}
|
|
}
|
|
|
|
// If it was the very first click without there being an existing selection,
|
|
// then the single-click behavior and double-click behavior should not collide
|
|
tool_data.first_selected_with_single_click = false;
|
|
} else if extend_selection {
|
|
responses.add(NodeGraphMessage::SelectedNodesAdd { nodes: vec![layer.to_node()] });
|
|
|
|
if let Some(selection) = &tool_data.stored_selection {
|
|
shape_editor.selected_shape_state = selection.clone();
|
|
tool_data.stored_selection = None;
|
|
}
|
|
} else {
|
|
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] });
|
|
}
|
|
|
|
responses.add(OverlaysMessage::Draw);
|
|
}
|
|
// Double clicked on the background
|
|
else {
|
|
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] });
|
|
}
|
|
|
|
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
|
|
}
|
|
(_, PathToolMessage::SetPivot { position }) => {
|
|
responses.add(DocumentMessage::StartTransaction);
|
|
|
|
tool_data.pivot_gizmo.pivot.last_non_none_reference_point = position;
|
|
let position: Option<DVec2> = position.into();
|
|
tool_data.pivot_gizmo.pivot.set_normalized_position(position.unwrap());
|
|
let pivot_gizmo = tool_data.pivot_gizmo();
|
|
responses.add(TransformLayerMessage::SetPivotGizmo { pivot_gizmo });
|
|
responses.add(NodeGraphMessage::RunDocumentGraph);
|
|
|
|
self
|
|
}
|
|
(_, _) => PathToolFsmState::Ready,
|
|
}
|
|
}
|
|
|
|
fn update_hints(&self, _responses: &mut VecDeque<Message>) {
|
|
// Moved logic to update_dynamic_hints
|
|
}
|
|
|
|
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,
|
|
}
|
|
|
|
/// Sets the cumulative description of the selected points: if `None` are selected, if `One` is selected, or if `Multiple` are selected.
|
|
/// Applies to any selected points, whether they are anchors or handles; and whether they are from a single shape or across multiple shapes.
|
|
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>();
|
|
|
|
// Check to see if only one manipulator group in a single shape is selected
|
|
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_if_feeds(layer, network_interface)
|
|
.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,
|
|
});
|
|
};
|
|
|
|
// Check to see if multiple manipulator groups are selected
|
|
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>) {
|
|
// Early return if no anchor or no adjacent anchors
|
|
|
|
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 => {
|
|
// Find non-shared segments
|
|
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 => {
|
|
// Use the angle formed by the handle of the shared segment relative to its associated anchor point.
|
|
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,
|
|
) {
|
|
// Condinting based on currently selected segment if it has any one g1 continuous handle
|
|
|
|
let hint_data = match state {
|
|
PathToolFsmState::Ready => {
|
|
// Show point sliding hints only when there is an anchor with colinear handles selected
|
|
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 {
|
|
// TODO: Dynamically show either "Smooth" or "Sharp" based on the current state
|
|
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));
|
|
}
|
|
|
|
if tool_data.molding_segment {
|
|
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)
|
|
} else {
|
|
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::SlidingPoint => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]),
|
|
};
|
|
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
|
}
|