Add segment editing mode to the Path tool (#2712)

* Segment select mode upto dragging

* Lasso select for segment editing

* Formatting

* Compatibility with point selection mode

* Add delete segment support and drawing from inside of shape

* Add GRS support for selected segments

* Cleanup and add dynamic hints

* Fix double click behaviour and overlays

* Format code

* Fix merge

* Fix Lint

* Fix formatting

* Fix lasso bug

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Adesh Gupta 2025-06-30 11:49:54 +05:30 committed by GitHub
parent a4fbea9193
commit 391ed34a30
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 613 additions and 187 deletions

View file

@ -36,9 +36,9 @@ thiserror = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
bezier-rs = { workspace = true } bezier-rs = { workspace = true }
kurbo = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
glam = { workspace = true } glam = { workspace = true }
kurbo = { workspace = true }
derivative = { workspace = true } derivative = { workspace = true }
specta = { workspace = true } specta = { workspace = true }
dyn-any = { workspace = true } dyn-any = { workspace = true }

View file

@ -102,7 +102,7 @@ pub const MANIPULATOR_GROUP_MARKER_SIZE: f64 = 6.;
pub const SELECTION_THRESHOLD: f64 = 10.; pub const SELECTION_THRESHOLD: f64 = 10.;
pub const HIDE_HANDLE_DISTANCE: f64 = 3.; pub const HIDE_HANDLE_DISTANCE: f64 = 3.;
pub const HANDLE_ROTATE_SNAP_ANGLE: f64 = 15.; pub const HANDLE_ROTATE_SNAP_ANGLE: f64 = 15.;
pub const SEGMENT_INSERTION_DISTANCE: f64 = 8.; pub const SEGMENT_INSERTION_DISTANCE: f64 = 5.;
pub const SEGMENT_OVERLAY_SIZE: f64 = 10.; pub const SEGMENT_OVERLAY_SIZE: f64 = 10.;
pub const HANDLE_LENGTH_FACTOR: f64 = 0.5; pub const HANDLE_LENGTH_FACTOR: f64 = 0.5;
@ -133,6 +133,7 @@ pub const SCALE_EFFECT: f64 = 0.5;
// COLORS // COLORS
pub const COLOR_OVERLAY_BLUE: &str = "#00a8ff"; pub const COLOR_OVERLAY_BLUE: &str = "#00a8ff";
pub const COLOR_OVERLAY_BLUE_50: &str = "rgba(0, 168, 255, 0.5)";
pub const COLOR_OVERLAY_YELLOW: &str = "#ffc848"; pub const COLOR_OVERLAY_YELLOW: &str = "#ffc848";
pub const COLOR_OVERLAY_GREEN: &str = "#63ce63"; pub const COLOR_OVERLAY_GREEN: &str = "#63ce63";
pub const COLOR_OVERLAY_RED: &str = "#ef5454"; pub const COLOR_OVERLAY_RED: &str = "#ef5454";

View file

@ -212,7 +212,7 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(Delete); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath), entry!(KeyDown(Delete); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
entry!(KeyDown(Backspace); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath), entry!(KeyDown(Backspace); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
entry!(KeyDownNoRepeat(Tab); action_dispatch=PathToolMessage::SwapSelectedHandles), 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 }), 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(MouseRight); action_dispatch=PathToolMessage::RightClick), entry!(KeyDown(MouseRight); action_dispatch=PathToolMessage::RightClick),
entry!(KeyDown(Escape); action_dispatch=PathToolMessage::Escape), entry!(KeyDown(Escape); action_dispatch=PathToolMessage::Escape),
entry!(KeyDown(KeyG); action_dispatch=PathToolMessage::GRS { key: KeyG }), entry!(KeyDown(KeyG); action_dispatch=PathToolMessage::GRS { key: KeyG }),

View file

