Improve the Path tool's segment editing mode and make hovering manipulators react contextually (#2860)
* Improve path editing mode * Code review * Tidy up UI * Update path selection behaviour * Fix linting * Remove frozen flag * Code review * Fix segment split * Fix deleting segments * Add requred methods in vello overlay context --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
523132da17
commit
668acd3c30
|
|
@ -106,6 +106,7 @@ pub const HIDE_HANDLE_DISTANCE: f64 = 3.;
|
|||
pub const HANDLE_ROTATE_SNAP_ANGLE: f64 = 15.;
|
||||
pub const SEGMENT_INSERTION_DISTANCE: f64 = 5.;
|
||||
pub const SEGMENT_OVERLAY_SIZE: f64 = 10.;
|
||||
pub const SEGMENT_SELECTED_THICKNESS: f64 = 3.;
|
||||
pub const HANDLE_LENGTH_FACTOR: f64 = 0.5;
|
||||
|
||||
// PEN TOOL
|
||||
|
|
|
|||
|
|
@ -212,13 +212,13 @@ pub fn input_mappings() -> Mapping {
|
|||
entry!(KeyDown(Delete); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
|
||||
entry!(KeyDown(Backspace); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
|
||||
entry!(KeyDownNoRepeat(Tab); action_dispatch=PathToolMessage::SwapSelectedHandles),
|
||||
entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { extend_selection: Shift, lasso_select: Control, handle_drag_from_anchor: Alt, drag_restore_handle: Control, molding_in_segment_edit: KeyA }),
|
||||
entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { extend_selection: Shift, lasso_select: Control, handle_drag_from_anchor: Alt, drag_restore_handle: Control, segment_editing_modifier: Control }),
|
||||
entry!(KeyDown(MouseRight); action_dispatch=PathToolMessage::RightClick),
|
||||
entry!(KeyDown(Escape); action_dispatch=PathToolMessage::Escape),
|
||||
entry!(KeyDown(KeyG); action_dispatch=PathToolMessage::GRS { key: KeyG }),
|
||||
entry!(KeyDown(KeyR); action_dispatch=PathToolMessage::GRS { key: KeyR }),
|
||||
entry!(KeyDown(KeyS); action_dispatch=PathToolMessage::GRS { key: KeyS }),
|
||||
entry!(PointerMove; refresh_keys=[KeyC, Space, Control, Shift, Alt], action_dispatch=PathToolMessage::PointerMove { toggle_colinear: KeyC, equidistant: Alt, move_anchor_with_handles: Space, snap_angle: Shift, lock_angle: Control, delete_segment: Alt, break_colinear_molding: Alt }),
|
||||
entry!(PointerMove; refresh_keys=[KeyC, Space, Control, Shift, Alt], action_dispatch=PathToolMessage::PointerMove { toggle_colinear: KeyC, equidistant: Alt, move_anchor_with_handles: Space, snap_angle: Shift, lock_angle: Control, delete_segment: Alt, break_colinear_molding: Alt, segment_editing_modifier: Control }),
|
||||
entry!(KeyDown(Delete); action_dispatch=PathToolMessage::Delete),
|
||||
entry!(KeyDown(KeyA); modifiers=[Accel], action_dispatch=PathToolMessage::SelectAllAnchors),
|
||||
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], canonical, action_dispatch=PathToolMessage::DeselectAllPoints),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use super::utility_functions::overlay_canvas_context;
|
|||
use crate::consts::{
|
||||
ARC_SWEEP_GIZMO_RADIUS, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COLOR_OVERLAY_YELLOW_DULL,
|
||||
COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE,
|
||||
PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER,
|
||||
PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, SEGMENT_SELECTED_THICKNESS,
|
||||
};
|
||||
use crate::messages::prelude::Message;
|
||||
use bezier_rs::{Bezier, Subpath};
|
||||
|
|
@ -460,6 +460,42 @@ impl OverlayContext {
|
|||
self.square(position, None, Some(color_fill), Some(color_stroke));
|
||||
}
|
||||
|
||||
pub fn hover_manipulator_handle(&mut self, position: DVec2, selected: bool) {
|
||||
self.start_dpi_aware_transform();
|
||||
|
||||
let position = position.round() - DVec2::splat(0.5);
|
||||
|
||||
self.render_context.begin_path();
|
||||
self.render_context
|
||||
.arc(position.x, position.y, (MANIPULATOR_GROUP_MARKER_SIZE + 2.) / 2., 0., TAU)
|
||||
.expect("Failed to draw the circle");
|
||||
|
||||
self.render_context.set_fill_style_str(COLOR_OVERLAY_BLUE_50);
|
||||
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE_50);
|
||||
self.render_context.fill();
|
||||
self.render_context.stroke();
|
||||
|
||||
self.render_context.begin_path();
|
||||
self.render_context
|
||||
.arc(position.x, position.y, MANIPULATOR_GROUP_MARKER_SIZE / 2., 0., TAU)
|
||||
.expect("Failed to draw the circle");
|
||||
|
||||
let color_fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE };
|
||||
|
||||
self.render_context.set_fill_style_str(color_fill);
|
||||
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE);
|
||||
self.render_context.fill();
|
||||
self.render_context.stroke();
|
||||
|
||||
self.end_dpi_aware_transform();
|
||||
}
|
||||
|
||||
pub fn hover_manipulator_anchor(&mut self, position: DVec2, selected: bool) {
|
||||
self.square(position, Some(MANIPULATOR_GROUP_MARKER_SIZE + 2.), Some(COLOR_OVERLAY_BLUE_50), Some(COLOR_OVERLAY_BLUE_50));
|
||||
let color_fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE };
|
||||
self.square(position, None, Some(color_fill), Some(COLOR_OVERLAY_BLUE));
|
||||
}
|
||||
|
||||
/// Transforms the canvas context to adjust for DPI scaling
|
||||
///
|
||||
/// Overwrites all existing tranforms. This operation can be reversed with [`Self::reset_transform`].
|
||||
|
|
@ -758,7 +794,7 @@ impl OverlayContext {
|
|||
self.render_context.begin_path();
|
||||
self.bezier_command(bezier, transform, true);
|
||||
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE);
|
||||
self.render_context.set_line_width(4.);
|
||||
self.render_context.set_line_width(SEGMENT_SELECTED_THICKNESS);
|
||||
self.render_context.stroke();
|
||||
|
||||
self.render_context.set_line_width(1.);
|
||||
|
|
@ -772,7 +808,7 @@ impl OverlayContext {
|
|||
self.render_context.begin_path();
|
||||
self.bezier_command(bezier, transform, true);
|
||||
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE_50);
|
||||
self.render_context.set_line_width(4.);
|
||||
self.render_context.set_line_width(SEGMENT_SELECTED_THICKNESS);
|
||||
self.render_context.stroke();
|
||||
|
||||
self.render_context.set_line_width(1.);
|
||||
|
|
|
|||
|
|
@ -293,12 +293,36 @@ impl OverlayContext {
|
|||
.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color.unwrap_or(COLOR_OVERLAY_BLUE)), None, &circle);
|
||||
}
|
||||
|
||||
pub fn hover_manipulator_handle(&mut self, position: DVec2, selected: bool) {
|
||||
let transform = self.get_transform();
|
||||
|
||||
let position = position.round() - DVec2::splat(0.5);
|
||||
|
||||
let circle = kurbo::Circle::new((position.x, position.y), (MANIPULATOR_GROUP_MARKER_SIZE + 2.) / 2.);
|
||||
|
||||
let fill = COLOR_OVERLAY_BLUE_50;
|
||||
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(fill), None, &circle);
|
||||
self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(COLOR_OVERLAY_BLUE_50), None, &circle);
|
||||
|
||||
let inner_circle = kurbo::Circle::new((position.x, position.y), MANIPULATOR_GROUP_MARKER_SIZE / 2.);
|
||||
|
||||
let color_fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE };
|
||||
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(color_fill), None, &circle);
|
||||
self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &inner_circle);
|
||||
}
|
||||
|
||||
pub fn manipulator_anchor(&mut self, position: DVec2, selected: bool, color: Option<&str>) {
|
||||
let color_stroke = color.unwrap_or(COLOR_OVERLAY_BLUE);
|
||||
let color_fill = if selected { color_stroke } else { COLOR_OVERLAY_WHITE };
|
||||
self.square(position, None, Some(color_fill), Some(color_stroke));
|
||||
}
|
||||
|
||||
pub fn hover_manipulator_anchor(&mut self, position: DVec2, selected: bool) {
|
||||
self.square(position, Some(MANIPULATOR_GROUP_MARKER_SIZE + 2.), Some(COLOR_OVERLAY_BLUE_50), Some(COLOR_OVERLAY_BLUE_50));
|
||||
let color_fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE };
|
||||
self.square(position, None, Some(color_fill), Some(COLOR_OVERLAY_BLUE));
|
||||
}
|
||||
|
||||
fn get_transform(&self) -> kurbo::Affine {
|
||||
kurbo::Affine::scale(self.device_pixel_ratio)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -302,9 +302,16 @@ impl ClosestSegment {
|
|||
(midpoint, segment_ids)
|
||||
}
|
||||
|
||||
pub fn adjusted_insert_and_select(&self, shape_editor: &mut ShapeState, responses: &mut VecDeque<Message>, extend_selection: bool) {
|
||||
let (id, _) = self.adjusted_insert(responses);
|
||||
shape_editor.select_anchor_point_by_id(self.layer, id, extend_selection)
|
||||
pub fn adjusted_insert_and_select(&self, shape_editor: &mut ShapeState, responses: &mut VecDeque<Message>, extend_selection: bool, point_mode: bool, is_segment_selected: bool) {
|
||||
let (id, segments) = self.adjusted_insert(responses);
|
||||
if point_mode || is_segment_selected {
|
||||
shape_editor.select_anchor_point_by_id(self.layer, id, extend_selection);
|
||||
}
|
||||
|
||||
if is_segment_selected {
|
||||
let Some(state) = shape_editor.selected_shape_state.get_mut(&self.layer) else { return };
|
||||
segments.iter().for_each(|segment| state.select_segment(*segment));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calculate_perp(&self, document: &DocumentMessageHandler) -> DVec2 {
|
||||
|
|
@ -551,7 +558,7 @@ impl ShapeState {
|
|||
select_threshold: f64,
|
||||
extend_selection: bool,
|
||||
path_overlay_mode: PathOverlayMode,
|
||||
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
) -> Option<Option<SelectedPointsInfo>> {
|
||||
if self.selected_shape_state.is_empty() {
|
||||
return None;
|
||||
|
|
@ -600,18 +607,18 @@ impl ShapeState {
|
|||
mouse_position: DVec2,
|
||||
select_threshold: f64,
|
||||
path_overlay_mode: PathOverlayMode,
|
||||
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
point_editing_mode: bool,
|
||||
) -> Option<(bool, Option<SelectedPointsInfo>)> {
|
||||
if self.selected_shape_state.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !point_editing_mode {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some((layer, manipulator_point_id)) = self.find_nearest_point_indices(network_interface, mouse_position, select_threshold) {
|
||||
// If not point editing mode then only handles are allowed to be dragged
|
||||
if !point_editing_mode && matches!(manipulator_point_id, ManipulatorPointId::Anchor(_)) {
|
||||
return None;
|
||||
}
|
||||
let vector_data = network_interface.compute_modified_vector(layer)?;
|
||||
let point_position = manipulator_point_id.get_position(&vector_data)?;
|
||||
|
||||
|
|
@ -1483,6 +1490,23 @@ impl ShapeState {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn delete_hanging_selected_anchors(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
for (&layer, state) in &self.selected_shape_state {
|
||||
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for point in &state.selected_points {
|
||||
if let ManipulatorPointId::Anchor(anchor) = point {
|
||||
if vector_data.all_connected(*anchor).all(|segment| state.is_segment_selected(segment.segment)) {
|
||||
let modification_type = VectorModificationType::RemovePoint { id: *anchor };
|
||||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn break_path_at_selected_point(&self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
for (&layer, state) in &self.selected_shape_state {
|
||||
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue };
|
||||
|
|
@ -1600,7 +1624,7 @@ impl ShapeState {
|
|||
mouse_position: DVec2,
|
||||
select_threshold: f64,
|
||||
path_overlay_mode: PathOverlayMode,
|
||||
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
) -> Option<(LayerNodeIdentifier, ManipulatorPointId)> {
|
||||
if self.selected_shape_state.is_empty() {
|
||||
return None;
|
||||
|
|
@ -1968,20 +1992,91 @@ impl ShapeState {
|
|||
selection_shape: SelectionShape,
|
||||
selection_change: SelectionChange,
|
||||
path_overlay_mode: PathOverlayMode,
|
||||
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
select_segments: bool,
|
||||
select_points: bool,
|
||||
// Here, "selection mode" represents touched or enclosed, not to be confused with editing modes
|
||||
selection_mode: SelectionMode,
|
||||
) {
|
||||
let (points_inside, segments_inside) = self.get_inside_points_and_segments(
|
||||
network_interface,
|
||||
selection_shape,
|
||||
path_overlay_mode,
|
||||
frontier_handles_info,
|
||||
select_segments,
|
||||
select_points,
|
||||
selection_mode,
|
||||
);
|
||||
|
||||
if selection_change == SelectionChange::Clear {
|
||||
self.deselect_all_points();
|
||||
self.deselect_all_segments();
|
||||
}
|
||||
|
||||
for (layer, points) in points_inside {
|
||||
let Some(state) = self.selected_shape_state.get_mut(&layer) else { continue };
|
||||
let Some(vector_data) = network_interface.compute_modified_vector(layer) else { continue };
|
||||
|
||||
for point in points {
|
||||
match (point, selection_change) {
|
||||
(_, SelectionChange::Shrink) => state.deselect_point(point),
|
||||
(ManipulatorPointId::EndHandle(_) | ManipulatorPointId::PrimaryHandle(_), _) => {
|
||||
let handle = point.as_handle().expect("Handle cannot be converted");
|
||||
if handle.length(&vector_data) > 0. {
|
||||
state.select_point(point);
|
||||
}
|
||||
}
|
||||
(_, _) => state.select_point(point),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (layer, segments) in segments_inside {
|
||||
let Some(state) = self.selected_shape_state.get_mut(&layer) else { continue };
|
||||
match selection_change {
|
||||
SelectionChange::Shrink => segments.iter().for_each(|segment| state.deselect_segment(*segment)),
|
||||
_ => segments.iter().for_each(|segment| state.select_segment(*segment)),
|
||||
}
|
||||
|
||||
// Also select/deselect the endpoints of respective segments
|
||||
let Some(vector_data) = network_interface.compute_modified_vector(layer) else { continue };
|
||||
if !select_points && select_segments {
|
||||
vector_data
|
||||
.segment_bezier_iter()
|
||||
.filter(|(segment, _, _, _)| segments.contains(segment))
|
||||
.for_each(|(_, _, start, end)| match selection_change {
|
||||
SelectionChange::Shrink => {
|
||||
state.deselect_point(ManipulatorPointId::Anchor(start));
|
||||
state.deselect_point(ManipulatorPointId::Anchor(end));
|
||||
}
|
||||
_ => {
|
||||
state.select_point(ManipulatorPointId::Anchor(start));
|
||||
state.select_point(ManipulatorPointId::Anchor(end));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn get_inside_points_and_segments(
|
||||
&mut self,
|
||||
network_interface: &NodeNetworkInterface,
|
||||
selection_shape: SelectionShape,
|
||||
path_overlay_mode: PathOverlayMode,
|
||||
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
select_segments: bool,
|
||||
select_points: bool,
|
||||
// Represents if the box/lasso selection touches or encloses the targets (not to be confused with editing modes).
|
||||
selection_mode: SelectionMode,
|
||||
) -> (HashMap<LayerNodeIdentifier, HashSet<ManipulatorPointId>>, HashMap<LayerNodeIdentifier, HashSet<SegmentId>>) {
|
||||
let selected_points = self.selected_points().cloned().collect::<HashSet<_>>();
|
||||
let selected_segments = selected_segments(network_interface, self);
|
||||
|
||||
for (&layer, state) in &mut self.selected_shape_state {
|
||||
if selection_change == SelectionChange::Clear {
|
||||
state.clear_points();
|
||||
state.clear_segments();
|
||||
}
|
||||
let mut points_inside: HashMap<LayerNodeIdentifier, HashSet<ManipulatorPointId>> = HashMap::new();
|
||||
let mut segments_inside: HashMap<LayerNodeIdentifier, HashSet<SegmentId>> = HashMap::new();
|
||||
|
||||
for &layer in self.selected_shape_state.keys() {
|
||||
let vector_data = network_interface.compute_modified_vector(layer);
|
||||
let Some(vector_data) = vector_data else { continue };
|
||||
let transform = network_interface.document_metadata().transform_to_viewport_if_feeds(layer, network_interface);
|
||||
|
|
@ -1997,7 +2092,7 @@ impl ShapeState {
|
|||
|
||||
let polygon_subpath = if let SelectionShape::Lasso(polygon) = selection_shape {
|
||||
if polygon.len() < 2 {
|
||||
return;
|
||||
return (points_inside, segments_inside);
|
||||
}
|
||||
let polygon: Subpath<PointId> = Subpath::from_anchors_linear(polygon.to_vec(), true);
|
||||
Some(polygon)
|
||||
|
|
@ -2037,10 +2132,7 @@ impl ShapeState {
|
|||
};
|
||||
|
||||
if select {
|
||||
match selection_change {
|
||||
SelectionChange::Shrink => state.deselect_segment(id),
|
||||
_ => state.select_segment(id),
|
||||
}
|
||||
segments_inside.entry(layer).or_default().insert(id);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2057,21 +2149,11 @@ impl ShapeState {
|
|||
.contains_point(transformed_position),
|
||||
};
|
||||
|
||||
if select {
|
||||
let is_visible_handle = is_visible_point(id, &vector_data, path_overlay_mode, frontier_handles_info.clone(), selected_segments.clone(), &selected_points);
|
||||
if select && select_points {
|
||||
let is_visible_handle = is_visible_point(id, &vector_data, path_overlay_mode, frontier_handles_info, selected_segments.clone(), &selected_points);
|
||||
|
||||
if is_visible_handle {
|
||||
match selection_change {
|
||||
SelectionChange::Shrink => state.deselect_point(id),
|
||||
_ => {
|
||||
// Select only the handles which are of nonzero length
|
||||
if let Some(handle) = id.as_handle() {
|
||||
if handle.length(&vector_data) > 0. {
|
||||
state.select_point(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
points_inside.entry(layer).or_default().insert(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2089,13 +2171,12 @@ impl ShapeState {
|
|||
.contains_point(transformed_position),
|
||||
};
|
||||
|
||||
if select {
|
||||
match selection_change {
|
||||
SelectionChange::Shrink => state.deselect_point(ManipulatorPointId::Anchor(id)),
|
||||
_ => state.select_point(ManipulatorPointId::Anchor(id)),
|
||||
}
|
||||
if select && select_points {
|
||||
points_inside.entry(layer).or_default().insert(ManipulatorPointId::Anchor(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(points_inside, segments_inside)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ pub fn is_visible_point(
|
|||
manipulator_point_id: ManipulatorPointId,
|
||||
vector_data: &VectorData,
|
||||
path_overlay_mode: PathOverlayMode,
|
||||
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
selected_segments: Vec<SegmentId>,
|
||||
selected_points: &HashSet<ManipulatorPointId>,
|
||||
) -> bool {
|
||||
|
|
@ -201,7 +201,7 @@ pub fn is_visible_point(
|
|||
warn!("No anchor for selected handle");
|
||||
return false;
|
||||
};
|
||||
let Some(frontier_handles) = &frontier_handles_info else {
|
||||
let Some(frontier_handles) = frontier_handles_info else {
|
||||
warn!("No frontier handles info provided");
|
||||
return false;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ pub enum PathToolMessage {
|
|||
lasso_select: Key,
|
||||
handle_drag_from_anchor: Key,
|
||||
drag_restore_handle: Key,
|
||||
molding_in_segment_edit: Key,
|
||||
segment_editing_modifier: Key,
|
||||
},
|
||||
NudgeSelectedPoints {
|
||||
delta_x: f64,
|
||||
|
|
@ -92,6 +92,7 @@ pub enum PathToolMessage {
|
|||
lock_angle: Key,
|
||||
delete_segment: Key,
|
||||
break_colinear_molding: Key,
|
||||
segment_editing_modifier: Key,
|
||||
},
|
||||
PointerOutsideViewport {
|
||||
equidistant: Key,
|
||||
|
|
@ -101,6 +102,7 @@ pub enum PathToolMessage {
|
|||
lock_angle: Key,
|
||||
delete_segment: Key,
|
||||
break_colinear_molding: Key,
|
||||
segment_editing_modifier: Key,
|
||||
},
|
||||
RightClick,
|
||||
SelectAllAnchors,
|
||||
|
|
@ -119,6 +121,8 @@ pub enum PathToolMessage {
|
|||
UpdateSelectedPointsStatus {
|
||||
overlay_context: OverlayContext,
|
||||
},
|
||||
TogglePointEditing,
|
||||
ToggleSegmentEditing,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, Default, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
|
|
@ -239,14 +243,14 @@ impl LayoutHolder for PathTool {
|
|||
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())
|
||||
.tooltip("Point Editing Mode\n\nShift + click to select both modes.")
|
||||
.on_update(|_| PathToolMessage::TogglePointEditing.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())
|
||||
.tooltip("Segment Editing Mode\n\nShift + click to select both modes.")
|
||||
.on_update(|_| PathToolMessage::ToggleSegmentEditing.into())
|
||||
.widget_holder();
|
||||
|
||||
let path_overlay_mode_widget = RadioInput::new(vec![
|
||||
|
|
@ -399,6 +403,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Path
|
|||
DeleteAndBreakPath,
|
||||
ClosePath,
|
||||
PointerMove,
|
||||
TogglePointEditing,
|
||||
ToggleSegmentEditing
|
||||
),
|
||||
PathToolFsmState::Dragging(_) => actions!(PathToolMessageDiscriminant;
|
||||
Escape,
|
||||
|
|
@ -410,6 +416,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Path
|
|||
BreakPath,
|
||||
DeleteAndBreakPath,
|
||||
SwapSelectedHandles,
|
||||
TogglePointEditing,
|
||||
ToggleSegmentEditing
|
||||
),
|
||||
PathToolFsmState::Drawing { .. } => actions!(PathToolMessageDiscriminant;
|
||||
DoubleClick,
|
||||
|
|
@ -421,6 +429,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Path
|
|||
DeleteAndBreakPath,
|
||||
Escape,
|
||||
RightClick,
|
||||
TogglePointEditing,
|
||||
ToggleSegmentEditing
|
||||
),
|
||||
PathToolFsmState::SlidingPoint => actions!(PathToolMessageDiscriminant;
|
||||
PointerMove,
|
||||
|
|
@ -500,10 +510,12 @@ struct PathToolData {
|
|||
snap_cache: SnapCache,
|
||||
double_click_handled: bool,
|
||||
delete_segment_pressed: bool,
|
||||
segment_editing_modifier: bool,
|
||||
multiple_toggle_pressed: bool,
|
||||
auto_panning: AutoPanning,
|
||||
saved_points_before_anchor_select_toggle: Vec<ManipulatorPointId>,
|
||||
select_anchor_toggled: bool,
|
||||
saved_points_before_handle_drag: Vec<ManipulatorPointId>,
|
||||
saved_selection_before_handle_drag: HashMap<LayerNodeIdentifier, (HashSet<ManipulatorPointId>, HashSet<SegmentId>)>,
|
||||
handle_drag_toggle: bool,
|
||||
saved_points_before_anchor_convert_smooth_sharp: HashSet<ManipulatorPointId>,
|
||||
last_click_time: u64,
|
||||
|
|
@ -650,7 +662,7 @@ impl PathToolData {
|
|||
lasso_select: bool,
|
||||
handle_drag_from_anchor: bool,
|
||||
drag_zero_handle: bool,
|
||||
molding_in_segment_edit: bool,
|
||||
segment_editing_modifier: bool,
|
||||
path_overlay_mode: PathOverlayMode,
|
||||
segment_editing_mode: bool,
|
||||
point_editing_mode: bool,
|
||||
|
|
@ -667,7 +679,13 @@ impl PathToolData {
|
|||
|
||||
self.last_click_time = input.time;
|
||||
|
||||
let old_selection = shape_editor.selected_points().cloned().collect::<Vec<_>>();
|
||||
let mut old_selection = HashMap::new();
|
||||
|
||||
for (layer, state) in &shape_editor.selected_shape_state {
|
||||
let selected_points = state.selected_points().collect::<HashSet<_>>();
|
||||
let selected_segments = state.selected_segments().collect::<HashSet<_>>();
|
||||
old_selection.insert(*layer, (selected_points, selected_segments));
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
@ -676,7 +694,7 @@ impl PathToolData {
|
|||
input.mouse.position,
|
||||
SELECTION_THRESHOLD,
|
||||
path_overlay_mode,
|
||||
self.frontier_handles_info.clone(),
|
||||
&self.frontier_handles_info,
|
||||
point_editing_mode,
|
||||
) {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
|
@ -694,7 +712,7 @@ impl PathToolData {
|
|||
SELECTION_THRESHOLD,
|
||||
extend_selection,
|
||||
path_overlay_mode,
|
||||
self.frontier_handles_info.clone(),
|
||||
&self.frontier_handles_info,
|
||||
) {
|
||||
selection_info = updated_selection_info;
|
||||
}
|
||||
|
|
@ -712,7 +730,7 @@ impl PathToolData {
|
|||
}
|
||||
}
|
||||
if dragging_only_handles && !self.handle_drag_toggle && !old_selection.is_empty() {
|
||||
self.saved_points_before_handle_drag = old_selection;
|
||||
self.saved_selection_before_handle_drag = old_selection;
|
||||
}
|
||||
|
||||
if handle_drag_from_anchor {
|
||||
|
|
@ -769,7 +787,7 @@ impl PathToolData {
|
|||
|
||||
self.set_ghost_outline(shape_editor, document);
|
||||
|
||||
if segment_editing_mode && !molding_in_segment_edit {
|
||||
if segment_editing_mode && !segment_editing_modifier {
|
||||
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));
|
||||
|
|
@ -786,6 +804,8 @@ impl PathToolData {
|
|||
if let Some(selected_shape_state) = shape_editor.selected_shape_state.get_mut(&layer) {
|
||||
selected_shape_state.select_segment(segment_id);
|
||||
}
|
||||
|
||||
// TODO: If the segment connected to one of the endpoints is also selected then select that point
|
||||
}
|
||||
|
||||
self.drag_start_pos = input.mouse.position;
|
||||
|
|
@ -1130,11 +1150,13 @@ impl PathToolData {
|
|||
tangent_vector.try_normalize()
|
||||
}
|
||||
|
||||
fn update_closest_segment(&mut self, shape_editor: &mut ShapeState, position: DVec2, document: &DocumentMessageHandler, path_overlay_mode: PathOverlayMode) {
|
||||
fn update_closest_segment(&mut self, shape_editor: &mut ShapeState, position: DVec2, document: &DocumentMessageHandler, path_overlay_mode: PathOverlayMode, point_editing_mode: bool) {
|
||||
// Check if there is no point nearby
|
||||
// If the point mode is deactivated then don't override closest segment even if there is a closer point
|
||||
if shape_editor
|
||||
.find_nearest_visible_point_indices(&document.network_interface, position, SELECTION_THRESHOLD, path_overlay_mode, self.frontier_handles_info.clone())
|
||||
.find_nearest_visible_point_indices(&document.network_interface, position, SELECTION_THRESHOLD, path_overlay_mode, &self.frontier_handles_info)
|
||||
.is_some()
|
||||
&& point_editing_mode
|
||||
{
|
||||
self.segment = None;
|
||||
}
|
||||
|
|
@ -1464,7 +1486,7 @@ impl Fsm for PathToolFsmState {
|
|||
) -> Self {
|
||||
let ToolActionMessageContext { document, input, shape_editor, .. } = tool_action_data;
|
||||
|
||||
update_dynamic_hints(self, responses, shape_editor, document, tool_data, tool_options);
|
||||
update_dynamic_hints(self, responses, shape_editor, document, tool_data, tool_options, input.mouse.position);
|
||||
|
||||
let ToolMessage::Path(event) = event else { return self };
|
||||
|
||||
|
|
@ -1493,8 +1515,96 @@ impl Fsm for PathToolFsmState {
|
|||
|
||||
self
|
||||
}
|
||||
(_, PathToolMessage::TogglePointEditing) => {
|
||||
// Clicked on the point edit mode button
|
||||
let point_edit = tool_options.path_editing_mode.point_editing_mode;
|
||||
let segment_edit = tool_options.path_editing_mode.segment_editing_mode;
|
||||
let multiple_toggle = tool_data.multiple_toggle_pressed;
|
||||
|
||||
if point_edit && !segment_edit {
|
||||
return self;
|
||||
}
|
||||
|
||||
match (multiple_toggle, point_edit) {
|
||||
(true, true) => {
|
||||
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: false }));
|
||||
}
|
||||
(true, false) => {
|
||||
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: true }));
|
||||
}
|
||||
(_, _) => {
|
||||
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: true }));
|
||||
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: false }));
|
||||
|
||||
// Select all of the end points of selected segments
|
||||
let selected_layers = shape_editor.selected_layers().cloned().collect::<Vec<_>>();
|
||||
|
||||
for layer in selected_layers {
|
||||
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue };
|
||||
|
||||
let selected_state = shape_editor.selected_shape_state.entry(layer).or_default();
|
||||
|
||||
for (segment, _, start, end) in vector_data.segment_bezier_iter() {
|
||||
if selected_state.is_segment_selected(segment) {
|
||||
selected_state.select_point(ManipulatorPointId::Anchor(start));
|
||||
selected_state.select_point(ManipulatorPointId::Anchor(end));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deselect all of the segments
|
||||
shape_editor.deselect_all_segments();
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
(_, PathToolMessage::ToggleSegmentEditing) => {
|
||||
// Clicked on the point edit mode button
|
||||
let segment_edit = tool_options.path_editing_mode.segment_editing_mode;
|
||||
let point_edit = tool_options.path_editing_mode.point_editing_mode;
|
||||
|
||||
let multiple_toggle = tool_data.multiple_toggle_pressed;
|
||||
|
||||
if segment_edit && !point_edit {
|
||||
return self;
|
||||
}
|
||||
|
||||
match (multiple_toggle, segment_edit) {
|
||||
(true, true) => {
|
||||
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: false }));
|
||||
}
|
||||
(true, false) => {
|
||||
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: true }));
|
||||
}
|
||||
(_, _) => {
|
||||
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: false }));
|
||||
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: true }));
|
||||
|
||||
// Select all the segments which have both of the ends selected
|
||||
let selected_layers = shape_editor.selected_layers().cloned().collect::<Vec<_>>();
|
||||
|
||||
for layer in selected_layers {
|
||||
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue };
|
||||
|
||||
let selected_state = shape_editor.selected_shape_state.entry(layer).or_default();
|
||||
|
||||
for (segment, _, start, end) in vector_data.segment_bezier_iter() {
|
||||
let first_selected = selected_state.is_point_selected(ManipulatorPointId::Anchor(start));
|
||||
let second_selected = selected_state.is_point_selected(ManipulatorPointId::Anchor(end));
|
||||
if first_selected && second_selected {
|
||||
selected_state.select_segment(segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
(_, PathToolMessage::Overlays(mut overlay_context)) => {
|
||||
if matches!(self, Self::Dragging(_)) {
|
||||
// Set this to show ghost line only if drag actually happened
|
||||
if matches!(self, Self::Dragging(_)) && tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD {
|
||||
for (outline, layer) in &tool_data.ghost_outline {
|
||||
let transform = document.metadata().transform_to_viewport(*layer);
|
||||
overlay_context.outline(outline.iter(), transform, Some(COLOR_OVERLAY_GRAY));
|
||||
|
|
@ -1563,10 +1673,47 @@ impl Fsm for PathToolFsmState {
|
|||
|
||||
match self {
|
||||
Self::Ready => {
|
||||
tool_data.update_closest_segment(shape_editor, input.mouse.position, document, tool_options.path_overlay_mode);
|
||||
tool_data.update_closest_segment(
|
||||
shape_editor,
|
||||
input.mouse.position,
|
||||
document,
|
||||
tool_options.path_overlay_mode,
|
||||
tool_options.path_editing_mode.point_editing_mode,
|
||||
);
|
||||
|
||||
// If there exists an underlying anchor, we show a hover overlay
|
||||
(|| {
|
||||
if !tool_options.path_editing_mode.point_editing_mode {
|
||||
return;
|
||||
}
|
||||
|
||||
let nearest_visible_point_indices = 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,
|
||||
);
|
||||
|
||||
let Some((layer, manipulator_point_id)) = nearest_visible_point_indices else { return };
|
||||
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { return };
|
||||
let Some(position) = manipulator_point_id.get_position(&vector_data) else {
|
||||
error!("No position for hovered point");
|
||||
return;
|
||||
};
|
||||
|
||||
let transform = document.metadata().transform_to_viewport(layer);
|
||||
let position = transform.transform_point2(position);
|
||||
let selected = shape_editor.selected_shape_state.entry(layer).or_default().is_point_selected(manipulator_point_id);
|
||||
|
||||
match manipulator_point_id {
|
||||
ManipulatorPointId::Anchor(_) => overlay_context.hover_manipulator_anchor(position, selected),
|
||||
_ => overlay_context.hover_manipulator_handle(position, selected),
|
||||
}
|
||||
})();
|
||||
|
||||
if let Some(closest_segment) = &tool_data.segment {
|
||||
if tool_options.path_editing_mode.segment_editing_mode {
|
||||
if tool_options.path_editing_mode.segment_editing_mode && !tool_data.segment_editing_modifier {
|
||||
let transform = document.metadata().transform_to_viewport_if_feeds(closest_segment.layer(), &document.network_interface);
|
||||
|
||||
overlay_context.outline_overlay_bezier(closest_segment.bezier(), transform);
|
||||
|
|
@ -1584,6 +1731,7 @@ impl Fsm for PathToolFsmState {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
// We want this overlay also when in segment_editing_mode
|
||||
let perp = closest_segment.calculate_perp(document);
|
||||
let point = closest_segment.closest_point(document.metadata(), &document.network_interface);
|
||||
|
||||
|
|
@ -1645,14 +1793,70 @@ impl Fsm for PathToolFsmState {
|
|||
};
|
||||
|
||||
let quad = tool_data.selection_quad(document.metadata());
|
||||
let polygon = &tool_data.lasso_polygon;
|
||||
|
||||
let select_segments = tool_options.path_editing_mode.segment_editing_mode;
|
||||
let select_points = tool_options.path_editing_mode.point_editing_mode;
|
||||
let (points_inside, segments_inside) = match selection_shape {
|
||||
SelectionShapeType::Box => {
|
||||
let previous_mouse = document.metadata().document_to_viewport.transform_point2(tool_data.previous_mouse_position);
|
||||
let bbox = [tool_data.drag_start_pos, previous_mouse];
|
||||
shape_editor.get_inside_points_and_segments(
|
||||
&document.network_interface,
|
||||
SelectionShape::Box(bbox),
|
||||
tool_options.path_overlay_mode,
|
||||
&tool_data.frontier_handles_info,
|
||||
select_segments,
|
||||
select_points,
|
||||
selection_mode,
|
||||
)
|
||||
}
|
||||
SelectionShapeType::Lasso => shape_editor.get_inside_points_and_segments(
|
||||
&document.network_interface,
|
||||
SelectionShape::Lasso(&tool_data.lasso_polygon),
|
||||
tool_options.path_overlay_mode,
|
||||
&tool_data.frontier_handles_info,
|
||||
select_segments,
|
||||
select_points,
|
||||
selection_mode,
|
||||
),
|
||||
};
|
||||
|
||||
for (layer, points) in points_inside {
|
||||
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue };
|
||||
|
||||
for point in points {
|
||||
let Some(position) = point.get_position(&vector_data) else { continue };
|
||||
|
||||
let transform = document.metadata().transform_to_viewport(layer);
|
||||
let position = transform.transform_point2(position);
|
||||
|
||||
let selected = shape_editor.selected_shape_state.entry(layer).or_default().is_point_selected(point);
|
||||
|
||||
match point {
|
||||
ManipulatorPointId::Anchor(_) => overlay_context.hover_manipulator_anchor(position, selected),
|
||||
_ => overlay_context.hover_manipulator_handle(position, selected),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (layer, segments) in segments_inside {
|
||||
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue };
|
||||
|
||||
let transform = document.metadata().transform_to_viewport_if_feeds(layer, &document.network_interface);
|
||||
|
||||
for (segment, bezier, _, _) in vector_data.segment_bezier_iter() {
|
||||
if segments.contains(&segment) {
|
||||
overlay_context.outline_overlay_bezier(bezier, transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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::Lasso, SelectionMode::Enclosed, _) => overlay_context.dashed_polygon(&tool_data.lasso_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::Lasso, _, _) => overlay_context.polygon(&tool_data.lasso_polygon, None, fill_color),
|
||||
(SelectionShapeType::Box, _, _) => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1698,14 +1902,14 @@ impl Fsm for PathToolFsmState {
|
|||
lasso_select,
|
||||
handle_drag_from_anchor,
|
||||
drag_restore_handle,
|
||||
molding_in_segment_edit,
|
||||
segment_editing_modifier,
|
||||
},
|
||||
) => {
|
||||
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);
|
||||
let segment_editing_modifier = input.keyboard.get(segment_editing_modifier as usize);
|
||||
|
||||
tool_data.selection_mode = None;
|
||||
tool_data.lasso_polygon.clear();
|
||||
|
|
@ -1719,7 +1923,7 @@ impl Fsm for PathToolFsmState {
|
|||
lasso_select,
|
||||
handle_drag_from_anchor,
|
||||
drag_zero_handle,
|
||||
molding_in_segment_edit,
|
||||
segment_editing_modifier,
|
||||
tool_options.path_overlay_mode,
|
||||
tool_options.path_editing_mode.segment_editing_mode,
|
||||
tool_options.path_editing_mode.point_editing_mode,
|
||||
|
|
@ -1735,6 +1939,7 @@ impl Fsm for PathToolFsmState {
|
|||
lock_angle,
|
||||
delete_segment,
|
||||
break_colinear_molding,
|
||||
segment_editing_modifier,
|
||||
},
|
||||
) => {
|
||||
tool_data.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
|
||||
|
|
@ -1758,6 +1963,7 @@ impl Fsm for PathToolFsmState {
|
|||
lock_angle,
|
||||
delete_segment,
|
||||
break_colinear_molding,
|
||||
segment_editing_modifier,
|
||||
}
|
||||
.into(),
|
||||
PathToolMessage::PointerMove {
|
||||
|
|
@ -1768,6 +1974,7 @@ impl Fsm for PathToolFsmState {
|
|||
lock_angle,
|
||||
delete_segment,
|
||||
break_colinear_molding,
|
||||
segment_editing_modifier,
|
||||
}
|
||||
.into(),
|
||||
];
|
||||
|
|
@ -1785,12 +1992,13 @@ impl Fsm for PathToolFsmState {
|
|||
lock_angle,
|
||||
delete_segment,
|
||||
break_colinear_molding,
|
||||
segment_editing_modifier,
|
||||
},
|
||||
) => {
|
||||
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) {
|
||||
if !tool_data.saved_selection_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;
|
||||
}
|
||||
|
||||
|
|
@ -1870,6 +2078,7 @@ impl Fsm for PathToolFsmState {
|
|||
lock_angle,
|
||||
delete_segment,
|
||||
break_colinear_molding,
|
||||
segment_editing_modifier,
|
||||
}
|
||||
.into(),
|
||||
PathToolMessage::PointerMove {
|
||||
|
|
@ -1880,6 +2089,7 @@ impl Fsm for PathToolFsmState {
|
|||
lock_angle,
|
||||
delete_segment,
|
||||
break_colinear_molding,
|
||||
segment_editing_modifier,
|
||||
}
|
||||
.into(),
|
||||
];
|
||||
|
|
@ -1891,8 +2101,18 @@ impl Fsm for PathToolFsmState {
|
|||
tool_data.slide_point(input.mouse.position, responses, &document.network_interface, shape_editor);
|
||||
PathToolFsmState::SlidingPoint
|
||||
}
|
||||
(PathToolFsmState::Ready, PathToolMessage::PointerMove { delete_segment, .. }) => {
|
||||
(
|
||||
PathToolFsmState::Ready,
|
||||
PathToolMessage::PointerMove {
|
||||
delete_segment,
|
||||
segment_editing_modifier,
|
||||
snap_angle,
|
||||
..
|
||||
},
|
||||
) => {
|
||||
tool_data.delete_segment_pressed = input.keyboard.get(delete_segment as usize);
|
||||
tool_data.segment_editing_modifier = input.keyboard.get(segment_editing_modifier as usize);
|
||||
tool_data.multiple_toggle_pressed = input.keyboard.get(snap_angle as usize);
|
||||
tool_data.saved_points_before_anchor_convert_smooth_sharp.clear();
|
||||
tool_data.adjacent_anchor_offset = None;
|
||||
tool_data.stored_selection = None;
|
||||
|
|
@ -1944,6 +2164,7 @@ impl Fsm for PathToolFsmState {
|
|||
lock_angle,
|
||||
delete_segment,
|
||||
break_colinear_molding,
|
||||
segment_editing_modifier,
|
||||
},
|
||||
) => {
|
||||
// Auto-panning
|
||||
|
|
@ -1956,6 +2177,7 @@ impl Fsm for PathToolFsmState {
|
|||
lock_angle,
|
||||
delete_segment,
|
||||
break_colinear_molding,
|
||||
segment_editing_modifier,
|
||||
}
|
||||
.into(),
|
||||
PathToolMessage::PointerMove {
|
||||
|
|
@ -1966,6 +2188,7 @@ impl Fsm for PathToolFsmState {
|
|||
lock_angle,
|
||||
delete_segment,
|
||||
break_colinear_molding,
|
||||
segment_editing_modifier,
|
||||
}
|
||||
.into(),
|
||||
];
|
||||
|
|
@ -2003,8 +2226,9 @@ impl Fsm for PathToolFsmState {
|
|||
SelectionShape::Box(bbox),
|
||||
selection_change,
|
||||
tool_options.path_overlay_mode,
|
||||
tool_data.frontier_handles_info.clone(),
|
||||
&tool_data.frontier_handles_info,
|
||||
tool_options.path_editing_mode.segment_editing_mode,
|
||||
tool_options.path_editing_mode.point_editing_mode,
|
||||
selection_mode,
|
||||
);
|
||||
}
|
||||
|
|
@ -2013,8 +2237,9 @@ impl Fsm for PathToolFsmState {
|
|||
SelectionShape::Lasso(&tool_data.lasso_polygon),
|
||||
selection_change,
|
||||
tool_options.path_overlay_mode,
|
||||
tool_data.frontier_handles_info.clone(),
|
||||
&tool_data.frontier_handles_info,
|
||||
tool_options.path_editing_mode.segment_editing_mode,
|
||||
tool_options.path_editing_mode.point_editing_mode,
|
||||
selection_mode,
|
||||
),
|
||||
}
|
||||
|
|
@ -2028,9 +2253,14 @@ impl Fsm for PathToolFsmState {
|
|||
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();
|
||||
for (layer, (selected_points, selected_segments)) in &tool_data.saved_selection_before_handle_drag {
|
||||
let Some(state) = shape_editor.selected_shape_state.get_mut(&layer) else { continue };
|
||||
selected_points.iter().for_each(|point| state.select_point(*point));
|
||||
selected_segments.iter().for_each(|segment| state.select_segment(*segment));
|
||||
}
|
||||
|
||||
tool_data.saved_selection_before_handle_drag.clear();
|
||||
tool_data.handle_drag_toggle = false;
|
||||
}
|
||||
tool_data.molding_info = None;
|
||||
|
|
@ -2092,8 +2322,9 @@ impl Fsm for PathToolFsmState {
|
|||
SelectionShape::Box(bbox),
|
||||
select_kind,
|
||||
tool_options.path_overlay_mode,
|
||||
tool_data.frontier_handles_info.clone(),
|
||||
&tool_data.frontier_handles_info,
|
||||
tool_options.path_editing_mode.segment_editing_mode,
|
||||
tool_options.path_editing_mode.point_editing_mode,
|
||||
selection_mode,
|
||||
);
|
||||
}
|
||||
|
|
@ -2102,8 +2333,9 @@ impl Fsm for PathToolFsmState {
|
|||
SelectionShape::Lasso(&tool_data.lasso_polygon),
|
||||
select_kind,
|
||||
tool_options.path_overlay_mode,
|
||||
tool_data.frontier_handles_info.clone(),
|
||||
&tool_data.frontier_handles_info,
|
||||
tool_options.path_editing_mode.segment_editing_mode,
|
||||
tool_options.path_editing_mode.point_editing_mode,
|
||||
selection_mode,
|
||||
),
|
||||
}
|
||||
|
|
@ -2123,20 +2355,33 @@ impl Fsm for PathToolFsmState {
|
|||
input.mouse.position,
|
||||
SELECTION_THRESHOLD,
|
||||
tool_options.path_overlay_mode,
|
||||
tool_data.frontier_handles_info.clone(),
|
||||
&tool_data.frontier_handles_info,
|
||||
);
|
||||
|
||||
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 {
|
||||
let point_mode = tool_options.path_editing_mode.point_editing_mode;
|
||||
// If segment mode and the insertion modifier is pressed or it is in point editing mode
|
||||
|
||||
if !drag_occurred && !tool_data.molding_segment && ((point_mode && !segment_mode) || (segment_mode && tool_data.segment_editing_modifier)) {
|
||||
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);
|
||||
let is_segment_selected = shape_editor
|
||||
.selected_shape_state
|
||||
.get(&segment.layer())
|
||||
.map_or(false, |state| state.is_segment_selected(segment.segment()));
|
||||
|
||||
segment.adjusted_insert_and_select(shape_editor, responses, extend_selection, point_mode, is_segment_selected);
|
||||
tool_data.segment = None;
|
||||
tool_data.molding_info = None;
|
||||
tool_data.molding_segment = false;
|
||||
tool_data.temporary_adjacent_handles_while_molding = None;
|
||||
return PathToolFsmState::Ready;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2147,8 +2392,9 @@ impl Fsm for PathToolFsmState {
|
|||
}
|
||||
|
||||
let segment_mode = tool_options.path_editing_mode.segment_editing_mode;
|
||||
let point_mode = tool_options.path_editing_mode.point_editing_mode;
|
||||
|
||||
if let Some((layer, nearest_point)) = nearest_point {
|
||||
if let (Some((layer, nearest_point)), true) = (nearest_point, point_mode) {
|
||||
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 {
|
||||
|
|
@ -2182,6 +2428,17 @@ impl Fsm for PathToolFsmState {
|
|||
.entry(nearest_segment.layer())
|
||||
.or_default()
|
||||
.deselect_segment(nearest_segment.segment());
|
||||
|
||||
// If in segment editing mode only, and upon deselecting a segment, we deselect both of its anchors
|
||||
if segment_mode && !point_mode {
|
||||
nearest_segment.points().iter().for_each(|point_id| {
|
||||
shape_editor
|
||||
.selected_shape_state
|
||||
.entry(nearest_segment.layer())
|
||||
.or_default()
|
||||
.deselect_point(ManipulatorPointId::Anchor(*point_id));
|
||||
});
|
||||
}
|
||||
} else {
|
||||
shape_editor.selected_shape_state.entry(nearest_segment.layer()).or_default().select_segment(nearest_segment.segment());
|
||||
}
|
||||
|
|
@ -2196,6 +2453,22 @@ impl Fsm for PathToolFsmState {
|
|||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
}
|
||||
|
||||
// If only in segment select mode, we also select all of the endpoints of selected segments
|
||||
let point_mode = tool_options.path_editing_mode.point_editing_mode;
|
||||
if !point_mode {
|
||||
let [start, end] = nearest_segment.points();
|
||||
shape_editor
|
||||
.selected_shape_state
|
||||
.entry(nearest_segment.layer())
|
||||
.or_default()
|
||||
.select_point(ManipulatorPointId::Anchor(start));
|
||||
shape_editor
|
||||
.selected_shape_state
|
||||
.entry(nearest_segment.layer())
|
||||
.or_default()
|
||||
.select_point(ManipulatorPointId::Anchor(end));
|
||||
}
|
||||
}
|
||||
// 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 {
|
||||
|
|
@ -2209,9 +2482,15 @@ impl Fsm for PathToolFsmState {
|
|||
|
||||
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);
|
||||
shape_editor.deselect_all_segments();
|
||||
|
||||
tool_data.saved_points_before_handle_drag.clear();
|
||||
for (layer, (selected_points, selected_segments)) in &tool_data.saved_selection_before_handle_drag {
|
||||
let Some(state) = shape_editor.selected_shape_state.get_mut(&layer) else { continue };
|
||||
selected_points.iter().for_each(|point| state.select_point(*point));
|
||||
selected_segments.iter().for_each(|segment| state.select_segment(*segment));
|
||||
}
|
||||
|
||||
tool_data.saved_selection_before_handle_drag.clear();
|
||||
tool_data.handle_drag_toggle = false;
|
||||
}
|
||||
|
||||
|
|
@ -2243,8 +2522,17 @@ impl Fsm for PathToolFsmState {
|
|||
(_, PathToolMessage::Delete) => {
|
||||
// Delete the selected points and clean up overlays
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
let point_mode = tool_options.path_editing_mode.point_editing_mode;
|
||||
let segment_mode = tool_options.path_editing_mode.segment_editing_mode;
|
||||
|
||||
let only_segment_mode = segment_mode && !point_mode;
|
||||
|
||||
shape_editor.delete_selected_segments(document, responses);
|
||||
shape_editor.delete_selected_points(document, responses);
|
||||
if only_segment_mode {
|
||||
shape_editor.delete_hanging_selected_anchors(document, responses);
|
||||
} else {
|
||||
shape_editor.delete_selected_points(document, responses);
|
||||
}
|
||||
responses.add(PathToolMessage::SelectionChanged);
|
||||
|
||||
PathToolFsmState::Ready
|
||||
|
|
@ -2695,6 +2983,7 @@ fn update_dynamic_hints(
|
|||
document: &DocumentMessageHandler,
|
||||
tool_data: &PathToolData,
|
||||
tool_options: &PathToolOptions,
|
||||
position: DVec2,
|
||||
) {
|
||||
// Condinting based on currently selected segment if it has any one g1 continuous handle
|
||||
|
||||
|
|
@ -2729,26 +3018,55 @@ fn update_dynamic_hints(
|
|||
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) => {
|
||||
let segment_edit = tool_options.path_editing_mode.segment_editing_mode;
|
||||
let point_edit = tool_options.path_editing_mode.point_editing_mode;
|
||||
|
||||
let hovering_segment = tool_data.segment.is_some();
|
||||
let hovering_point = shape_editor
|
||||
.find_nearest_visible_point_indices(
|
||||
&document.network_interface,
|
||||
position,
|
||||
SELECTION_THRESHOLD,
|
||||
tool_options.path_overlay_mode,
|
||||
&tool_data.frontier_handles_info,
|
||||
)
|
||||
.is_some();
|
||||
|
||||
let mut hint_data = if hovering_segment {
|
||||
if segment_edit {
|
||||
// Hovering a segment in segment editing mode
|
||||
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")]),
|
||||
HintGroup(vec![HintInfo::keys_and_mouse([Key::Control], MouseMotion::Lmb, "Insert Point on Segment")]),
|
||||
HintGroup(vec![HintInfo::keys_and_mouse([Key::Control], MouseMotion::LmbDrag, "Mold Segment")]),
|
||||
]
|
||||
}
|
||||
(true, false) => {
|
||||
} else {
|
||||
// Hovering a segment in point editing mode
|
||||
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()]),
|
||||
]
|
||||
} else if hovering_point {
|
||||
if point_edit {
|
||||
// Hovering over a point in point editing mode
|
||||
vec![HintGroup(vec![
|
||||
HintInfo::mouse(MouseMotion::Lmb, "Select Point"),
|
||||
HintInfo::keys([Key::Shift], "Extend").prepend_plus(),
|
||||
])]
|
||||
} else {
|
||||
// Hovering over a point in segment selection mode (will select a nearby segment)
|
||||
vec![HintGroup(vec![
|
||||
HintInfo::mouse(MouseMotion::Lmb, "Select Segment"),
|
||||
HintInfo::keys([Key::Shift], "Extend").prepend_plus(),
|
||||
])]
|
||||
}
|
||||
} else {
|
||||
vec![HintGroup(vec![
|
||||
HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"),
|
||||
HintInfo::keys([Key::Control], "Lasso").prepend_plus(),
|
||||
])]
|
||||
};
|
||||
|
||||
if at_least_one_anchor_selected {
|
||||
|
|
|
|||
Loading…
Reference in New Issue