diff --git a/editor/Cargo.toml b/editor/Cargo.toml index ffc95a927..1f6af599c 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -36,9 +36,9 @@ thiserror = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } bezier-rs = { workspace = true } +kurbo = { workspace = true } futures = { workspace = true } glam = { workspace = true } -kurbo = { workspace = true } derivative = { workspace = true } specta = { workspace = true } dyn-any = { workspace = true } diff --git a/editor/src/consts.rs b/editor/src/consts.rs index c943efe24..9adba6d90 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -102,7 +102,7 @@ pub const MANIPULATOR_GROUP_MARKER_SIZE: f64 = 6.; pub const SELECTION_THRESHOLD: f64 = 10.; pub const HIDE_HANDLE_DISTANCE: f64 = 3.; 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 HANDLE_LENGTH_FACTOR: f64 = 0.5; @@ -133,6 +133,7 @@ pub const SCALE_EFFECT: f64 = 0.5; // COLORS 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_GREEN: &str = "#63ce63"; pub const COLOR_OVERLAY_RED: &str = "#ef5454"; diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index c2465d00d..d24393031 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -212,7 +212,7 @@ 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 }), + 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(Escape); action_dispatch=PathToolMessage::Escape), entry!(KeyDown(KeyG); action_dispatch=PathToolMessage::GRS { key: KeyG }), diff --git a/editor/src/messages/portfolio/document/overlays/utility_functions.rs b/editor/src/messages/portfolio/document/overlays/utility_functions.rs index 92a16c907..1e012b867 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_functions.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_functions.rs @@ -124,8 +124,19 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle 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 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 { 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 transform = document.metadata().transform_to_viewport(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) { let Some(position) = vector_data.point_domain.position_from_id(point) else { continue }; diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 9587e1d63..a17d711a3 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -1,7 +1,7 @@ use super::utility_functions::overlay_canvas_context; 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, - COMPASS_ROSE_RING_INNER_DIAMETER, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_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_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 bezier_rs::{Bezier, Subpath}; @@ -581,6 +581,35 @@ impl OverlayContext { 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) { self.start_dpi_aware_transform(); diff --git a/editor/src/messages/portfolio/document/utility_types/transformation.rs b/editor/src/messages/portfolio/document/utility_types/transformation.rs index a8aff3805..18e969e33 100644 --- a/editor/src/messages/portfolio/document/utility_types/transformation.rs +++ b/editor/src/messages/portfolio/document/utility_types/transformation.rs @@ -88,6 +88,18 @@ impl OriginalTransforms { let Some(selected_points) = shape_editor.selected_points_in_layer(layer) else { 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 let anchor_ids = selected_points.iter().filter_map(|point| point.as_anchor()); diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index c1f7e99f0..24ce92a45 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -1,14 +1,15 @@ use super::graph_modification_utils::merge_layers; 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::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::misc::{PathSnapSource, SnapSource}; use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface; +use crate::messages::preferences::SelectionMode; use crate::messages::prelude::*; 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 bezier_rs::{Bezier, BezierHandles, Subpath, TValue}; use glam::{DAffine2, DVec2}; @@ -45,6 +46,7 @@ pub enum ManipulatorAngle { #[derive(Clone, Debug, Default)] pub struct SelectedLayerState { selected_points: HashSet, + selected_segments: HashSet, /// Keeps track of the current state; helps avoid unnecessary computation when called by [`ShapeState`]. ignore_handles: bool, ignore_anchors: bool, @@ -54,11 +56,27 @@ pub struct SelectedLayerState { } impl SelectedLayerState { - pub fn selected(&self) -> impl Iterator + '_ { + pub fn selected_points(&self) -> impl Iterator + '_ { self.selected_points.iter().copied() } - pub fn is_selected(&self, point: ManipulatorPointId) -> bool { + pub fn selected_segments(&self) -> impl Iterator + '_ { + 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) } @@ -66,10 +84,26 @@ impl SelectedLayerState { 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) { 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) { if self.ignore_handles != status { return; @@ -101,14 +135,6 @@ impl SelectedLayerState { 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; @@ -128,6 +154,12 @@ pub struct SelectedPointsInfo { pub vector_data: VectorData, } +#[derive(Debug)] +pub struct SelectedSegmentsInfo { + pub segments: Vec, + pub vector_data: VectorData, +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct ManipulatorPointInfo { pub layer: LayerNodeIdentifier, @@ -136,6 +168,7 @@ pub struct ManipulatorPointInfo { pub type OpposingHandleLengths = HashMap>; +#[derive(Clone)] pub struct ClosestSegment { layer: LayerNodeIdentifier, segment: SegmentId, @@ -159,6 +192,10 @@ impl ClosestSegment { self.points } + pub fn bezier(&self) -> Bezier { + self.bezier + } + pub fn closest_point_document(&self) -> DVec2 { self.bezier.evaluate(TValue::Parametric(self.t)) } @@ -473,7 +510,7 @@ impl ShapeState { if let Some(id) = selected.as_anchor() { for neighbor in vector_data.connected_points(id) { - if state.is_selected(ManipulatorPointId::Anchor(neighbor)) { + if state.is_point_selected(ManipulatorPointId::Anchor(neighbor)) { 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 selected_shape_state = self.selected_shape_state.get(&layer)?; - let already_selected = selected_shape_state.is_selected(manipulator_point_id); - - // Should we select or deselect the point? - let new_selected = if already_selected { !extend_selection } else { true }; + let already_selected = selected_shape_state.is_point_selected(manipulator_point_id); // 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); // 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 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 })); - } else { - let selected_shape_state = self.selected_shape_state.get_mut(&layer)?; - selected_shape_state.deselect_point(manipulator_point_id); - - return Some(None); + let retain_existing_selection = extend_selection || already_selected; + if !retain_existing_selection { + self.deselect_all_points(); + self.deselect_all_segments(); } + + // 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 } @@ -555,11 +584,16 @@ impl ShapeState { select_threshold: f64, path_overlay_mode: PathOverlayMode, frontier_handles_info: Option>>, + point_editing_mode: bool, ) -> Option<(bool, Option)> { 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) { let vector_data = network_interface.compute_modified_vector(layer)?; 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 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 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 while let Some(point) = selected_stack.pop() { let anchor_point = ManipulatorPointId::Anchor(point); - if !state.is_selected(anchor_point) { + if !state.is_point_selected(anchor_point) { state.select_point(anchor_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) { for state in self.selected_shape_state.values_mut() { self.ignore_anchors = !status; @@ -736,10 +777,18 @@ impl ShapeState { self.selected_shape_state.values().flat_map(|state| &state.selected_points) } + pub fn selected_segments(&self) -> impl Iterator { + self.selected_shape_state.values().flat_map(|state| &state.selected_segments) + } + pub fn selected_points_in_layer(&self, layer: LayerNodeIdentifier) -> Option<&HashSet> { self.selected_shape_state.get(&layer).map(|state| &state.selected_points) } + pub fn selected_segments_in_layer(&self, layer: LayerNodeIdentifier) -> Option<&HashSet> { + 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) { responses.add(GraphOperationMessage::Vector { layer, @@ -766,7 +815,7 @@ impl ShapeState { let Some((start, _end, bezier)) = vector_data.segment_points_from_id(segment) else { continue }; 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; } @@ -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)] - pub fn move_selected_points( + pub fn move_selected_points_and_segments( &self, handle_lengths: Option, document: &DocumentMessageHandler, @@ -1040,7 +1089,17 @@ impl ShapeState { }; 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) { continue; } @@ -1055,7 +1114,7 @@ impl ShapeState { }; 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; } @@ -1074,7 +1133,7 @@ impl ShapeState { 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. let handles = [handle, other]; let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false }; @@ -1125,12 +1184,12 @@ impl ShapeState { // ii) The anchor is not selected. 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 { 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 { [true, false] => handles[1], @@ -1208,11 +1267,15 @@ impl ShapeState { continue; }; + let selected_segments = &state.selected_segments; + for point in std::mem::take(&mut state.selected_points) { match point { ManipulatorPointId::Anchor(anchor) => { 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); } @@ -1257,6 +1320,8 @@ impl ShapeState { 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 let [handle_start, handle_end] = [start, end].map(|(handle, _)| { let handle = handle.opposite(); @@ -1304,6 +1369,20 @@ impl ShapeState { } } + pub fn delete_selected_segments(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque) { + 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) { for (&layer, state) in &self.selected_shape_state { let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue }; @@ -1748,6 +1827,7 @@ impl ShapeState { false } + #[allow(clippy::too_many_arguments)] pub fn select_all_in_shape( &mut self, network_interface: &NodeNetworkInterface, @@ -1755,13 +1835,17 @@ impl ShapeState { selection_change: SelectionChange, path_overlay_mode: PathOverlayMode, frontier_handles_info: Option>>, + 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::>(); 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_points(); + state.clear_segments(); } let vector_data = network_interface.compute_modified_vector(layer); @@ -1787,7 +1871,46 @@ impl ShapeState { None }; + // Selection segments 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))] { let Some(position) = position else { continue }; 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()) { let transformed_position = transform.transform_point2(position); diff --git a/editor/src/messages/tool/common_functionality/utility_functions.rs b/editor/src/messages/tool/common_functionality/utility_functions.rs index abe84224a..b7b95123c 100644 --- a/editor/src/messages/tool/common_functionality/utility_functions.rs +++ b/editor/src/messages/tool/common_functionality/utility_functions.rs @@ -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::tool_messages::path_tool::PathOverlayMode; use crate::messages::tool::utility_types::ToolType; -use bezier_rs::Bezier; +use bezier_rs::{Bezier, BezierHandles}; use glam::{DAffine2, DVec2}; use graphene_std::renderer::Quad; use graphene_std::text::{FontCache, load_face}; 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. 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( document: &DocumentMessageHandler, responses: &mut VecDeque, @@ -221,7 +287,7 @@ pub fn resize_bounds( let snap = Some(SizeSnapData { manager: snap_manager, 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 (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); } } +#[allow(clippy::too_many_arguments)] pub fn rotate_bounds( document: &DocumentMessageHandler, responses: &mut VecDeque, @@ -280,7 +347,7 @@ pub fn rotate_bounds( let mut selected = Selected::new( &mut bounds.original_transforms, &mut bounds.center_of_transformation, - &dragging_layers, + dragging_layers, responses, &document.network_interface, 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); } } @@ -365,7 +432,7 @@ pub fn transforming_transform_cage( let mut selected = Selected::new( &mut bounds.original_transforms, &mut bounds.center_of_transformation, - &layers_dragging, + layers_dragging, responses, &document.network_interface, None, @@ -423,7 +490,7 @@ pub fn transforming_transform_cage( } // 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. diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 51c1fa579..ff929ce9b 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -32,6 +32,7 @@ pub struct PathTool { #[derive(Default)] pub struct PathToolOptions { path_overlay_mode: PathOverlayMode, + path_editing_mode: PathEditingMode, } #[impl_message(Message, ToolMessage, Path)] @@ -69,6 +70,7 @@ pub enum PathToolMessage { lasso_select: Key, handle_drag_from_anchor: Key, drag_restore_handle: Key, + molding_in_segment_edit: Key, }, NudgeSelectedPoints { delta_x: f64, @@ -116,9 +118,26 @@ pub enum PathOverlayMode { FrontierHandles = 2, } +#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)] +pub struct PathEditingMode { + point_editing_mode: bool, + segment_editing_mode: bool, +} + +impl Default for PathEditingMode { + fn default() -> Self { + Self { + point_editing_mode: true, + segment_editing_mode: false, + } + } +} + #[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)] pub enum PathOptionsUpdate { OverlayModeType(PathOverlayMode), + PointEditingMode { enabled: bool }, + SegmentEditingMode { enabled: bool }, } impl ToolMetadata for PathTool { @@ -203,6 +222,19 @@ impl LayoutHolder for PathTool { .for_checkbox(&mut checkbox_id) .widget_holder(); + let point_editing_mode = CheckboxInput::new(self.options.path_editing_mode.point_editing_mode) + // TODO(Keavon): Replace with a real icon + .icon("Dot") + .tooltip("Point Editing Mode") + .on_update(|input| PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: input.checked }).into()) + .widget_holder(); + let segment_editing_mode = CheckboxInput::new(self.options.path_editing_mode.segment_editing_mode) + // TODO(Keavon): Replace with a real icon + .icon("Remove") + .tooltip("Segment Editing Mode") + .on_update(|input| PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: input.checked }).into()) + .widget_holder(); + let path_overlay_mode_widget = RadioInput::new(vec![ RadioEntryData::new("all") .icon("HandleVisibilityAll") @@ -227,8 +259,12 @@ impl LayoutHolder for PathTool { y_location, unrelated_seperator.clone(), colinear_handle_checkbox, - related_seperator, + related_seperator.clone(), colinear_handles_label, + unrelated_seperator.clone(), + point_editing_mode, + related_seperator.clone(), + segment_editing_mode, unrelated_seperator, path_overlay_mode_widget, ], @@ -246,6 +282,14 @@ impl<'a> MessageHandler> for PathToo self.options.path_overlay_mode = overlay_mode_type; responses.add(OverlaysMessage::Draw); } + PathOptionsUpdate::PointEditingMode { enabled } => { + self.options.path_editing_mode.point_editing_mode = enabled; + responses.add(OverlaysMessage::Draw); + } + PathOptionsUpdate::SegmentEditingMode { enabled } => { + self.options.path_editing_mode.segment_editing_mode = enabled; + responses.add(OverlaysMessage::Draw); + } }, ToolMessage::Path(PathToolMessage::ClosePath) => { responses.add(DocumentMessage::AddTransaction); @@ -405,6 +449,7 @@ struct PathToolData { angle: f64, opposite_handle_position: Option, last_clicked_point_was_selected: bool, + last_clicked_segment_was_selected: bool, snapping_axis: Option, alt_clicked_on_anchor: bool, alt_dragging_from_anchor: bool, @@ -416,6 +461,7 @@ struct PathToolData { frontier_handles_info: Option>>, adjacent_anchor_offset: Option, sliding_point_info: Option, + started_drawing_from_inside: bool, } impl PathToolData { @@ -476,6 +522,7 @@ impl PathToolData { self.selection_status = selection_status; } + // TODO: This function is for basic point select mode. We definitely need to make a new one for the segment select mode. #[allow(clippy::too_many_arguments)] fn mouse_down( &mut self, @@ -487,7 +534,10 @@ impl PathToolData { lasso_select: bool, handle_drag_from_anchor: bool, drag_zero_handle: bool, + molding_in_segment_edit: bool, path_overlay_mode: PathOverlayMode, + segment_editing_mode: bool, + point_editing_mode: bool, ) -> PathToolFsmState { self.double_click_handled = false; self.opposing_handle_lengths = None; @@ -510,6 +560,7 @@ impl PathToolData { SELECTION_THRESHOLD, path_overlay_mode, self.frontier_handles_info.clone(), + point_editing_mode, ) { responses.add(DocumentMessage::StartTransaction); @@ -593,25 +644,51 @@ impl PathToolData { } 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 - else if let Some(closed_segment) = &mut self.segment { + // We didn't find a point nearby, so we will see if there is a segment to select or insert a point on + else if let Some(segment) = shape_editor.upper_closest_segment(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD) { responses.add(DocumentMessage::StartTransaction); - // Calculating and storing handle positions - let handle1 = ManipulatorPointId::PrimaryHandle(closed_segment.segment()); - let handle2 = ManipulatorPointId::EndHandle(closed_segment.segment()); + if segment_editing_mode && !molding_in_segment_edit { + let layer = segment.layer(); + let segment_id = segment.segment(); + let already_selected = shape_editor.selected_shape_state.get(&layer).is_some_and(|state| state.is_segment_selected(segment_id)); + self.last_clicked_segment_was_selected = already_selected; - if let Some(vector_data) = document.network_interface.compute_modified_vector(closed_segment.layer()) { - if let (Some(pos1), Some(pos2)) = (handle1.get_position(&vector_data), handle2.get_position(&vector_data)) { - self.molding_info = Some((pos1, pos2)) + if !(already_selected && extend_selection) { + let retain_existing_selection = extend_selection || already_selected; + if !retain_existing_selection { + shape_editor.deselect_all_segments(); + shape_editor.deselect_all_points(); + } + + // Add to selected segments + if let Some(selected_shape_state) = shape_editor.selected_shape_state.get_mut(&layer) { + selected_shape_state.select_segment(segment_id); + } } - } - 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) { shape_editor.deselect_all_points(); + shape_editor.deselect_all_segments(); if extend_selection { responses.add(NodeGraphMessage::SelectedNodesAdd { nodes: vec![layer.to_node()] }); } else { @@ -620,9 +697,10 @@ impl PathToolData { self.drag_start_pos = 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 else { @@ -644,7 +722,7 @@ impl PathToolData { let transform = document.metadata().transform_to_document(layer); 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 }; layer_manipulators.insert(anchor); let Some([handle1, handle2]) = point.get_handle_pair(&vector_data) else { continue }; @@ -748,7 +826,7 @@ impl PathToolData { } // 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 layer_to_document = document.metadata().transform_to_document(*layer); @@ -875,7 +953,7 @@ impl PathToolData { let drag_start = self.drag_start_pos; 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 let delta = current_mouse - drag_start; @@ -887,7 +965,7 @@ impl PathToolData { _ => 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) { @@ -903,12 +981,12 @@ impl PathToolData { _ => 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 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; } @@ -930,6 +1008,28 @@ impl PathToolData { 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 { 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; 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); } else { let Some(axis) = self.snapping_axis else { return }; @@ -1201,7 +1301,7 @@ impl PathToolData { Axis::Y => DVec2::new(0., unsnapped_delta.y), _ => 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); } @@ -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) -> Self { 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 }; match (self, event) { @@ -1309,48 +1409,43 @@ impl Fsm for PathToolFsmState { match self { Self::Ready => { - // Check if there is no point nearby - 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); - } + tool_data.update_closest_segment(shape_editor, input.mouse.position, document, tool_options.path_overlay_mode); if let Some(closest_segment) = &tool_data.segment { - let perp = closest_segment.calculate_perp(document); - let point = closest_segment.closest_point(document.metadata()); + if tool_options.path_editing_mode.segment_editing_mode { + let transform = document.metadata().transform_to_viewport(closest_segment.layer()); - // 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.outline_overlay_bezier(closest_segment.bezier(), transform); - 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); + // Draw the anchors again + let display_anchors = overlay_context.visibility_settings.anchors(); + if display_anchors { + let start_pos = transform.transform_point2(closest_segment.bezier().start); + let end_pos = transform.transform_point2(closest_segment.bezier().end); + let start_id = closest_segment.points()[0]; + let end_id = closest_segment.points()[1]; + if let Some(shape_state) = shape_editor.selected_shape_state.get_mut(&closest_segment.layer()) { + overlay_context.manipulator_anchor(start_pos, shape_state.is_point_selected(ManipulatorPointId::Anchor(start_id)), None); + overlay_context.manipulator_anchor(end_pos, shape_state.is_point_selected(ManipulatorPointId::Anchor(end_id)), None); + } + } + } else { + let perp = closest_segment.calculate_perp(document); + let point = closest_segment.closest_point(document.metadata()); + + // 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 polygon = &tool_data.lasso_polygon; - match (selection_shape, selection_mode) { - (SelectionShapeType::Box, SelectionMode::Enclosed) => overlay_context.dashed_quad(quad, None, fill_color, Some(4.), Some(4.), Some(0.5)), - (SelectionShapeType::Lasso, SelectionMode::Enclosed) => overlay_context.dashed_polygon(polygon, None, fill_color, Some(4.), Some(4.), Some(0.5)), - (SelectionShapeType::Box, _) => overlay_context.quad(quad, None, fill_color), - (SelectionShapeType::Lasso, _) => overlay_context.polygon(polygon, None, fill_color), + match (selection_shape, selection_mode, tool_data.started_drawing_from_inside) { + // Don't draw box if it is from inside a shape and selection just began + (SelectionShapeType::Box, SelectionMode::Enclosed, false) => overlay_context.dashed_quad(quad, None, fill_color, Some(4.), Some(4.), Some(0.5)), + (SelectionShapeType::Lasso, SelectionMode::Enclosed, _) => overlay_context.dashed_polygon(polygon, None, fill_color, Some(4.), Some(4.), Some(0.5)), + (SelectionShapeType::Box, _, false) => overlay_context.quad(quad, None, fill_color), + (SelectionShapeType::Lasso, _, _) => overlay_context.polygon(polygon, None, fill_color), + (SelectionShapeType::Box, _, _) => {} } } Self::Dragging(_) => { @@ -1420,12 +1517,14 @@ impl Fsm for PathToolFsmState { lasso_select, handle_drag_from_anchor, drag_restore_handle, + molding_in_segment_edit, }, ) => { let extend_selection = input.keyboard.get(extend_selection as usize); let lasso_select = input.keyboard.get(lasso_select as usize); let handle_drag_from_anchor = input.keyboard.get(handle_drag_from_anchor as usize); let drag_zero_handle = input.keyboard.get(drag_restore_handle as usize); + let molding_in_segment_edit = input.keyboard.get(molding_in_segment_edit as usize); tool_data.selection_mode = None; tool_data.lasso_polygon.clear(); @@ -1439,7 +1538,10 @@ impl Fsm for PathToolFsmState { lasso_select, handle_drag_from_anchor, drag_zero_handle, + molding_in_segment_edit, tool_options.path_overlay_mode, + tool_options.path_editing_mode.segment_editing_mode, + tool_options.path_editing_mode.point_editing_mode, ) } ( @@ -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.started_drawing_from_inside = false; if selection_shape == SelectionShapeType::Lasso { extend_lasso(&mut tool_data.lasso_polygon, input.mouse.position); @@ -1516,12 +1619,6 @@ impl Fsm for PathToolFsmState { 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 initial_press = anchor_and_handle_toggled && !tool_data.select_anchor_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 { responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] }); } else { + let selection_mode = match tool_action_data.preferences.get_selection_mode() { + SelectionMode::Directional => tool_data.calculate_selection_mode_from_direction(document.metadata()), + selection_mode => selection_mode, + }; + match selection_shape { SelectionShapeType::Box => { let bbox = [tool_data.drag_start_pos, previous_mouse]; @@ -1716,6 +1818,8 @@ impl Fsm for PathToolFsmState { selection_change, tool_options.path_overlay_mode, tool_data.frontier_handles_info.clone(), + tool_options.path_editing_mode.segment_editing_mode, + selection_mode, ); } SelectionShapeType::Lasso => shape_editor.select_all_in_shape( @@ -1724,6 +1828,8 @@ impl Fsm for PathToolFsmState { selection_change, tool_options.path_overlay_mode, 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) => { 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(); @@ -1783,8 +1890,17 @@ impl Fsm for PathToolFsmState { let document_to_viewport = document.metadata().document_to_viewport; let previous_mouse = document_to_viewport.transform_point2(tool_data.previous_mouse_position); - if tool_data.drag_start_pos == previous_mouse { - responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] }); + + 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 { match selection_shape { SelectionShapeType::Box => { @@ -1795,6 +1911,8 @@ impl Fsm for PathToolFsmState { select_kind, tool_options.path_overlay_mode, tool_data.frontier_handles_info.clone(), + tool_options.path_editing_mode.segment_editing_mode, + selection_mode, ); } SelectionShapeType::Lasso => shape_editor.select_all_in_shape( @@ -1803,6 +1921,8 @@ impl Fsm for PathToolFsmState { select_kind, tool_options.path_overlay_mode, 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(), ); + let nearest_segment = tool_data.segment.clone(); + 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 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()); - responses.add(DocumentMessage::EndTransaction); } } else { 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.molding_info = None; tool_data.molding_segment = false; 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 { + let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == point); 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 { shape_editor.selected_shape_state.entry(layer).or_default().deselect_point(nearest_point); } else { @@ -1856,6 +1975,49 @@ impl Fsm for PathToolFsmState { } 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::>(); + } + + 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 { @@ -1881,25 +2043,6 @@ impl Fsm for PathToolFsmState { 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::>(); - } - - 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.sliding_point_info = None; @@ -1915,6 +2058,7 @@ impl Fsm for PathToolFsmState { (_, PathToolMessage::Delete) => { // Delete the selected points and clean up overlays responses.add(DocumentMessage::AddTransaction); + shape_editor.delete_selected_segments(document, responses); shape_editor.delete_selected_points(document, responses); responses.add(PathToolMessage::SelectionChanged); @@ -1951,6 +2095,7 @@ impl Fsm for PathToolFsmState { if let Some(layer) = document.click(input) { // Select all points in the layer shape_editor.select_connected_anchors(document, layer, input.mouse.position); + responses.add(OverlaysMessage::Draw); } PathToolFsmState::Ready @@ -1960,7 +2105,7 @@ impl Fsm for PathToolFsmState { PathToolFsmState::Ready } (_, PathToolMessage::NudgeSelectedPoints { delta_x, delta_y }) => { - shape_editor.move_selected_points( + shape_editor.move_selected_points_and_segments( tool_data.opposing_handle_lengths.take(), document, (delta_x, delta_y).into(), @@ -2040,10 +2185,6 @@ enum SelectionStatus { } impl SelectionStatus { - fn is_none(&self) -> bool { - self == &SelectionStatus::None - } - fn as_one(&self) -> Option<&SingleSelectedPoint> { match self { 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 { - let Some((anchor, handle_position)) = handle_id.get_anchor(&vector_data).zip(handle_id.get_position(vector_data)) else { - return None; - }; + let (anchor, handle_position) = handle_id.get_anchor(vector_data).zip(handle_id.get_position(vector_data))?; let check_if_close = |point_id: &PointId| { let Some(anchor_position) = vector_data.point_domain.position_from_id(*point_id) else { @@ -2184,7 +2323,7 @@ fn check_handle_over_adjacent_anchor(handle_id: ManipulatorPointId, vector_data: (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( currently_dragged_handle: ManipulatorPointId, @@ -2240,7 +2379,7 @@ fn calculate_adjacent_anchor_tangent( }; let angle = shared_segment_handle - .get_position(&vector_data) + .get_position(vector_data) .zip(adjacent_anchor_position) .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, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, tool_data: &PathToolData) { +fn update_dynamic_hints( + state: PathToolFsmState, + responses: &mut VecDeque, + shape_editor: &mut ShapeState, + document: &DocumentMessageHandler, + tool_data: &PathToolData, + tool_options: &PathToolOptions, +) { // Condinting based on currently selected segment if it has any one g1 continuous handle let hint_data = match state { @@ -2285,12 +2431,27 @@ fn update_dynamic_hints(state: PathToolFsmState, responses: &mut VecDeque { + vec![ + HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Select Segment"), HintInfo::keys([Key::Shift], "Extend").prepend_plus()]), + HintGroup(vec![HintInfo::keys_and_mouse([Key::KeyA], MouseMotion::Lmb, "Mold Segment")]), + ] + } + (true, false) => { + vec![ + HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Insert Point on Segment")]), + HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Mold Segment")]), + HintGroup(vec![HintInfo::keys_and_mouse([Key::Alt], MouseMotion::Lmb, "Delete Segment")]), + ] + } + (false, _) => { + vec![ + HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Select Point"), HintInfo::keys([Key::Shift], "Extend").prepend_plus()]), + HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), HintInfo::keys([Key::Control], "Lasso").prepend_plus()]), + ] + } + }; if at_least_one_anchor_selected { // TODO: Dynamically show either "Smooth" or "Sharp" based on the current state diff --git a/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs b/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs index 287faf00d..104f1a66a 100644 --- a/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs +++ b/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs @@ -180,14 +180,28 @@ impl MessageHandler> for TransformLayer *selected.pivot = selected.mean_average_of_pivots(); 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()); - } 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(); let viewspace = document.metadata().transform_to_viewport(selected_layers[0]); - let selected_points = shape_editor.selected_points().collect::>(); + + let selected_segments = shape_editor.selected_segments().collect::>(); + + let mut affected_points = shape_editor.selected_points().copied().collect::>(); + + 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)); - 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; self.local_pivot = document_to_viewport.inverse().transform_point2(*selected.pivot); @@ -390,7 +404,8 @@ impl MessageHandler> for TransformLayer } TransformLayerMessage::BeginGRS { transform_type } => { 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::>(); + if (using_path_tool && selected_points.is_empty() && selected_segments.is_empty()) || (!using_path_tool && !using_select_tool && !using_pen_tool && !using_shape_tool) || selected_layers.is_empty() || transform_type.equivalent_to(self.transform_operation) diff --git a/frontend/assets/icon-12px-solid/dot.svg b/frontend/assets/icon-12px-solid/dot.svg new file mode 100644 index 000000000..63f4a4e05 --- /dev/null +++ b/frontend/assets/icon-12px-solid/dot.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/utility-functions/icons.ts b/frontend/src/utility-functions/icons.ts index d3f7584c9..3e57c4acd 100644 --- a/frontend/src/utility-functions/icons.ts +++ b/frontend/src/utility-functions/icons.ts @@ -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 CloseX from "@graphite-frontend/assets/icon-12px-solid/close-x.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 Edit12px from "@graphite-frontend/assets/icon-12px-solid/edit-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 }, CloseX: { svg: CloseX, size: 12 }, Delay: { svg: Delay, size: 12 }, + Dot: { svg: Dot, size: 12 }, DropdownArrow: { svg: DropdownArrow, size: 12 }, Edit12px: { svg: Edit12px, size: 12 }, Empty12px: { svg: Empty12px, size: 12 }, diff --git a/node-graph/gcore/src/vector/vector_data.rs b/node-graph/gcore/src/vector/vector_data.rs index 4a6a66034..ed7c32db3 100644 --- a/node-graph/gcore/src/vector/vector_data.rs +++ b/node-graph/gcore/src/vector/vector_data.rs @@ -12,12 +12,13 @@ use crate::vector::click_target::{ClickTargetType, FreePoint}; use crate::{AlphaBlending, Color, GraphicGroupTable}; pub use attributes::*; use bezier_rs::ManipulatorGroup; +use core::borrow::Borrow; +use core::hash::Hash; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; pub use indexed::VectorDataIndex; use kurbo::{Affine, Rect, Shape}; pub use modification::*; -use std::borrow::Borrow; use std::collections::HashMap; // TODO: Eventually remove this migration document upgrade code