@ -124,8 +124,19 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
overlay_context.outline_vector(&vector_data, transform); overlay_context.outline_vector(&vector_data, transform);
} }
// Get the selected segments and then add a bold line overlay on them
for (segment_id, bezier, _, _) in vector_data.segment_bezier_iter() {
let Some(selected_shape_state) = shape_editor.selected_shape_state.get_mut(&layer) else {
continue;
};
if selected_shape_state.is_segment_selected(segment_id) {
overlay_context.outline_select_bezier(bezier, transform);
}
}
let selected = shape_editor.selected_shape_state.get(&layer); let selected = shape_editor.selected_shape_state.get(&layer);
let is_selected = |point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_selected(point)); let is_selected = |point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_point_selected(point));
if display_handles { if display_handles {
let opposite_handles_data: Vec<(PointId, SegmentId)> = shape_editor.selected_points().filter_map(|point_id| vector_data.adjacent_segment(point_id)).collect(); let opposite_handles_data: Vec<(PointId, SegmentId)> = shape_editor.selected_points().filter_map(|point_id| vector_data.adjacent_segment(point_id)).collect();
@ -187,7 +198,7 @@ pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &
//let document_to_viewport = document.navigation_handler.calculate_offset_transform(overlay_context.size / 2., &document.document_ptz); //let document_to_viewport = document.navigation_handler.calculate_offset_transform(overlay_context.size / 2., &document.document_ptz);
let transform = document.metadata().transform_to_viewport(layer); let transform = document.metadata().transform_to_viewport(layer);
let selected = shape_editor.selected_shape_state.get(&layer); let selected = shape_editor.selected_shape_state.get(&layer);
let is_selected = |selected: Option<&SelectedLayerState>, point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_selected(point)); let is_selected = |selected: Option<&SelectedLayerState>, point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_point_selected(point));
for point in vector_data.extendable_points(preferences.vector_meshes) { for point in vector_data.extendable_points(preferences.vector_meshes) {
let Some(position) = vector_data.point_domain.position_from_id(point) else { continue }; let Some(position) = vector_data.point_domain.position_from_id(point) else { continue };

View file

@ -1,7 +1,7 @@
use super::utility_functions::overlay_canvas_context; use super::utility_functions::overlay_canvas_context;
use crate::consts::{ use crate::consts::{
COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER,
COMPASS_ROSE_RING_INNER_DIAMETER, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER,
}; };
use crate::messages::prelude::Message; use crate::messages::prelude::Message;
use bezier_rs::{Bezier, Subpath}; use bezier_rs::{Bezier, Subpath};
@ -581,6 +581,35 @@ impl OverlayContext {
self.end_dpi_aware_transform(); self.end_dpi_aware_transform();
} }
/// Used by the path tool segment mode in order to show the selected segments.
pub fn outline_select_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
self.start_dpi_aware_transform();
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.stroke();
self.render_context.set_line_width(1.);
self.end_dpi_aware_transform();
}
pub fn outline_overlay_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
self.start_dpi_aware_transform();
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.stroke();
self.render_context.set_line_width(1.);
self.end_dpi_aware_transform();
}
fn bezier_command(&self, bezier: Bezier, transform: DAffine2, move_to: bool) { fn bezier_command(&self, bezier: Bezier, transform: DAffine2, move_to: bool) {
self.start_dpi_aware_transform(); self.start_dpi_aware_transform();

View file

@ -88,6 +88,18 @@ impl OriginalTransforms {
let Some(selected_points) = shape_editor.selected_points_in_layer(layer) else { let Some(selected_points) = shape_editor.selected_points_in_layer(layer) else {
continue; continue;
}; };
let Some(selected_segments) = shape_editor.selected_segments_in_layer(layer) else {
continue;
};
let mut selected_points = selected_points.clone();
for (segment_id, _, start, end) in vector_data.segment_bezier_iter() {
if selected_segments.contains(&segment_id) {
selected_points.insert(ManipulatorPointId::Anchor(start));
selected_points.insert(ManipulatorPointId::Anchor(end));
}
}
// Anchors also move their handles // Anchors also move their handles
let anchor_ids = selected_points.iter().filter_map(|point| point.as_anchor()); let anchor_ids = selected_points.iter().filter_map(|point| point.as_anchor());

View file

@ -1,14 +1,15 @@
use super::graph_modification_utils::merge_layers; use super::graph_modification_utils::merge_layers;
use super::snapping::{SnapCache, SnapCandidatePoint, SnapData, SnapManager, SnappedPoint}; use super::snapping::{SnapCache, SnapCandidatePoint, SnapData, SnapManager, SnappedPoint};
use super::utility_functions::{adjust_handle_colinearity, calculate_segment_angle, restore_g1_continuity, restore_previous_handle_position}; use super::utility_functions::{adjust_handle_colinearity, calculate_bezier_bbox, calculate_segment_angle, restore_g1_continuity, restore_previous_handle_position};
use crate::consts::HANDLE_LENGTH_FACTOR; use crate::consts::HANDLE_LENGTH_FACTOR;
use crate::messages::portfolio::document::overlays::utility_functions::selected_segments; use crate::messages::portfolio::document::overlays::utility_functions::selected_segments;
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
use crate::messages::portfolio::document::utility_types::misc::{PathSnapSource, SnapSource}; use crate::messages::portfolio::document::utility_types::misc::{PathSnapSource, SnapSource};
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface; use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
use crate::messages::preferences::SelectionMode;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::snapping::SnapTypeConfiguration; use crate::messages::tool::common_functionality::snapping::SnapTypeConfiguration;
use crate::messages::tool::common_functionality::utility_functions::is_visible_point; use crate::messages::tool::common_functionality::utility_functions::{is_intersecting, is_visible_point};
use crate::messages::tool::tool_messages::path_tool::{PathOverlayMode, PointSelectState}; use crate::messages::tool::tool_messages::path_tool::{PathOverlayMode, PointSelectState};
use bezier_rs::{Bezier, BezierHandles, Subpath, TValue}; use bezier_rs::{Bezier, BezierHandles, Subpath, TValue};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
@ -45,6 +46,7 @@ pub enum ManipulatorAngle {
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct SelectedLayerState { pub struct SelectedLayerState {
selected_points: HashSet<ManipulatorPointId>, selected_points: HashSet<ManipulatorPointId>,
selected_segments: HashSet<SegmentId>,
/// Keeps track of the current state; helps avoid unnecessary computation when called by [`ShapeState`]. /// Keeps track of the current state; helps avoid unnecessary computation when called by [`ShapeState`].
ignore_handles: bool, ignore_handles: bool,
ignore_anchors: bool, ignore_anchors: bool,
@ -54,11 +56,27 @@ pub struct SelectedLayerState {
} }
impl SelectedLayerState { impl SelectedLayerState {
pub fn selected(&self) -> impl Iterator<Item = ManipulatorPointId> + '_ { pub fn selected_points(&self) -> impl Iterator<Item = ManipulatorPointId> + '_ {
self.selected_points.iter().copied() self.selected_points.iter().copied()
} }
pub fn is_selected(&self, point: ManipulatorPointId) -> bool { pub fn selected_segments(&self) -> impl Iterator<Item = SegmentId> + '_ {
self.selected_segments.iter().copied()
}
pub fn selected_points_count(&self) -> usize {
self.selected_points.len()
}
pub fn selected_segments_count(&self) -> usize {
self.selected_segments.len()
}
pub fn is_segment_selected(&self, segment: SegmentId) -> bool {
self.selected_segments.contains(&segment)
}
pub fn is_point_selected(&self, point: ManipulatorPointId) -> bool {
self.selected_points.contains(&point) self.selected_points.contains(&point)
} }
@ -66,10 +84,26 @@ impl SelectedLayerState {
self.selected_points.insert(point); self.selected_points.insert(point);
} }
pub fn select_segment(&mut self, segment: SegmentId) {
self.selected_segments.insert(segment);
}
pub fn deselect_point(&mut self, point: ManipulatorPointId) { pub fn deselect_point(&mut self, point: ManipulatorPointId) {
self.selected_points.remove(&point); self.selected_points.remove(&point);
} }
pub fn deselect_segment(&mut self, segment: SegmentId) {
self.selected_segments.remove(&segment);
}
pub fn clear_points(&mut self) {
self.selected_points.clear();
}
pub fn clear_segments(&mut self) {
self.selected_segments.clear();
}
pub fn ignore_handles(&mut self, status: bool) { pub fn ignore_handles(&mut self, status: bool) {
if self.ignore_handles != status { if self.ignore_handles != status {
return; return;
@ -101,14 +135,6 @@ impl SelectedLayerState {
self.ignored_anchor_points.clear(); self.ignored_anchor_points.clear();
} }
} }
pub fn clear_points(&mut self) {
self.selected_points.clear();
}
pub fn selected_points_count(&self) -> usize {
self.selected_points.len()
}
} }
pub type SelectedShapeState = HashMap<LayerNodeIdentifier, SelectedLayerState>; pub type SelectedShapeState = HashMap<LayerNodeIdentifier, SelectedLayerState>;
@ -128,6 +154,12 @@ pub struct SelectedPointsInfo {
pub vector_data: VectorData, pub vector_data: VectorData,
} }
#[derive(Debug)]
pub struct SelectedSegmentsInfo {
pub segments: Vec<SegmentId>,
pub vector_data: VectorData,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ManipulatorPointInfo { pub struct ManipulatorPointInfo {
pub layer: LayerNodeIdentifier, pub layer: LayerNodeIdentifier,
@ -136,6 +168,7 @@ pub struct ManipulatorPointInfo {
pub type OpposingHandleLengths = HashMap<LayerNodeIdentifier, HashMap<HandleId, f64>>; pub type OpposingHandleLengths = HashMap<LayerNodeIdentifier, HashMap<HandleId, f64>>;
#[derive(Clone)]
pub struct ClosestSegment { pub struct ClosestSegment {
layer: LayerNodeIdentifier, layer: LayerNodeIdentifier,
segment: SegmentId, segment: SegmentId,
@ -159,6 +192,10 @@ impl ClosestSegment {
self.points self.points
} }
pub fn bezier(&self) -> Bezier {
self.bezier
}
pub fn closest_point_document(&self) -> DVec2 { pub fn closest_point_document(&self) -> DVec2 {
self.bezier.evaluate(TValue::Parametric(self.t)) self.bezier.evaluate(TValue::Parametric(self.t))
} }
@ -473,7 +510,7 @@ impl ShapeState {
if let Some(id) = selected.as_anchor() { if let Some(id) = selected.as_anchor() {
for neighbor in vector_data.connected_points(id) { for neighbor in vector_data.connected_points(id) {
if state.is_selected(ManipulatorPointId::Anchor(neighbor)) { if state.is_point_selected(ManipulatorPointId::Anchor(neighbor)) {
continue; continue;
} }
let Some(position) = vector_data.point_domain.position_from_id(neighbor) else { continue }; let Some(position) = vector_data.point_domain.position_from_id(neighbor) else { continue };
@ -512,38 +549,30 @@ impl ShapeState {
let point_position = manipulator_point_id.get_position(&vector_data)?; let point_position = manipulator_point_id.get_position(&vector_data)?;
let selected_shape_state = self.selected_shape_state.get(&layer)?; let selected_shape_state = self.selected_shape_state.get(&layer)?;
let already_selected = selected_shape_state.is_selected(manipulator_point_id); let already_selected = selected_shape_state.is_point_selected(manipulator_point_id);
// Should we select or deselect the point?
let new_selected = if already_selected { !extend_selection } else { true };
// Offset to snap the selected point to the cursor // Offset to snap the selected point to the cursor
let offset = mouse_position - network_interface.document_metadata().transform_to_viewport(layer).transform_point2(point_position); let offset = mouse_position - network_interface.document_metadata().transform_to_viewport(layer).transform_point2(point_position);
// This is selecting the manipulator only for now, next to generalize to points // This is selecting the manipulator only for now, next to generalize to points
if new_selected {
let retain_existing_selection = extend_selection || already_selected;
if !retain_existing_selection {
self.deselect_all_points();
}
// Add to the selected points let retain_existing_selection = extend_selection || already_selected;
let selected_shape_state = self.selected_shape_state.get_mut(&layer)?; if !retain_existing_selection {
selected_shape_state.select_point(manipulator_point_id); self.deselect_all_points();
self.deselect_all_segments();
let points = self
.selected_shape_state
.iter()
.flat_map(|(layer, state)| state.selected_points.iter().map(|&point_id| ManipulatorPointInfo { layer: *layer, point_id }))
.collect();
return Some(Some(SelectedPointsInfo { points, offset, vector_data }));
} else {
let selected_shape_state = self.selected_shape_state.get_mut(&layer)?;
selected_shape_state.deselect_point(manipulator_point_id);
return Some(None);
} }
// Add to the selected points (deselect is managed in DraggingState, DragStop)
let selected_shape_state = self.selected_shape_state.get_mut(&layer)?;
selected_shape_state.select_point(manipulator_point_id);
let points = self
.selected_shape_state
.iter()
.flat_map(|(layer, state)| state.selected_points.iter().map(|&point_id| ManipulatorPointInfo { layer: *layer, point_id }))
.collect();
return Some(Some(SelectedPointsInfo { points, offset, vector_data }));
} }
None None
} }
@ -555,11 +584,16 @@ impl ShapeState {
select_threshold: f64, select_threshold: f64,
path_overlay_mode: PathOverlayMode, 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>)> { ) -> Option<(bool, Option<SelectedPointsInfo>)> {
if self.selected_shape_state.is_empty() { if self.selected_shape_state.is_empty() {
return None; 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 let Some((layer, manipulator_point_id)) = self.find_nearest_point_indices(network_interface, mouse_position, select_threshold) {
let vector_data = network_interface.compute_modified_vector(layer)?; let vector_data = network_interface.compute_modified_vector(layer)?;
let point_position = manipulator_point_id.get_position(&vector_data)?; let point_position = manipulator_point_id.get_position(&vector_data)?;
@ -572,7 +606,7 @@ impl ShapeState {
} }
let selected_shape_state = self.selected_shape_state.get(&layer)?; let selected_shape_state = self.selected_shape_state.get(&layer)?;
let already_selected = selected_shape_state.is_selected(manipulator_point_id); let already_selected = selected_shape_state.is_point_selected(manipulator_point_id);
// Offset to snap the selected point to the cursor // Offset to snap the selected point to the cursor
let offset = mouse_position - network_interface.document_metadata().transform_to_viewport(layer).transform_point2(point_position); let offset = mouse_position - network_interface.document_metadata().transform_to_viewport(layer).transform_point2(point_position);
@ -630,7 +664,7 @@ impl ShapeState {
// Select all connected points // Select all connected points
while let Some(point) = selected_stack.pop() { while let Some(point) = selected_stack.pop() {
let anchor_point = ManipulatorPointId::Anchor(point); let anchor_point = ManipulatorPointId::Anchor(point);
if !state.is_selected(anchor_point) { if !state.is_point_selected(anchor_point) {
state.select_point(anchor_point); state.select_point(anchor_point);
selected_stack.extend(vector_data.connected_points(point)); selected_stack.extend(vector_data.connected_points(point));
} }
@ -671,6 +705,13 @@ impl ShapeState {
} }
} }
/// Deselects all segments across every selected layer
pub fn deselect_all_segments(&mut self) {
for state in self.selected_shape_state.values_mut() {
state.selected_segments.clear()
}
}
pub fn update_selected_anchors_status(&mut self, status: bool) { pub fn update_selected_anchors_status(&mut self, status: bool) {
for state in self.selected_shape_state.values_mut() { for state in self.selected_shape_state.values_mut() {
self.ignore_anchors = !status; self.ignore_anchors = !status;
@ -736,10 +777,18 @@ impl ShapeState {
self.selected_shape_state.values().flat_map(|state| &state.selected_points) self.selected_shape_state.values().flat_map(|state| &state.selected_points)
} }
pub fn selected_segments(&self) -> impl Iterator<Item = &'_ SegmentId> {
self.selected_shape_state.values().flat_map(|state| &state.selected_segments)
}
pub fn selected_points_in_layer(&self, layer: LayerNodeIdentifier) -> Option<&HashSet<ManipulatorPointId>> { pub fn selected_points_in_layer(&self, layer: LayerNodeIdentifier) -> Option<&HashSet<ManipulatorPointId>> {
self.selected_shape_state.get(&layer).map(|state| &state.selected_points) self.selected_shape_state.get(&layer).map(|state| &state.selected_points)
} }
pub fn selected_segments_in_layer(&self, layer: LayerNodeIdentifier) -> Option<&HashSet<SegmentId>> {
self.selected_shape_state.get(&layer).map(|state| &state.selected_segments)
}
pub fn move_primary(&self, segment: SegmentId, delta: DVec2, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) { pub fn move_primary(&self, segment: SegmentId, delta: DVec2, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
responses.add(GraphOperationMessage::Vector { responses.add(GraphOperationMessage::Vector {
layer, layer,
@ -766,7 +815,7 @@ impl ShapeState {
let Some((start, _end, bezier)) = vector_data.segment_points_from_id(segment) else { continue }; let Some((start, _end, bezier)) = vector_data.segment_points_from_id(segment) else { continue };
if let BezierHandles::Quadratic { handle } = bezier.handles { if let BezierHandles::Quadratic { handle } = bezier.handles {
if selected.is_some_and(|selected| selected.is_selected(ManipulatorPointId::Anchor(start))) { if selected.is_some_and(|selected| selected.is_point_selected(ManipulatorPointId::Anchor(start))) {
continue; continue;
} }
@ -1012,9 +1061,9 @@ impl ShapeState {
} }
} }
/// Move the selected points by dragging the mouse. /// Move the selected points and segments by dragging the mouse.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn move_selected_points( pub fn move_selected_points_and_segments(
&self, &self,
handle_lengths: Option<OpposingHandleLengths>, handle_lengths: Option<OpposingHandleLengths>,
document: &DocumentMessageHandler, document: &DocumentMessageHandler,
@ -1040,7 +1089,17 @@ impl ShapeState {
}; };
let delta = delta_transform.inverse().transform_vector2(delta); let delta = delta_transform.inverse().transform_vector2(delta);
for &point in state.selected_points.iter() { // Make a new collection of anchor points which needs to be moved
let mut affected_points = state.selected_points.clone();
for (segment_id, _, start, end) in vector_data.segment_bezier_iter() {
if state.is_segment_selected(segment_id) {
affected_points.insert(ManipulatorPointId::Anchor(start));
affected_points.insert(ManipulatorPointId::Anchor(end));
}
}
for &point in affected_points.iter() {
if self.is_point_ignored(&point) { if self.is_point_ignored(&point) {
continue; continue;
} }
@ -1055,7 +1114,7 @@ impl ShapeState {
}; };
let Some(anchor_id) = point.get_anchor(&vector_data) else { continue }; let Some(anchor_id) = point.get_anchor(&vector_data) else { continue };
if state.is_selected(ManipulatorPointId::Anchor(anchor_id)) { if state.is_point_selected(ManipulatorPointId::Anchor(anchor_id)) {
continue; continue;
} }
@ -1074,7 +1133,7 @@ impl ShapeState {
continue; continue;
} }
if state.is_selected(other.to_manipulator_point()) { if state.is_point_selected(other.to_manipulator_point()) {
// If two colinear handles are being dragged at the same time but not the anchor, it is necessary to break the colinear state. // If two colinear handles are being dragged at the same time but not the anchor, it is necessary to break the colinear state.
let handles = [handle, other]; let handles = [handle, other];
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false }; let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
@ -1125,12 +1184,12 @@ impl ShapeState {
// ii) The anchor is not selected. // ii) The anchor is not selected.
let anchor = handles[0].to_manipulator_point().get_anchor(&vector_data)?; let anchor = handles[0].to_manipulator_point().get_anchor(&vector_data)?;
let anchor_selected = state.is_selected(ManipulatorPointId::Anchor(anchor)); let anchor_selected = state.is_point_selected(ManipulatorPointId::Anchor(anchor));
if anchor_selected { if anchor_selected {
return None; return None;
} }
let handles_selected = handles.map(|handle| state.is_selected(handle.to_manipulator_point())); let handles_selected = handles.map(|handle| state.is_point_selected(handle.to_manipulator_point()));
let other = match handles_selected { let other = match handles_selected {
[true, false] => handles[1], [true, false] => handles[1],
@ -1208,11 +1267,15 @@ impl ShapeState {
continue; continue;
}; };
let selected_segments = &state.selected_segments;
for point in std::mem::take(&mut state.selected_points) { for point in std::mem::take(&mut state.selected_points) {
match point { match point {
ManipulatorPointId::Anchor(anchor) => { ManipulatorPointId::Anchor(anchor) => {
if let Some(handles) = Self::dissolve_anchor(anchor, responses, layer, &vector_data) { if let Some(handles) = Self::dissolve_anchor(anchor, responses, layer, &vector_data) {
missing_anchors.insert(anchor, handles); if !vector_data.all_connected(anchor).any(|a| selected_segments.contains(&a.segment)) {
missing_anchors.insert(anchor, handles);
}
} }
deleted_anchors.insert(anchor); deleted_anchors.insert(anchor);
} }
@ -1257,6 +1320,8 @@ impl ShapeState {
continue; continue;
} }
// Avoid reconnecting to points which have adjacent segments selected
// Grab the handles from the opposite side of the segment(s) being deleted and make it relative to the anchor // Grab the handles from the opposite side of the segment(s) being deleted and make it relative to the anchor
let [handle_start, handle_end] = [start, end].map(|(handle, _)| { let [handle_start, handle_end] = [start, end].map(|(handle, _)| {
let handle = handle.opposite(); let handle = handle.opposite();
@ -1304,6 +1369,20 @@ impl ShapeState {
} }
} }
pub fn delete_selected_segments(&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 (segment, _, start, end) in vector_data.segment_bezier_iter() {
if state.selected_segments.contains(&segment) {
self.dissolve_segment(responses, layer, &vector_data, segment, [start, end]);
}
}
}
}
pub fn break_path_at_selected_point(&self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) { pub fn break_path_at_selected_point(&self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
for (&layer, state) in &self.selected_shape_state { for (&layer, state) in &self.selected_shape_state {
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue }; let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue };
@ -1748,6 +1827,7 @@ impl ShapeState {
false false
} }
#[allow(clippy::too_many_arguments)]
pub fn select_all_in_shape( pub fn select_all_in_shape(
&mut self, &mut self,
network_interface: &NodeNetworkInterface, network_interface: &NodeNetworkInterface,
@ -1755,13 +1835,17 @@ impl ShapeState {
selection_change: SelectionChange, selection_change: SelectionChange,
path_overlay_mode: PathOverlayMode, path_overlay_mode: PathOverlayMode,
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>, frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
select_segments: bool,
// Here, "selection mode" represents touched or enclosed, not to be confused with editing modes
selection_mode: SelectionMode,
) { ) {
let selected_points = self.selected_points().cloned().collect::<HashSet<_>>(); let selected_points = self.selected_points().cloned().collect::<HashSet<_>>();
let selected_segments = selected_segments(network_interface, self); let selected_segments = selected_segments(network_interface, self);
for (&layer, state) in &mut self.selected_shape_state { for (&layer, state) in &mut self.selected_shape_state {
if selection_change == SelectionChange::Clear { if selection_change == SelectionChange::Clear {
state.clear_points() state.clear_points();
state.clear_segments();
} }
let vector_data = network_interface.compute_modified_vector(layer); let vector_data = network_interface.compute_modified_vector(layer);
@ -1787,7 +1871,46 @@ impl ShapeState {
None None
}; };
// Selection segments
for (id, bezier, _, _) in vector_data.segment_bezier_iter() { for (id, bezier, _, _) in vector_data.segment_bezier_iter() {
if select_segments {
// Select segments if they lie inside the bounding box or lasso polygon
let segment_bbox = calculate_bezier_bbox(bezier);
let bottom_left = transform.transform_point2(segment_bbox[0]);
let top_right = transform.transform_point2(segment_bbox[1]);
let select = match selection_shape {
SelectionShape::Box(quad) => {
let enclosed = quad[0].min(quad[1]).cmple(bottom_left).all() && quad[0].max(quad[1]).cmpge(top_right).all();
match selection_mode {
SelectionMode::Enclosed => enclosed,
_ => {
// Check for intersection with the segment
enclosed || is_intersecting(bezier, quad, transform)
}
}
}
SelectionShape::Lasso(_) => {
let polygon = polygon_subpath.as_ref().expect("If `selection_shape` is a polygon then subpath is constructed beforehand.");
// Sample 10 points on the bezier and check if all or some lie inside the polygon
let points = bezier.compute_lookup_table(Some(10), None);
match selection_mode {
SelectionMode::Enclosed => points.map(|p| transform.transform_point2(p)).all(|p| polygon.contains_point(p)),
_ => points.map(|p| transform.transform_point2(p)).any(|p| polygon.contains_point(p)),
}
}
};
if select {
match selection_change {
SelectionChange::Shrink => state.deselect_segment(id),
_ => state.select_segment(id),
}
}
}
// Selecting handles
for (position, id) in [(bezier.handle_start(), ManipulatorPointId::PrimaryHandle(id)), (bezier.handle_end(), ManipulatorPointId::EndHandle(id))] { for (position, id) in [(bezier.handle_start(), ManipulatorPointId::PrimaryHandle(id)), (bezier.handle_end(), ManipulatorPointId::EndHandle(id))] {
let Some(position) = position else { continue }; let Some(position) = position else { continue };
let transformed_position = transform.transform_point2(position); let transformed_position = transform.transform_point2(position);
@ -1820,6 +1943,7 @@ impl ShapeState {
} }
} }
// Checking for selection of anchor points
for (&id, &position) in vector_data.point_domain.ids().iter().zip(vector_data.point_domain.positions()) { for (&id, &position) in vector_data.point_domain.ids().iter().zip(vector_data.point_domain.positions()) {
let transformed_position = transform.transform_point2(position); let transformed_position = transform.transform_point2(position);

View file

@ -8,11 +8,12 @@ use crate::messages::tool::common_functionality::graph_modification_utils::get_t
use crate::messages::tool::common_functionality::transformation_cage::SelectedEdges; use crate::messages::tool::common_functionality::transformation_cage::SelectedEdges;
use crate::messages::tool::tool_messages::path_tool::PathOverlayMode; use crate::messages::tool::tool_messages::path_tool::PathOverlayMode;
use crate::messages::tool::utility_types::ToolType; use crate::messages::tool::utility_types::ToolType;
use bezier_rs::Bezier; use bezier_rs::{Bezier, BezierHandles};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphene_std::renderer::Quad; use graphene_std::renderer::Quad;
use graphene_std::text::{FontCache, load_face}; use graphene_std::text::{FontCache, load_face};
use graphene_std::vector::{HandleExt, HandleId, ManipulatorPointId, PointId, SegmentId, VectorData, VectorModificationType}; use graphene_std::vector::{HandleExt, HandleId, ManipulatorPointId, PointId, SegmentId, VectorData, VectorModificationType};
use kurbo::{CubicBez, Line, ParamCurveExtrema, PathSeg, Point, QuadBez};
/// Determines if a path should be extended. Goal in viewport space. Returns the path and if it is extending from the start, if applicable. /// Determines if a path should be extended. Goal in viewport space. Returns the path and if it is extending from the start, if applicable.
pub fn should_extend( pub fn should_extend(
@ -204,6 +205,71 @@ pub fn is_visible_point(
} }
} }
/// Function to find the bounding box of bezier (uses method from kurbo)
pub fn calculate_bezier_bbox(bezier: Bezier) -> [DVec2; 2] {
let start = Point::new(bezier.start.x, bezier.start.y);
let end = Point::new(bezier.end.x, bezier.end.y);
let bbox = match bezier.handles {
BezierHandles::Cubic { handle_start, handle_end } => {
let p1 = Point::new(handle_start.x, handle_start.y);
let p2 = Point::new(handle_end.x, handle_end.y);
CubicBez::new(start, p1, p2, end).bounding_box()
}
BezierHandles::Quadratic { handle } => {
let p1 = Point::new(handle.x, handle.y);
QuadBez::new(start, p1, end).bounding_box()
}
BezierHandles::Linear => Line::new(start, end).bounding_box(),
};
[DVec2::new(bbox.x0, bbox.y0), DVec2::new(bbox.x1, bbox.y1)]
}
pub fn is_intersecting(bezier: Bezier, quad: [DVec2; 2], transform: DAffine2) -> bool {
let to_layerspace = transform.inverse();
let quad = [to_layerspace.transform_point2(quad[0]), to_layerspace.transform_point2(quad[1])];
let start = Point::new(bezier.start.x, bezier.start.y);
let end = Point::new(bezier.end.x, bezier.end.y);
let segment = match bezier.handles {
BezierHandles::Cubic { handle_start, handle_end } => {
let p1 = Point::new(handle_start.x, handle_start.y);
let p2 = Point::new(handle_end.x, handle_end.y);
PathSeg::Cubic(CubicBez::new(start, p1, p2, end))
}
BezierHandles::Quadratic { handle } => {
let p1 = Point::new(handle.x, handle.y);
PathSeg::Quad(QuadBez::new(start, p1, end))
}
BezierHandles::Linear => PathSeg::Line(Line::new(start, end)),
};
// Create a list of all the sides
let sides = [
Line::new((quad[0].x, quad[0].y), (quad[1].x, quad[0].y)),
Line::new((quad[0].x, quad[0].y), (quad[0].x, quad[1].y)),
Line::new((quad[1].x, quad[1].y), (quad[1].x, quad[0].y)),
Line::new((quad[1].x, quad[1].y), (quad[0].x, quad[1].y)),
];
let mut is_intersecting = false;
for line in sides {
let intersections = segment.intersect_line(line);
let mut intersects = false;
for intersection in intersections {
if intersection.line_t <= 1. && intersection.line_t >= 0. && intersection.segment_t <= 1. && intersection.segment_t >= 0. {
// There is a valid intersection point
intersects = true;
break;
}
}
if intersects {
is_intersecting = true;
break;
}
}
is_intersecting
}
#[allow(clippy::too_many_arguments)]
pub fn resize_bounds( pub fn resize_bounds(
document: &DocumentMessageHandler, document: &DocumentMessageHandler,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
@ -221,7 +287,7 @@ pub fn resize_bounds(
let snap = Some(SizeSnapData { let snap = Some(SizeSnapData {
manager: snap_manager, manager: snap_manager,
points: snap_candidates, points: snap_candidates,
snap_data: SnapData::ignore(document, input, &dragging_layers), snap_data: SnapData::ignore(document, input, dragging_layers),
}); });
let (position, size) = movement.new_size(input.mouse.position, bounds.original_bound_transform, center, constrain, snap); let (position, size) = movement.new_size(input.mouse.position, bounds.original_bound_transform, center, constrain, snap);
let (delta, mut pivot) = movement.bounds_to_scale_transform(position, size); let (delta, mut pivot) = movement.bounds_to_scale_transform(position, size);
@ -238,11 +304,12 @@ pub fn resize_bounds(
} }
}); });
let mut selected = Selected::new(&mut bounds.original_transforms, &mut pivot, &dragging_layers, responses, &document.network_interface, None, &tool, None); let mut selected = Selected::new(&mut bounds.original_transforms, &mut pivot, dragging_layers, responses, &document.network_interface, None, &tool, None);
selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse(), None); selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse(), None);
} }
} }
#[allow(clippy::too_many_arguments)]
pub fn rotate_bounds( pub fn rotate_bounds(
document: &DocumentMessageHandler, document: &DocumentMessageHandler,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
@ -280,7 +347,7 @@ pub fn rotate_bounds(
let mut selected = Selected::new( let mut selected = Selected::new(
&mut bounds.original_transforms, &mut bounds.original_transforms,
&mut bounds.center_of_transformation, &mut bounds.center_of_transformation,
&dragging_layers, dragging_layers,
responses, responses,
&document.network_interface, &document.network_interface,
None, None,
@ -313,7 +380,7 @@ pub fn skew_bounds(
} }
}); });
let mut selected = Selected::new(&mut bounds.original_transforms, &mut pivot, &layers, responses, &document.network_interface, None, &tool, None); let mut selected = Selected::new(&mut bounds.original_transforms, &mut pivot, layers, responses, &document.network_interface, None, &tool, None);
selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse(), None); selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse(), None);
} }
} }
@ -365,7 +432,7 @@ pub fn transforming_transform_cage(
let mut selected = Selected::new( let mut selected = Selected::new(
&mut bounds.original_transforms, &mut bounds.original_transforms,
&mut bounds.center_of_transformation, &mut bounds.center_of_transformation,
&layers_dragging, layers_dragging,
responses, responses,
&document.network_interface, &document.network_interface,
None, None,
@ -423,7 +490,7 @@ pub fn transforming_transform_cage(
} }
// No resize, rotate, or skew // No resize, rotate, or skew
return (false, false, false); (false, false, false)
} }
/// Calculates similarity metric between new bezier curve and two old beziers by using sampled points. /// Calculates similarity metric between new bezier curve and two old beziers by using sampled points.

View file

@ -32,6 +32,7 @@ pub struct PathTool {
#[derive(Default)] #[derive(Default)]
pub struct PathToolOptions { pub struct PathToolOptions {
path_overlay_mode: PathOverlayMode, path_overlay_mode: PathOverlayMode,
path_editing_mode: PathEditingMode,
} }
#[impl_message(Message, ToolMessage, Path)] #[impl_message(Message, ToolMessage, Path)]
@ -69,6 +70,7 @@ pub enum PathToolMessage {
lasso_select: Key, lasso_select: Key,
handle_drag_from_anchor: Key, handle_drag_from_anchor: Key,
drag_restore_handle: Key, drag_restore_handle: Key,
molding_in_segment_edit: Key,
}, },
NudgeSelectedPoints { NudgeSelectedPoints {
delta_x: f64, delta_x: f64,
@ -116,9 +118,26 @@ pub enum PathOverlayMode {
FrontierHandles = 2, 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)] #[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum PathOptionsUpdate { pub enum PathOptionsUpdate {
OverlayModeType(PathOverlayMode), OverlayModeType(PathOverlayMode),
PointEditingMode { enabled: bool },
SegmentEditingMode { enabled: bool },
} }
impl ToolMetadata for PathTool { impl ToolMetadata for PathTool {
@ -203,6 +222,19 @@ impl LayoutHolder for PathTool {
.for_checkbox(&mut checkbox_id) .for_checkbox(&mut checkbox_id)
.widget_holder(); .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![ let path_overlay_mode_widget = RadioInput::new(vec![
RadioEntryData::new("all") RadioEntryData::new("all")
.icon("HandleVisibilityAll") .icon("HandleVisibilityAll")
@ -227,8 +259,12 @@ impl LayoutHolder for PathTool {
y_location, y_location,
unrelated_seperator.clone(), unrelated_seperator.clone(),
colinear_handle_checkbox, colinear_handle_checkbox,
related_seperator, related_seperator.clone(),
colinear_handles_label, colinear_handles_label,
unrelated_seperator.clone(),
point_editing_mode,
related_seperator.clone(),
segment_editing_mode,
unrelated_seperator, unrelated_seperator,
path_overlay_mode_widget, path_overlay_mode_widget,
], ],
@ -246,6 +282,14 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
self.options.path_overlay_mode = overlay_mode_type; self.options.path_overlay_mode = overlay_mode_type;
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
} }
PathOptionsUpdate::PointEditingMode { enabled } => {
self.options.path_editing_mode.point_editing_mode = enabled;
responses.add(OverlaysMessage::Draw);
}
PathOptionsUpdate::SegmentEditingMode { enabled } => {
self.options.path_editing_mode.segment_editing_mode = enabled;
responses.add(OverlaysMessage::Draw);
}
}, },
ToolMessage::Path(PathToolMessage::ClosePath) => { ToolMessage::Path(PathToolMessage::ClosePath) => {
responses.add(DocumentMessage::AddTransaction); responses.add(DocumentMessage::AddTransaction);
@ -405,6 +449,7 @@ struct PathToolData {
angle: f64, angle: f64,
opposite_handle_position: Option<DVec2>, opposite_handle_position: Option<DVec2>,
last_clicked_point_was_selected: bool, last_clicked_point_was_selected: bool,
last_clicked_segment_was_selected: bool,
snapping_axis: Option<Axis>, snapping_axis: Option<Axis>,
alt_clicked_on_anchor: bool, alt_clicked_on_anchor: bool,
alt_dragging_from_anchor: bool, alt_dragging_from_anchor: bool,
@ -416,6 +461,7 @@ struct PathToolData {
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>, frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
adjacent_anchor_offset: Option<DVec2>, adjacent_anchor_offset: Option<DVec2>,
sliding_point_info: Option<SlidingPointInfo>, sliding_point_info: Option<SlidingPointInfo>,
started_drawing_from_inside: bool,
} }
impl PathToolData { impl PathToolData {
@ -476,6 +522,7 @@ impl PathToolData {
self.selection_status = selection_status; 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)] #[allow(clippy::too_many_arguments)]
fn mouse_down( fn mouse_down(
&mut self, &mut self,
@ -487,7 +534,10 @@ impl PathToolData {
lasso_select: bool, lasso_select: bool,
handle_drag_from_anchor: bool, handle_drag_from_anchor: bool,
drag_zero_handle: bool, drag_zero_handle: bool,
molding_in_segment_edit: bool,
path_overlay_mode: PathOverlayMode, path_overlay_mode: PathOverlayMode,
segment_editing_mode: bool,
point_editing_mode: bool,
) -> PathToolFsmState { ) -> PathToolFsmState {
self.double_click_handled = false; self.double_click_handled = false;
self.opposing_handle_lengths = None; self.opposing_handle_lengths = None;
@ -510,6 +560,7 @@ impl PathToolData {
SELECTION_THRESHOLD, SELECTION_THRESHOLD,
path_overlay_mode, path_overlay_mode,
self.frontier_handles_info.clone(), self.frontier_handles_info.clone(),
point_editing_mode,
) { ) {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
@ -593,25 +644,51 @@ impl PathToolData {
} }
PathToolFsmState::Dragging(self.dragging_state) PathToolFsmState::Dragging(self.dragging_state)
} }
// We didn't find a point nearby, so we will see if there is a segment to insert a point on // 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(closed_segment) = &mut self.segment { else if let Some(segment) = shape_editor.upper_closest_segment(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD) {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
// Calculating and storing handle positions if segment_editing_mode && !molding_in_segment_edit {
let handle1 = ManipulatorPointId::PrimaryHandle(closed_segment.segment()); let layer = segment.layer();
let handle2 = ManipulatorPointId::EndHandle(closed_segment.segment()); 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 let Some(vector_data) = document.network_interface.compute_modified_vector(closed_segment.layer()) { if !(already_selected && extend_selection) {
if let (Some(pos1), Some(pos2)) = (handle1.get_position(&vector_data), handle2.get_position(&vector_data)) { let retain_existing_selection = extend_selection || already_selected;
self.molding_info = Some((pos1, pos2)) 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);
}
} }
}
PathToolFsmState::MoldingSegment self.drag_start_pos = input.mouse.position;
let viewport_to_document = document.metadata().document_to_viewport.inverse();
self.previous_mouse_position = viewport_to_document.transform_point2(input.mouse.position);
responses.add(OverlaysMessage::Draw);
PathToolFsmState::Dragging(self.dragging_state)
} else {
let handle1 = ManipulatorPointId::PrimaryHandle(segment.segment());
let handle2 = ManipulatorPointId::EndHandle(segment.segment());
if let Some(vector_data) = document.network_interface.compute_modified_vector(segment.layer()) {
if let (Some(pos1), Some(pos2)) = (handle1.get_position(&vector_data), handle2.get_position(&vector_data)) {
self.molding_info = Some((pos1, pos2))
}
}
PathToolFsmState::MoldingSegment
}
} }
// We didn't find a segment, so consider selecting the nearest shape instead // We didn't find a segment, so consider selecting the nearest shape instead and start drawing
else if let Some(layer) = document.click(input) { else if let Some(layer) = document.click(input) {
shape_editor.deselect_all_points(); shape_editor.deselect_all_points();
shape_editor.deselect_all_segments();
if extend_selection { if extend_selection {
responses.add(NodeGraphMessage::SelectedNodesAdd { nodes: vec![layer.to_node()] }); responses.add(NodeGraphMessage::SelectedNodesAdd { nodes: vec![layer.to_node()] });
} else { } else {
@ -620,9 +697,10 @@ impl PathToolData {
self.drag_start_pos = input.mouse.position; self.drag_start_pos = input.mouse.position;
self.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position); self.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
responses.add(DocumentMessage::StartTransaction); self.started_drawing_from_inside = true;
PathToolFsmState::Dragging(self.dragging_state) let selection_shape = if lasso_select { SelectionShapeType::Lasso } else { SelectionShapeType::Box };
PathToolFsmState::Drawing { selection_shape }
} }
// Start drawing // Start drawing
else { else {
@ -644,7 +722,7 @@ impl PathToolData {
let transform = document.metadata().transform_to_document(layer); let transform = document.metadata().transform_to_document(layer);
let mut layer_manipulators = HashSet::with_hasher(NoHashBuilder); let mut layer_manipulators = HashSet::with_hasher(NoHashBuilder);
for point in state.selected() { for point in state.selected_points() {
let Some(anchor) = point.get_anchor(&vector_data) else { continue }; let Some(anchor) = point.get_anchor(&vector_data) else { continue };
layer_manipulators.insert(anchor); layer_manipulators.insert(anchor);
let Some([handle1, handle2]) = point.get_handle_pair(&vector_data) else { continue }; let Some([handle1, handle2]) = point.get_handle_pair(&vector_data) else { continue };
@ -748,7 +826,7 @@ impl PathToolData {
} }
// Only count selected handles // Only count selected handles
let selected_handle = selection.selected().next()?.as_handle()?; let selected_handle = selection.selected_points().next()?.as_handle()?;
let handle_id = selected_handle.to_manipulator_point(); let handle_id = selected_handle.to_manipulator_point();
let layer_to_document = document.metadata().transform_to_document(*layer); let layer_to_document = document.metadata().transform_to_document(*layer);
@ -875,7 +953,7 @@ impl PathToolData {
let drag_start = self.drag_start_pos; let drag_start = self.drag_start_pos;
let opposite_delta = drag_start - current_mouse; let opposite_delta = drag_start - current_mouse;
shape_editor.move_selected_points(None, document, opposite_delta, false, true, false, None, false, responses); 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 // Calculate the projected delta and shift the points along that delta
let delta = current_mouse - drag_start; let delta = current_mouse - drag_start;
@ -887,7 +965,7 @@ impl PathToolData {
_ => DVec2::new(delta.x, 0.), _ => DVec2::new(delta.x, 0.),
}; };
shape_editor.move_selected_points(None, document, projected_delta, false, true, false, None, false, responses); 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>) { fn stop_snap_along_axis(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
@ -903,12 +981,12 @@ impl PathToolData {
_ => DVec2::new(opposite_delta.x, 0.), _ => DVec2::new(opposite_delta.x, 0.),
}; };
shape_editor.move_selected_points(None, document, opposite_projected_delta, false, true, false, None, false, responses); 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 // Calculate what actually would have been the original delta for the point, and apply that
let delta = current_mouse - drag_start; let delta = current_mouse - drag_start;
shape_editor.move_selected_points(None, document, delta, false, true, false, None, false, responses); shape_editor.move_selected_points_and_segments(None, document, delta, false, true, false, None, false, responses);
self.snapping_axis = None; self.snapping_axis = None;
} }
@ -930,6 +1008,28 @@ impl PathToolData {
tangent_vector.try_normalize() 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(), 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 { 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(_))); let single_anchor_selected = shape_editor.selected_points().count() == 1 && shape_editor.selected_points().any(|point| matches!(point, ManipulatorPointId::Anchor(_)));
@ -1192,7 +1292,7 @@ impl PathToolData {
self.temporary_colinear_handles = false; self.temporary_colinear_handles = false;
skip_opposite = true; skip_opposite = true;
} }
shape_editor.move_selected_points(handle_lengths, document, snapped_delta, equidistant, true, was_alt_dragging, opposite, skip_opposite, responses); 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); self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(snapped_delta);
} else { } else {
let Some(axis) = self.snapping_axis else { return }; let Some(axis) = self.snapping_axis else { return };
@ -1201,7 +1301,7 @@ impl PathToolData {
Axis::Y => DVec2::new(0., unsnapped_delta.y), Axis::Y => DVec2::new(0., unsnapped_delta.y),
_ => DVec2::new(unsnapped_delta.x, 0.), _ => DVec2::new(unsnapped_delta.x, 0.),
}; };
shape_editor.move_selected_points(handle_lengths, document, projected_delta, equidistant, true, false, opposite, false, responses); 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); self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(unsnapped_delta);
} }
@ -1225,7 +1325,7 @@ impl Fsm for PathToolFsmState {
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque<Message>) -> Self { 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; let ToolActionHandlerData { document, input, shape_editor, .. } = tool_action_data;
update_dynamic_hints(self, responses, shape_editor, document, tool_data); update_dynamic_hints(self, responses, shape_editor, document, tool_data, tool_options);
let ToolMessage::Path(event) = event else { return self }; let ToolMessage::Path(event) = event else { return self };
match (self, event) { match (self, event) {
@ -1309,48 +1409,43 @@ impl Fsm for PathToolFsmState {
match self { match self {
Self::Ready => { Self::Ready => {
// Check if there is no point nearby tool_data.update_closest_segment(shape_editor, input.mouse.position, document, tool_options.path_overlay_mode);
if 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(),
)
.is_some()
{
tool_data.segment = None;
}
// If already hovering on a segment, then recalculate its closest point
else if let Some(closest_segment) = &mut tool_data.segment {
closest_segment.update_closest_point(document.metadata(), input.mouse.position);
if closest_segment.too_far(input.mouse.position, SEGMENT_INSERTION_DISTANCE) {
tool_data.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, input.mouse.position, SEGMENT_INSERTION_DISTANCE) {
tool_data.segment = Some(closest_segment);
}
if let Some(closest_segment) = &tool_data.segment { if let Some(closest_segment) = &tool_data.segment {
let perp = closest_segment.calculate_perp(document); if tool_options.path_editing_mode.segment_editing_mode {
let point = closest_segment.closest_point(document.metadata()); let transform = document.metadata().transform_to_viewport(closest_segment.layer());
// Draw an X on the segment overlay_context.outline_overlay_bezier(closest_segment.bezier(), transform);
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); // Draw the anchors again
overlay_context.line(point - tilted_perp * SEGMENT_OVERLAY_SIZE, point + tilted_perp * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), None); let display_anchors = overlay_context.visibility_settings.anchors();
} if display_anchors {
// Draw a line on the segment let start_pos = transform.transform_point2(closest_segment.bezier().start);
else { let end_pos = transform.transform_point2(closest_segment.bezier().end);
overlay_context.line(point - perp * SEGMENT_OVERLAY_SIZE, point + perp * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), None); 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());
// 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);
}
} }
} }
} }
@ -1370,11 +1465,13 @@ impl Fsm for PathToolFsmState {
let quad = tool_data.selection_quad(document.metadata()); let quad = tool_data.selection_quad(document.metadata());
let polygon = &tool_data.lasso_polygon; let polygon = &tool_data.lasso_polygon;
match (selection_shape, selection_mode) { match (selection_shape, selection_mode, tool_data.started_drawing_from_inside) {
(SelectionShapeType::Box, SelectionMode::Enclosed) => overlay_context.dashed_quad(quad, None, fill_color, Some(4.), Some(4.), Some(0.5)), // Don't draw box if it is from inside a shape and selection just began
(SelectionShapeType::Lasso, SelectionMode::Enclosed) => overlay_context.dashed_polygon(polygon, None, fill_color, Some(4.), Some(4.), Some(0.5)), (SelectionShapeType::Box, SelectionMode::Enclosed, false) => overlay_context.dashed_quad(quad, None, fill_color, Some(4.), Some(4.), Some(0.5)),
(SelectionShapeType::Box, _) => overlay_context.quad(quad, None, fill_color), (SelectionShapeType::Lasso, SelectionMode::Enclosed, _) => overlay_context.dashed_polygon(polygon, None, fill_color, Some(4.), Some(4.), Some(0.5)),
(SelectionShapeType::Lasso, _) => overlay_context.polygon(polygon, None, fill_color), (SelectionShapeType::Box, _, false) => overlay_context.quad(quad, None, fill_color),
(SelectionShapeType::Lasso, _, _) => overlay_context.polygon(polygon, None, fill_color),
(SelectionShapeType::Box, _, _) => {}
} }
} }
Self::Dragging(_) => { Self::Dragging(_) => {
@ -1420,12 +1517,14 @@ impl Fsm for PathToolFsmState {
lasso_select, lasso_select,
handle_drag_from_anchor, handle_drag_from_anchor,
drag_restore_handle, drag_restore_handle,
molding_in_segment_edit,
}, },
) => { ) => {
let extend_selection = input.keyboard.get(extend_selection as usize); let extend_selection = input.keyboard.get(extend_selection as usize);
let lasso_select = input.keyboard.get(lasso_select 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 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 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.selection_mode = None;
tool_data.lasso_polygon.clear(); tool_data.lasso_polygon.clear();
@ -1439,7 +1538,10 @@ impl Fsm for PathToolFsmState {
lasso_select, lasso_select,
handle_drag_from_anchor, handle_drag_from_anchor,
drag_zero_handle, drag_zero_handle,
molding_in_segment_edit,
tool_options.path_overlay_mode, tool_options.path_overlay_mode,
tool_options.path_editing_mode.segment_editing_mode,
tool_options.path_editing_mode.point_editing_mode,
) )
} }
( (
@ -1455,6 +1557,7 @@ impl Fsm for PathToolFsmState {
}, },
) => { ) => {
tool_data.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position); tool_data.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
tool_data.started_drawing_from_inside = false;
if selection_shape == SelectionShapeType::Lasso { if selection_shape == SelectionShapeType::Lasso {
extend_lasso(&mut tool_data.lasso_polygon, input.mouse.position); extend_lasso(&mut tool_data.lasso_polygon, input.mouse.position);
@ -1516,12 +1619,6 @@ impl Fsm for PathToolFsmState {
tool_data.handle_drag_toggle = true; tool_data.handle_drag_toggle = true;
} }
if tool_data.selection_status.is_none() {
if let Some(layer) = document.click(input) {
shape_editor.select_all_anchors_in_layer(document, layer);
}
}
let anchor_and_handle_toggled = input.keyboard.get(move_anchor_with_handles as usize); 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 initial_press = anchor_and_handle_toggled && !tool_data.select_anchor_toggled;
let released_from_toggle = tool_data.select_anchor_toggled && !anchor_and_handle_toggled; let released_from_toggle = tool_data.select_anchor_toggled && !anchor_and_handle_toggled;
@ -1707,6 +1804,11 @@ impl Fsm for PathToolFsmState {
if tool_data.drag_start_pos == previous_mouse { if tool_data.drag_start_pos == previous_mouse {
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] }); responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] });
} else { } 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 { match selection_shape {
SelectionShapeType::Box => { SelectionShapeType::Box => {
let bbox = [tool_data.drag_start_pos, previous_mouse]; let bbox = [tool_data.drag_start_pos, previous_mouse];
@ -1716,6 +1818,8 @@ impl Fsm for PathToolFsmState {
selection_change, selection_change,
tool_options.path_overlay_mode, tool_options.path_overlay_mode,
tool_data.frontier_handles_info.clone(), tool_data.frontier_handles_info.clone(),
tool_options.path_editing_mode.segment_editing_mode,
selection_mode,
); );
} }
SelectionShapeType::Lasso => shape_editor.select_all_in_shape( SelectionShapeType::Lasso => shape_editor.select_all_in_shape(
@ -1724,6 +1828,8 @@ impl Fsm for PathToolFsmState {
selection_change, selection_change,
tool_options.path_overlay_mode, tool_options.path_overlay_mode,
tool_data.frontier_handles_info.clone(), tool_data.frontier_handles_info.clone(),
tool_options.path_editing_mode.segment_editing_mode,
selection_mode,
), ),
} }
} }
@ -1735,6 +1841,7 @@ impl Fsm for PathToolFsmState {
(PathToolFsmState::Dragging { .. }, PathToolMessage::Escape | PathToolMessage::RightClick) => { (PathToolFsmState::Dragging { .. }, PathToolMessage::Escape | PathToolMessage::RightClick) => {
if tool_data.handle_drag_toggle && tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD { 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_points();
shape_editor.deselect_all_segments();
shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_handle_drag); shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_handle_drag);
tool_data.saved_points_before_handle_drag.clear(); tool_data.saved_points_before_handle_drag.clear();
@ -1783,8 +1890,17 @@ impl Fsm for PathToolFsmState {
let document_to_viewport = document.metadata().document_to_viewport; let document_to_viewport = document.metadata().document_to_viewport;
let previous_mouse = document_to_viewport.transform_point2(tool_data.previous_mouse_position); 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![] }); let selection_mode = match tool_action_data.preferences.get_selection_mode() {
SelectionMode::Directional => tool_data.calculate_selection_mode_from_direction(document.metadata()),
selection_mode => selection_mode,
};
if tool_data.drag_start_pos.distance(previous_mouse) < 1e-8 {
// If click happens inside of a shape then don't set selected nodes to empty
if document.click(input).is_none() {
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] });
}
} else { } else {
match selection_shape { match selection_shape {
SelectionShapeType::Box => { SelectionShapeType::Box => {
@ -1795,6 +1911,8 @@ impl Fsm for PathToolFsmState {
select_kind, select_kind,
tool_options.path_overlay_mode, tool_options.path_overlay_mode,
tool_data.frontier_handles_info.clone(), tool_data.frontier_handles_info.clone(),
tool_options.path_editing_mode.segment_editing_mode,
selection_mode,
); );
} }
SelectionShapeType::Lasso => shape_editor.select_all_in_shape( SelectionShapeType::Lasso => shape_editor.select_all_in_shape(
@ -1803,6 +1921,8 @@ impl Fsm for PathToolFsmState {
select_kind, select_kind,
tool_options.path_overlay_mode, tool_options.path_overlay_mode,
tool_data.frontier_handles_info.clone(), tool_data.frontier_handles_info.clone(),
tool_options.path_editing_mode.segment_editing_mode,
selection_mode,
), ),
} }
} }
@ -1823,32 +1943,31 @@ impl Fsm for PathToolFsmState {
tool_data.frontier_handles_info.clone(), tool_data.frontier_handles_info.clone(),
); );
let nearest_segment = tool_data.segment.clone();
if let Some(segment) = &mut tool_data.segment { if let Some(segment) = &mut tool_data.segment {
if !drag_occurred && !tool_data.molding_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 tool_data.delete_segment_pressed {
if let Some(vector_data) = document.network_interface.compute_modified_vector(segment.layer()) { 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()); shape_editor.dissolve_segment(responses, segment.layer(), &vector_data, segment.segment(), segment.points());
responses.add(DocumentMessage::EndTransaction);
} }
} else { } else {
segment.adjusted_insert_and_select(shape_editor, responses, extend_selection); segment.adjusted_insert_and_select(shape_editor, responses, extend_selection);
responses.add(DocumentMessage::EndTransaction);
} }
} else {
responses.add(DocumentMessage::EndTransaction);
} }
tool_data.segment = None; tool_data.segment = None;
tool_data.molding_info = None; tool_data.molding_info = None;
tool_data.molding_segment = false; tool_data.molding_segment = false;
tool_data.temporary_adjacent_handles_while_molding = None; tool_data.temporary_adjacent_handles_while_molding = None;
return PathToolFsmState::Ready;
} }
let segment_mode = tool_options.path_editing_mode.segment_editing_mode;
if let Some((layer, nearest_point)) = nearest_point { 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 !drag_occurred && extend_selection {
let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == point);
if clicked_selected && tool_data.last_clicked_point_was_selected { if clicked_selected && tool_data.last_clicked_point_was_selected {
shape_editor.selected_shape_state.entry(layer).or_default().deselect_point(nearest_point); shape_editor.selected_shape_state.entry(layer).or_default().deselect_point(nearest_point);
} else { } else {
@ -1856,6 +1975,49 @@ impl Fsm for PathToolFsmState {
} }
responses.add(OverlaysMessage::Draw); 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 { if tool_data.temporary_colinear_handles {
@ -1881,25 +2043,6 @@ impl Fsm for PathToolFsmState {
tool_data.select_anchor_toggled = false; tool_data.select_anchor_toggled = false;
} }
if let Some((layer, nearest_point)) = nearest_point {
if !drag_occurred && !extend_selection {
let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == point);
if 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.selected_shape_state.entry(layer).or_default().select_point(nearest_point);
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();
}
tool_data.snapping_axis = None; tool_data.snapping_axis = None;
tool_data.sliding_point_info = None; tool_data.sliding_point_info = None;
@ -1915,6 +2058,7 @@ impl Fsm for PathToolFsmState {
(_, PathToolMessage::Delete) => { (_, PathToolMessage::Delete) => {
// Delete the selected points and clean up overlays // Delete the selected points and clean up overlays
responses.add(DocumentMessage::AddTransaction); responses.add(DocumentMessage::AddTransaction);
shape_editor.delete_selected_segments(document, responses);
shape_editor.delete_selected_points(document, responses); shape_editor.delete_selected_points(document, responses);
responses.add(PathToolMessage::SelectionChanged); responses.add(PathToolMessage::SelectionChanged);
@ -1951,6 +2095,7 @@ impl Fsm for PathToolFsmState {
if let Some(layer) = document.click(input) { if let Some(layer) = document.click(input) {
// Select all points in the layer // Select all points in the layer
shape_editor.select_connected_anchors(document, layer, input.mouse.position); shape_editor.select_connected_anchors(document, layer, input.mouse.position);
responses.add(OverlaysMessage::Draw);
} }
PathToolFsmState::Ready PathToolFsmState::Ready
@ -1960,7 +2105,7 @@ impl Fsm for PathToolFsmState {
PathToolFsmState::Ready PathToolFsmState::Ready
} }
(_, PathToolMessage::NudgeSelectedPoints { delta_x, delta_y }) => { (_, PathToolMessage::NudgeSelectedPoints { delta_x, delta_y }) => {
shape_editor.move_selected_points( shape_editor.move_selected_points_and_segments(
tool_data.opposing_handle_lengths.take(), tool_data.opposing_handle_lengths.take(),
document, document,
(delta_x, delta_y).into(), (delta_x, delta_y).into(),
@ -2040,10 +2185,6 @@ enum SelectionStatus {
} }
impl SelectionStatus { impl SelectionStatus {
fn is_none(&self) -> bool {
self == &SelectionStatus::None
}
fn as_one(&self) -> Option<&SingleSelectedPoint> { fn as_one(&self) -> Option<&SingleSelectedPoint> {
match self { match self {
SelectionStatus::One(one) => Some(one), SelectionStatus::One(one) => Some(one),
@ -2173,9 +2314,7 @@ fn calculate_lock_angle(
} }
fn check_handle_over_adjacent_anchor(handle_id: ManipulatorPointId, vector_data: &VectorData) -> Option<PointId> { fn check_handle_over_adjacent_anchor(handle_id: ManipulatorPointId, vector_data: &VectorData) -> Option<PointId> {
let Some((anchor, handle_position)) = handle_id.get_anchor(&vector_data).zip(handle_id.get_position(vector_data)) else { let (anchor, handle_position) = handle_id.get_anchor(vector_data).zip(handle_id.get_position(vector_data))?;
return None;
};
let check_if_close = |point_id: &PointId| { let check_if_close = |point_id: &PointId| {
let Some(anchor_position) = vector_data.point_domain.position_from_id(*point_id) else { let Some(anchor_position) = vector_data.point_domain.position_from_id(*point_id) else {
@ -2184,7 +2323,7 @@ fn check_handle_over_adjacent_anchor(handle_id: ManipulatorPointId, vector_data:
(anchor_position - handle_position).length() < 10. (anchor_position - handle_position).length() < 10.
}; };
vector_data.connected_points(anchor).find(|point| check_if_close(point)) vector_data.connected_points(anchor).find(check_if_close)
} }
fn calculate_adjacent_anchor_tangent( fn calculate_adjacent_anchor_tangent(
currently_dragged_handle: ManipulatorPointId, currently_dragged_handle: ManipulatorPointId,
@ -2240,7 +2379,7 @@ fn calculate_adjacent_anchor_tangent(
}; };
let angle = shared_segment_handle let angle = shared_segment_handle
.get_position(&vector_data) .get_position(vector_data)
.zip(adjacent_anchor_position) .zip(adjacent_anchor_position)
.map(|(handle, anchor)| -(handle - anchor).angle_to(DVec2::X)); .map(|(handle, anchor)| -(handle - anchor).angle_to(DVec2::X));
@ -2251,7 +2390,14 @@ fn calculate_adjacent_anchor_tangent(
} }
} }
fn update_dynamic_hints(state: PathToolFsmState, responses: &mut VecDeque<Message>, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, tool_data: &PathToolData) { 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 // Condinting based on currently selected segment if it has any one g1 continuous handle
let hint_data = match state { let hint_data = match state {
@ -2285,12 +2431,27 @@ fn update_dynamic_hints(state: PathToolFsmState, responses: &mut VecDeque<Messag
drag_selected_hints.push(HintInfo::multi_keys([[Key::Control], [Key::Shift]], "Slide").prepend_plus()); drag_selected_hints.push(HintInfo::multi_keys([[Key::Control], [Key::Shift]], "Slide").prepend_plus());
} }
let mut hint_data = vec![ let mut hint_data = match (tool_data.segment.is_some(), tool_options.path_editing_mode.segment_editing_mode) {
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Select Point"), HintInfo::keys([Key::Shift], "Extend").prepend_plus()]), (true, true) => {
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), HintInfo::keys([Key::Control], "Lasso").prepend_plus()]), vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Insert Point on Segment")]), HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Select Segment"), HintInfo::keys([Key::Shift], "Extend").prepend_plus()]),
HintGroup(vec![HintInfo::keys_and_mouse([Key::Alt], MouseMotion::Lmb, "Delete Segment")]), 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 { if at_least_one_anchor_selected {
// TODO: Dynamically show either "Smooth" or "Sharp" based on the current state // TODO: Dynamically show either "Smooth" or "Sharp" based on the current state

View file

@ -180,14 +180,28 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
*selected.pivot = selected.mean_average_of_pivots(); *selected.pivot = selected.mean_average_of_pivots();
self.local_pivot = document.metadata().document_to_viewport.inverse().transform_point2(*selected.pivot); self.local_pivot = document.metadata().document_to_viewport.inverse().transform_point2(*selected.pivot);
self.grab_target = document.metadata().document_to_viewport.inverse().transform_point2(selected.mean_average_of_pivots()); self.grab_target = document.metadata().document_to_viewport.inverse().transform_point2(selected.mean_average_of_pivots());
} else if let Some(vector_data) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer)) { }
// Here vector data from all layers is not considered which can be a problem in pivot calculation
else if let Some(vector_data) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer)) {
*selected.original_transforms = OriginalTransforms::default(); *selected.original_transforms = OriginalTransforms::default();
let viewspace = document.metadata().transform_to_viewport(selected_layers[0]); let viewspace = document.metadata().transform_to_viewport(selected_layers[0]);
let selected_points = shape_editor.selected_points().collect::<Vec<_>>();
let selected_segments = shape_editor.selected_segments().collect::<HashSet<_>>();
let mut affected_points = shape_editor.selected_points().copied().collect::<Vec<_>>();
for (segment_id, _, start, end) in vector_data.segment_bezier_iter() {
if selected_segments.contains(&segment_id) {
affected_points.push(ManipulatorPointId::Anchor(start));
affected_points.push(ManipulatorPointId::Anchor(end));
}
}
let affected_point_refs = affected_points.iter().collect();
let get_location = |point: &&ManipulatorPointId| point.get_position(&vector_data).map(|position| viewspace.transform_point2(position)); let get_location = |point: &&ManipulatorPointId| point.get_position(&vector_data).map(|position| viewspace.transform_point2(position));
if let Some((new_pivot, grab_target)) = calculate_pivot(&selected_points, &vector_data, viewspace, |point: &ManipulatorPointId| get_location(&point)) { if let Some((new_pivot, grab_target)) = calculate_pivot(&affected_point_refs, &vector_data, viewspace, |point: &ManipulatorPointId| get_location(&point)) {
*selected.pivot = new_pivot; *selected.pivot = new_pivot;
self.local_pivot = document_to_viewport.inverse().transform_point2(*selected.pivot); self.local_pivot = document_to_viewport.inverse().transform_point2(*selected.pivot);
@ -390,7 +404,8 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
} }
TransformLayerMessage::BeginGRS { transform_type } => { TransformLayerMessage::BeginGRS { transform_type } => {
let selected_points: Vec<&ManipulatorPointId> = shape_editor.selected_points().collect(); let selected_points: Vec<&ManipulatorPointId> = shape_editor.selected_points().collect();
if (using_path_tool && selected_points.is_empty()) let selected_segments = shape_editor.selected_segments().collect::<Vec<_>>();
if (using_path_tool && selected_points.is_empty() && selected_segments.is_empty())
|| (!using_path_tool && !using_select_tool && !using_pen_tool && !using_shape_tool) || (!using_path_tool && !using_select_tool && !using_pen_tool && !using_shape_tool)
|| selected_layers.is_empty() || selected_layers.is_empty()
|| transform_type.equivalent_to(self.transform_operation) || transform_type.equivalent_to(self.transform_operation)

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<circle cx="6" cy="6" r="1.5" />
</svg>

After

Width:  |  Height:  |  Size: 102 B

View file

@ -13,6 +13,7 @@ import Checkmark from "@graphite-frontend/assets/icon-12px-solid/checkmark.svg";
import Clipped from "@graphite-frontend/assets/icon-12px-solid/clipped.svg"; import Clipped from "@graphite-frontend/assets/icon-12px-solid/clipped.svg";
import CloseX from "@graphite-frontend/assets/icon-12px-solid/close-x.svg"; import CloseX from "@graphite-frontend/assets/icon-12px-solid/close-x.svg";
import Delay from "@graphite-frontend/assets/icon-12px-solid/delay.svg"; import Delay from "@graphite-frontend/assets/icon-12px-solid/delay.svg";
import Dot from "@graphite-frontend/assets/icon-12px-solid/dot.svg";
import DropdownArrow from "@graphite-frontend/assets/icon-12px-solid/dropdown-arrow.svg"; import DropdownArrow from "@graphite-frontend/assets/icon-12px-solid/dropdown-arrow.svg";
import Edit12px from "@graphite-frontend/assets/icon-12px-solid/edit-12px.svg"; import Edit12px from "@graphite-frontend/assets/icon-12px-solid/edit-12px.svg";
import Empty12px from "@graphite-frontend/assets/icon-12px-solid/empty-12px.svg"; import Empty12px from "@graphite-frontend/assets/icon-12px-solid/empty-12px.svg";
@ -55,6 +56,7 @@ const SOLID_12PX = {
Clipped: { svg: Clipped, size: 12 }, Clipped: { svg: Clipped, size: 12 },
CloseX: { svg: CloseX, size: 12 }, CloseX: { svg: CloseX, size: 12 },
Delay: { svg: Delay, size: 12 }, Delay: { svg: Delay, size: 12 },
Dot: { svg: Dot, size: 12 },
DropdownArrow: { svg: DropdownArrow, size: 12 }, DropdownArrow: { svg: DropdownArrow, size: 12 },
Edit12px: { svg: Edit12px, size: 12 }, Edit12px: { svg: Edit12px, size: 12 },
Empty12px: { svg: Empty12px, size: 12 }, Empty12px: { svg: Empty12px, size: 12 },

View file

@ -12,12 +12,13 @@ use crate::vector::click_target::{ClickTargetType, FreePoint};
use crate::{AlphaBlending, Color, GraphicGroupTable}; use crate::{AlphaBlending, Color, GraphicGroupTable};
pub use attributes::*; pub use attributes::*;
use bezier_rs::ManipulatorGroup; use bezier_rs::ManipulatorGroup;
use core::borrow::Borrow;
use core::hash::Hash;
use dyn_any::DynAny; use dyn_any::DynAny;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
pub use indexed::VectorDataIndex; pub use indexed::VectorDataIndex;
use kurbo::{Affine, Rect, Shape}; use kurbo::{Affine, Rect, Shape};
pub use modification::*; pub use modification::*;
use std::borrow::Borrow;
use std::collections::HashMap; use std::collections::HashMap;
// TODO: Eventually remove this migration document upgrade code // TODO: Eventually remove this migration document upgrade code