diff --git a/editor/src/consts.rs b/editor/src/consts.rs index 87720830..9d50dfba 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -45,6 +45,8 @@ pub const BOUNDS_ROTATE_THRESHOLD: f64 = 20.; pub const MANIPULATOR_GROUP_MARKER_SIZE: f64 = 5.; pub const SELECTION_THRESHOLD: f64 = 10.; pub const HIDE_HANDLE_DISTANCE: f64 = 3.; +pub const INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE: f64 = 50.; +pub const INSERT_POINT_ON_SEGMENT_TOO_CLOSE_DISTANCE: f64 = 5.; // Pen tool pub const CREATE_CURVE_THRESHOLD: f64 = 5.; diff --git a/editor/src/messages/input_mapper/default_mapping.rs b/editor/src/messages/input_mapper/default_mapping.rs index 2aed6d66..8fc0ef98 100644 --- a/editor/src/messages/input_mapper/default_mapping.rs +++ b/editor/src/messages/input_mapper/default_mapping.rs @@ -181,7 +181,12 @@ pub fn default_mapping() -> Mapping { entry!(KeyDown(Backspace); modifiers=[Accel], action_dispatch=PathToolMessage::DeleteAndBreakPath), entry!(KeyDown(Delete); modifiers=[Accel, Shift], action_dispatch=PathToolMessage::BreakPath), entry!(KeyDown(Backspace); modifiers=[Accel, Shift], action_dispatch=PathToolMessage::BreakPath), - entry!(KeyDown(Lmb); action_dispatch=PathToolMessage::DragStart { add_to_selection: Shift }), + entry!(KeyDown(Lmb); action_dispatch=PathToolMessage::MouseDown { ctrl: Control, shift: Shift }), + entry!(KeyDown(Rmb); action_dispatch=PathToolMessage::RightClick), + entry!(KeyDown(Escape); action_dispatch=PathToolMessage::Escape), + entry!(KeyDown(KeyG); action_dispatch=PathToolMessage::GRS { key: KeyG }), + entry!(KeyDown(KeyR); action_dispatch=PathToolMessage::GRS { key: KeyR }), + entry!(KeyDown(KeyS); action_dispatch=PathToolMessage::GRS { key: KeyS }), entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=PathToolMessage::PointerMove { alt: Alt, shift: Shift }), entry!(KeyDown(Delete); action_dispatch=PathToolMessage::Delete), entry!(KeyDown(KeyA); modifiers=[Control], action_dispatch=PathToolMessage::SelectAllPoints), @@ -190,7 +195,7 @@ pub fn default_mapping() -> Mapping { entry!(KeyDown(Enter); action_dispatch=PathToolMessage::Enter { add_to_selection: Shift }), - entry!(DoubleClick(MouseButton::Left); action_dispatch=PathToolMessage::InsertPoint), + entry!(DoubleClick(MouseButton::Left); action_dispatch=PathToolMessage::FlipSharp), entry!(KeyDown(ArrowRight); action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: NUDGE_AMOUNT, delta_y: 0. }), entry!(KeyDown(ArrowRight); modifiers=[Shift], action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: BIG_NUDGE_AMOUNT, delta_y: 0. }), entry!(KeyDown(ArrowRight); modifiers=[ArrowUp], action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }), diff --git a/editor/src/messages/portfolio/document/overlays/utility_functions.rs b/editor/src/messages/portfolio/document/overlays/utility_functions.rs index be00c45f..09a7d42a 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_functions.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_functions.rs @@ -48,7 +48,7 @@ pub fn path_overlays(document: &DocumentMessageHandler, shape_editor: &mut Shape overlay_context.handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::OutHandle))); } - overlay_context.square(anchor_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::Anchor))); + overlay_context.square(anchor_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::Anchor)), None); } } } @@ -66,14 +66,14 @@ pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: & let anchor = first_manipulator.anchor; let anchor_position = transform.transform_point2(anchor); - overlay_context.square(anchor_position, is_selected(selected, ManipulatorPointId::new(first_manipulator.id, SelectedType::Anchor))); + overlay_context.square(anchor_position, is_selected(selected, ManipulatorPointId::new(first_manipulator.id, SelectedType::Anchor)), None); }; if let Some(last_manipulator) = manipulator_groups.last() { let anchor = last_manipulator.anchor; let anchor_position = transform.transform_point2(anchor); - overlay_context.square(anchor_position, is_selected(selected, ManipulatorPointId::new(last_manipulator.id, SelectedType::Anchor))); + overlay_context.square(anchor_position, is_selected(selected, ManipulatorPointId::new(last_manipulator.id, SelectedType::Anchor)), None); }; } } diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index b7690be1..e99113aa 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -54,21 +54,23 @@ impl OverlayContext { .expect("draw circle"); let fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE }; - self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(&fill)); + self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(fill)); self.render_context.fill(); self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(COLOR_OVERLAY_BLUE)); self.render_context.stroke(); } - pub fn square(&mut self, position: DVec2, selected: bool) { + pub fn square(&mut self, position: DVec2, selected: bool, color_selected: Option<&str>) { + let color_selected = color_selected.unwrap_or(COLOR_OVERLAY_BLUE); + self.render_context.begin_path(); let corner = position - DVec2::splat(MANIPULATOR_GROUP_MARKER_SIZE) / 2.; self.render_context .rect(corner.x.round(), corner.y.round(), MANIPULATOR_GROUP_MARKER_SIZE, MANIPULATOR_GROUP_MARKER_SIZE); - let fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE }; - self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(&fill)); + let fill = if selected { color_selected } else { COLOR_OVERLAY_WHITE }; + self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(fill)); self.render_context.fill(); - self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(COLOR_OVERLAY_BLUE)); + self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(color_selected)); self.render_context.stroke(); } diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index 7aca2318..84f1e41f 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -194,6 +194,14 @@ pub fn get_text(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> O Some((text, font, font_size)) } +pub fn get_stroke_width(layer: LayerNodeIdentifier, network: &NodeNetwork) -> Option { + if let TaggedValue::F32(width) = NodeGraphLayer::new(layer, network)?.find_input("Stroke", 2)? { + Some(*width) + } else { + None + } +} + /// Checks if a specified layer uses an upstream node matching the given name. pub fn is_layer_fed_by_node_of_name(layer: LayerNodeIdentifier, document_network: &NodeNetwork, node_name: &str) -> bool { NodeGraphLayer::new(layer, document_network).is_some_and(|layer| layer.find_node_inputs(node_name).is_some()) diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index a2f37c6a..eac7fecb 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -1,6 +1,6 @@ use super::graph_modification_utils; use super::snapping::{group_smooth, SnapCandidatePoint, SnapData, SnapManager, SnappedPoint}; -use crate::consts::DRAG_THRESHOLD; +use crate::consts::{DRAG_THRESHOLD, INSERT_POINT_ON_SEGMENT_TOO_CLOSE_DISTANCE}; use crate::messages::portfolio::document::node_graph::VectorDataModification; use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use crate::messages::portfolio::document::utility_types::misc::{GeometrySnapSource, SnapSource}; @@ -9,6 +9,7 @@ use crate::messages::tool::common_functionality::graph_modification_utils::{get_ use bezier_rs::{Bezier, ManipulatorGroup, TValue}; use graph_craft::document::NodeNetwork; +use graphene_core::transform::Transform; use graphene_core::uuid::ManipulatorGroupId; use graphene_core::vector::{ManipulatorPointId, SelectedType}; @@ -64,6 +65,190 @@ pub struct ManipulatorPointInfo { pub type OpposingHandleLengths = HashMap>>; +struct ClosestSegmentInfo { + pub bezier: Bezier, + pub t: f64, + pub bezier_point_to_viewport: DVec2, + pub layer_scale: DVec2, +} + +pub struct ClosestSegment { + layer: LayerNodeIdentifier, + start: ManipulatorGroupId, + end: ManipulatorGroupId, + bezier: Bezier, + t: f64, + t_min: f64, + t_max: f64, + scale: f64, + stroke_width: f64, + bezier_point_to_viewport: DVec2, + has_start_handle: bool, + has_end_handle: bool, +} + +impl ClosestSegment { + fn new(info: ClosestSegmentInfo, layer: LayerNodeIdentifier, document_network: &NodeNetwork, start: ManipulatorGroup, end: ManipulatorGroup) -> Self { + // 0.5 is half the line (center to side) but it's convenient to allow targetting slightly more than half the line width + const STROKE_WIDTH_PERCENT: f64 = 0.7; + + let bezier = info.bezier; + let t = info.t; + let (t_min, t_max) = ClosestSegment::t_min_max(&bezier, info.layer_scale); + let stroke_width = graph_modification_utils::get_stroke_width(layer, document_network).unwrap_or(1.) as f64 * STROKE_WIDTH_PERCENT; + let bezier_point_to_viewport = info.bezier_point_to_viewport; + let has_start_handle = start.has_out_handle(); + let has_end_handle = end.has_in_handle(); + + Self { + layer, + start: start.id, + end: end.id, + bezier, + t, + t_min, + t_max, + scale: 1., + stroke_width, + bezier_point_to_viewport, + has_start_handle, + has_end_handle, + } + } + + pub fn layer(&self) -> LayerNodeIdentifier { + self.layer + } + + pub fn closest_point_to_viewport(&self) -> DVec2 { + self.bezier_point_to_viewport + } + + fn t_min_max(bezier: &Bezier, layer_scale: DVec2) -> (f64, f64) { + let length = bezier.apply_transformation(|point| point * layer_scale).length(Some(100)); + let too_close_t = (INSERT_POINT_ON_SEGMENT_TOO_CLOSE_DISTANCE / length).min(0.5); + + let t_min_euclidean = too_close_t; + let t_max_euclidean = 1. - too_close_t; + + // We need parametric values because they are faster to calculate + let t_min = bezier.euclidean_to_parametric(t_min_euclidean, 0.001); + let t_max = bezier.euclidean_to_parametric(t_max_euclidean, 0.001); + + (t_min, t_max) + } + + /// Updates this [`ClosestSegment`] with the viewport-space location of the closest point on the segment to the given mouse position. + pub fn update_closest_point(&mut self, document_metadata: &DocumentMetadata, mouse_position: DVec2) { + let transform = document_metadata.transform_to_viewport(self.layer); + let layer_m_pos = transform.inverse().transform_point2(mouse_position); + + self.scale = document_metadata.document_to_viewport.decompose_scale().x.max(1.); + + // Linear approximation of parametric t-value ranges: + let t_min = self.t_min / self.scale; + let t_max = 1. - ((1. - self.t_max) / self.scale); + let t = self.bezier.project(layer_m_pos, None).max(t_min).min(t_max); + self.t = t; + + let bezier_point = self.bezier.evaluate(TValue::Parametric(t)); + let bezier_point = transform.transform_point2(bezier_point); + self.bezier_point_to_viewport = bezier_point; + } + + pub fn distance_squared(&self, mouse_position: DVec2) -> f64 { + self.bezier_point_to_viewport.distance_squared(mouse_position) + } + + pub fn split(&self) -> [Bezier; 2] { + self.bezier.split(TValue::Parametric(self.t)) + } + + pub fn too_far(&self, mouse_position: DVec2, tolerance: f64) -> bool { + let dist_sq = self.distance_squared(mouse_position); + let stroke_width = self.scale * self.stroke_width; + let stroke_width_sq = stroke_width * stroke_width; + let tolerance_sq = tolerance * tolerance; + (stroke_width_sq + tolerance_sq) < dist_sq + } + + pub fn adjust_start_handle(&self, responses: &mut VecDeque) { + if !self.has_start_handle { + return; + } + + let [first, _] = self.split(); + let point = ManipulatorPointId::new(self.start, SelectedType::OutHandle); + + // `first.handle_start()` should always be expected + let position = first.handle_start().unwrap_or(first.start()); + + let out_handle = GraphOperationMessage::Vector { + layer: self.layer, + modification: VectorDataModification::SetManipulatorPosition { point, position }, + }; + responses.add(out_handle); + } + + pub fn adjust_end_handle(&self, responses: &mut VecDeque) { + if !self.has_end_handle { + return; + } + + let [_, second] = self.split(); + let point = ManipulatorPointId::new(self.end, SelectedType::InHandle); + + // `second.handle_end()` should not be expected in the quadratic case + let position = if second.handles.is_cubic() { second.handle_end() } else { second.handle_start() }; + let position = position.unwrap_or(second.end()); + + let in_handle = GraphOperationMessage::Vector { + layer: self.layer, + modification: VectorDataModification::SetManipulatorPosition { point, position }, + }; + responses.add(in_handle); + } + + /// Inserts the point that this [`ClosestSegment`] currently has. Returns the [`ManipulatorGroupId`] of the inserted point. + pub fn insert_point(&self, responses: &mut VecDeque) -> ManipulatorGroupId { + let [first, second] = self.split(); + + let layer = self.layer; + let anchor = first.end(); + + // `first.handle_end()` should not be expected in the quadratic case + let in_handle = if first.handles.is_cubic() { first.handle_end() } else { first.handle_start() }; + let out_handle = second.handle_start(); + let (in_handle, out_handle) = match (self.has_start_handle, self.has_end_handle) { + (false, false) => (None, None), + (false, true) => (in_handle, if second.handles.is_cubic() { out_handle } else { None }), + (true, false) => (if first.handles.is_cubic() { in_handle } else { None }, out_handle), + (true, true) => (in_handle, out_handle), + }; + + let manipulator_group = ManipulatorGroup::new(anchor, in_handle, out_handle); + let modification = VectorDataModification::AddManipulatorGroup { + manipulator_group, + after_id: self.start, + }; + let insert = GraphOperationMessage::Vector { layer, modification }; + responses.add(insert); + + manipulator_group.id + } + + pub fn adjusted_insert(&self, responses: &mut VecDeque) -> ManipulatorGroupId { + self.adjust_start_handle(responses); + self.adjust_end_handle(responses); + self.insert_point(responses) + } + + pub fn adjusted_insert_and_select(&self, shape_editor: &mut ShapeState, responses: &mut VecDeque, add_to_selection: bool) { + let id = self.adjusted_insert(responses); + shape_editor.select_anchor_point_by_id(self.layer, id, add_to_selection) + } +} + // TODO Consider keeping a list of selected manipulators to minimize traversals of the layers impl ShapeState { // Snap, returning a viewport delta @@ -97,7 +282,7 @@ impl ShapeState { } else { SnapSource::Geometry(GeometrySnapSource::Sharp) }; - let Some(position) = handle.get_position(&group) else { continue }; + let Some(position) = handle.get_position(group) else { continue }; let mut point = SnapCandidatePoint::new_source(to_document.transform_point2(position) + mouse_delta, source); let mut push_neighbor = |group: ManipulatorGroup| { @@ -133,23 +318,30 @@ impl ShapeState { document.metadata.document_to_viewport.transform_vector2(offset) } - /// Select the first point within the selection threshold. + pub fn select_anchor_point_by_id(&mut self, layer: LayerNodeIdentifier, id: ManipulatorGroupId, add_to_selection: bool) { + if !add_to_selection { + self.deselect_all(); + } + let point = ManipulatorPointId::new(id, SelectedType::Anchor); + let Some(selected_state) = self.selected_shape_state.get_mut(&layer) else { return }; + selected_state.select_point(point); + } + + /// Select/deselect the first point within the selection threshold. /// Returns a tuple of the points if found and the offset, or `None` otherwise. - pub fn select_point( + pub fn change_point_selection( &mut self, document_network: &NodeNetwork, document_metadata: &DocumentMetadata, mouse_position: DVec2, select_threshold: f64, add_to_selection: bool, - ) -> Option { + ) -> Option> { if self.selected_shape_state.is_empty() { return None; } if let Some((layer, manipulator_point_id)) = self.find_nearest_point_indices(document_network, document_metadata, mouse_position, select_threshold) { - trace!("Selecting... manipulator point: {manipulator_point_id:?}"); - let subpaths = get_subpaths(layer, document_network)?; let manipulator_group = get_manipulator_groups(subpaths).find(|group| group.id == manipulator_point_id.group)?; let point_position = manipulator_point_id.manipulator_type.get_position(manipulator_group)?; @@ -160,6 +352,9 @@ impl ShapeState { // Should we select or deselect the point? let new_selected = if already_selected { !add_to_selection } else { true }; + // Offset to snap the selected point to the cursor + let offset = mouse_position - 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 = add_to_selection || already_selected; @@ -171,21 +366,18 @@ impl ShapeState { let selected_shape_state = self.selected_shape_state.get_mut(&layer)?; selected_shape_state.select_point(manipulator_point_id); - // Offset to snap the selected point to the cursor - let offset = mouse_position - document_metadata.transform_to_viewport(layer).transform_point2(point_position); - 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(SelectedPointsInfo { points, offset }); + return Some(Some(SelectedPointsInfo { points, offset })); } else { let selected_shape_state = self.selected_shape_state.get_mut(&layer)?; selected_shape_state.deselect_point(manipulator_point_id); - return None; + return Some(None); } } None @@ -219,6 +411,13 @@ impl ShapeState { self.selected_shape_state.keys() } + /// iterate over all selected layers in order from top to bottom + /// # WARN + /// iterate over all layers of the document + pub fn sorted_selected_layers<'a>(&'a self, document_metadata: &'a DocumentMetadata) -> impl Iterator + 'a { + document_metadata.all_layers().filter(|layer| self.selected_shape_state.contains_key(layer)) + } + pub fn has_selected_layers(&self) -> bool { !self.selected_shape_state.is_empty() } @@ -228,7 +427,6 @@ impl ShapeState { self.iter(document_network).flat_map(|subpaths| get_manipulator_groups(subpaths)) } - // Sets the selected points to all points for the corresponding intersection pub fn select_all_anchors(&mut self, document_network: &NodeNetwork, layer: LayerNodeIdentifier) { let Some(subpaths) = get_subpaths(layer, document_network) else { return }; let Some(state) = self.selected_shape_state.get_mut(&layer) else { return }; @@ -895,24 +1093,21 @@ impl ShapeState { } /// Find the `t` value along the path segment we have clicked upon, together with that segment ID. - fn closest_segment( - &self, - document_network: &NodeNetwork, - document_metadata: &DocumentMetadata, - layer: LayerNodeIdentifier, - position: glam::DVec2, - tolerance: f64, - ) -> Option<(ManipulatorGroupId, ManipulatorGroupId, Bezier, f64)> { + fn closest_segment(&self, document_network: &NodeNetwork, document_metadata: &DocumentMetadata, layer: LayerNodeIdentifier, position: glam::DVec2, tolerance: f64) -> Option { let transform = document_metadata.transform_to_viewport(layer); let layer_pos = transform.inverse().transform_point2(position); - let projection_options = bezier_rs::ProjectionOptions { lut_size: 5, ..Default::default() }; - let mut result = None; + let scale = document_metadata.document_to_viewport.decompose_scale().x; + let tolerance = tolerance + 0.5 * scale; // make more talerance at large scale + let lut_size = ((5. + scale) as usize).min(20); // need more precision at large scale + let projection_options = bezier_rs::ProjectionOptions { lut_size, ..Default::default() }; + + let mut closest = None; let mut closest_distance_squared: f64 = tolerance * tolerance; let subpaths = get_subpaths(layer, document_network)?; - for subpath in subpaths { + for (subpath_index, subpath) in subpaths.iter().enumerate() { for (manipulator_index, bezier) in subpath.iter().enumerate() { let t = bezier.project(layer_pos, Some(projection_options)); let layerspace = bezier.evaluate(TValue::Parametric(t)); @@ -922,50 +1117,41 @@ impl ShapeState { if distance_squared < closest_distance_squared { closest_distance_squared = distance_squared; - let start = subpath.manipulator_groups()[manipulator_index]; - let end = subpath.manipulator_groups()[(manipulator_index + 1) % subpath.len()]; - result = Some((start.id, end.id, bezier, t)); + + let info = ClosestSegmentInfo { + bezier, + t, + // needs for correct length calc when there is non 1x1 layer scale + layer_scale: transform.decompose_scale() / scale, + bezier_point_to_viewport: screenspace, + }; + closest = Some(((subpath_index, manipulator_index), info)) } } } - result + closest.map(|((subpath_index, manipulator_index), info)| { + let subpath = &subpaths[subpath_index]; + let start = subpath.manipulator_groups()[manipulator_index]; + let end = subpath.manipulator_groups()[(manipulator_index + 1) % subpath.len()]; + ClosestSegment::new(info, layer, document_network, start, end) + }) + } + + /// find closest to the position segment on selected layers. If there is more than one layers with close enough segment it return upper from them + pub fn upper_closest_segment(&self, document_network: &NodeNetwork, document_metadata: &DocumentMetadata, position: glam::DVec2, tolerance: f64) -> Option { + let closest_seg = |layer| self.closest_segment(document_network, document_metadata, layer, position, tolerance); + match self.selected_shape_state.len() { + 0 => None, + 1 => self.selected_layers().next().copied().and_then(closest_seg), + _ => self.sorted_selected_layers(document_metadata).find_map(closest_seg), + } } /// Handles the splitting of a curve to insert new points (which can be activated by double clicking on a curve with the Path tool). pub fn split(&self, document_network: &NodeNetwork, document_metadata: &DocumentMetadata, position: glam::DVec2, tolerance: f64, responses: &mut VecDeque) { - for &layer in self.selected_layers() { - if let Some((start, end, bezier, t)) = self.closest_segment(document_network, document_metadata, layer, position, tolerance) { - let [first, second] = bezier.split(TValue::Parametric(t)); - - // Adjust the first manipulator group's out handle - let point = ManipulatorPointId::new(start, SelectedType::OutHandle); - let position = first.handle_start().unwrap_or(first.start()); - let out_handle = GraphOperationMessage::Vector { - layer, - modification: VectorDataModification::SetManipulatorPosition { point, position }, - }; - responses.add(out_handle); - - // Insert a new manipulator group between the existing ones - let manipulator_group = ManipulatorGroup::new(first.end(), first.handle_end(), second.handle_start()); - let insert = GraphOperationMessage::Vector { - layer, - modification: VectorDataModification::AddManipulatorGroup { manipulator_group, after_id: start }, - }; - responses.add(insert); - - // Adjust the last manipulator group's in handle - let point = ManipulatorPointId::new(end, SelectedType::InHandle); - let position = second.handle_end().unwrap_or(second.end()); - let in_handle = GraphOperationMessage::Vector { - layer, - modification: VectorDataModification::SetManipulatorPosition { point, position }, - }; - responses.add(in_handle); - - return; - } + if let Some(segment) = self.upper_closest_segment(document_network, document_metadata, position, tolerance) { + segment.adjusted_insert(responses); } } diff --git a/editor/src/messages/tool/common_functionality/snapping.rs b/editor/src/messages/tool/common_functionality/snapping.rs index 64540c72..419c30f2 100644 --- a/editor/src/messages/tool/common_functionality/snapping.rs +++ b/editor/src/messages/tool/common_functionality/snapping.rs @@ -334,7 +334,7 @@ impl SnapManager { let viewport = to_viewport.transform_point2(ind.snapped_point_document); overlay_context.text(&format!("{:?} to {:?}", ind.source, ind.target), viewport - DVec2::new(0., 5.), "rgba(0, 0, 0, 0.8)", 3.); - overlay_context.square(viewport, true); + overlay_context.square(viewport, true, None); } } diff --git a/editor/src/messages/tool/common_functionality/transformation_cage.rs b/editor/src/messages/tool/common_functionality/transformation_cage.rs index 296608ae..8b2c1b8c 100644 --- a/editor/src/messages/tool/common_functionality/transformation_cage.rs +++ b/editor/src/messages/tool/common_functionality/transformation_cage.rs @@ -246,7 +246,7 @@ impl BoundingBoxManager { overlay_context.quad(self.transform * Quad::from_box(self.bounds)); for position in self.evaluate_transform_handle_positions() { - overlay_context.square(position, false); + overlay_context.square(position, false, None); } } diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 069cc27e..9dc42582 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -1,10 +1,10 @@ use super::tool_prelude::*; -use crate::consts::{DRAG_THRESHOLD, SELECTION_THRESHOLD, SELECTION_TOLERANCE}; +use crate::consts::{COLOR_OVERLAY_YELLOW, DRAG_THRESHOLD, INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE, SELECTION_THRESHOLD, SELECTION_TOLERANCE}; use crate::messages::portfolio::document::overlays::utility_functions::path_overlays; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use crate::messages::tool::common_functionality::graph_modification_utils::{get_manipulator_from_id, get_mirror_handles, get_subpaths}; -use crate::messages::tool::common_functionality::shape_editor::{ManipulatorAngle, ManipulatorPointInfo, OpposingHandleLengths, SelectedPointsInfo, ShapeState}; +use crate::messages::tool::common_functionality::shape_editor::{ClosestSegment, ManipulatorAngle, ManipulatorPointInfo, OpposingHandleLengths, SelectedPointsInfo, ShapeState}; use crate::messages::tool::common_functionality::snapping::{SnapData, SnapManager}; use graph_craft::document::NodeNetwork; @@ -35,18 +35,24 @@ pub enum PathToolMessage { BreakPath, Delete, DeleteAndBreakPath, - DragStart { - add_to_selection: Key, - }, DragStop { shift_mirror_distance: Key, }, Enter { add_to_selection: Key, }, - InsertPoint, + Escape, + FlipSharp, + GRS { + // Should be `Key::KeyG` (Grab), `Key::KeyR` (Rotate), or `Key::KeyS` (Scale) + key: Key, + }, ManipulatorAngleMakeSharp, ManipulatorAngleMakeSmooth, + MouseDown { + ctrl: Key, + shift: Key, + }, NudgeSelectedPoints { delta_x: f64, delta_y: f64, @@ -55,6 +61,7 @@ pub enum PathToolMessage { alt: Key, shift: Key, }, + RightClick, SelectAllPoints, SelectedPointUpdated, SelectedPointXChanged { @@ -155,8 +162,8 @@ impl<'a> MessageHandler> for PathToo match self.fsm_state { Ready => actions!(PathToolMessageDiscriminant; - InsertPoint, - DragStart, + FlipSharp, + MouseDown, Delete, NudgeSelectedPoints, Enter, @@ -165,7 +172,7 @@ impl<'a> MessageHandler> for PathToo DeleteAndBreakPath, ), Dragging => actions!(PathToolMessageDiscriminant; - InsertPoint, + FlipSharp, DragStop, PointerMove, Delete, @@ -174,7 +181,7 @@ impl<'a> MessageHandler> for PathToo DeleteAndBreakPath, ), DrawingBox => actions!(PathToolMessageDiscriminant; - InsertPoint, + FlipSharp, DragStop, PointerMove, Delete, @@ -183,6 +190,15 @@ impl<'a> MessageHandler> for PathToo BreakPath, DeleteAndBreakPath, ), + InsertPoint => actions!(PathToolMessageDiscriminant; + Enter, + MouseDown, + PointerMove, + Escape, + Delete, + RightClick, + GRS, + ), } } } @@ -204,6 +220,12 @@ enum PathToolFsmState { Ready, Dragging, DrawingBox, + InsertPoint, +} + +enum InsertEndKind { + Abort, + Add { shift: bool }, } #[derive(Default)] @@ -216,30 +238,88 @@ struct PathToolData { /// Describes information about the selected point(s), if any, across one or multiple shapes and manipulator point types (anchor or handle). /// The available information varies depending on whether `None`, `One`, or `Multiple` points are currently selected. selection_status: SelectionStatus, + segment: Option, + double_click_handled: bool, } impl PathToolData { + fn start_insertion(&mut self, responses: &mut VecDeque, segment: ClosestSegment) -> PathToolFsmState { + if self.segment.is_some() { + warn!("Segment was `Some(..)` before `start_insertion`") + } + self.segment = Some(segment); + responses.add(OverlaysMessage::Draw); + PathToolFsmState::InsertPoint + } + + fn update_insertion(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, responses: &mut VecDeque, mouse_position: DVec2) -> PathToolFsmState { + if let Some(closed_segment) = &mut self.segment { + closed_segment.update_closest_point(&document.metadata, mouse_position); + if closed_segment.too_far(mouse_position, INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE) { + self.end_insertion(shape_editor, responses, InsertEndKind::Abort) + } else { + PathToolFsmState::InsertPoint + } + } else { + warn!("Segment was `None` on `update_insertion`"); + PathToolFsmState::Ready + } + } + + fn end_insertion(&mut self, shape_editor: &mut ShapeState, responses: &mut VecDeque, kind: InsertEndKind) -> PathToolFsmState { + match self.segment.as_mut() { + None => { + warn!("Segment was `None` before `end_insertion`") + } + Some(closed_segment) => { + if let InsertEndKind::Add { shift } = kind { + responses.add(DocumentMessage::StartTransaction); + closed_segment.adjusted_insert_and_select(shape_editor, responses, shift); + responses.add(DocumentMessage::CommitTransaction); + } + } + } + + self.segment = None; + responses.add(OverlaysMessage::Draw); + PathToolFsmState::Ready + } + fn mouse_down( &mut self, - shift: bool, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, + add_to_selection: bool, + direct_insert_without_sliding: bool, ) -> PathToolFsmState { + self.double_click_handled = false; self.opposing_handle_lengths = None; - let _selected_layers = shape_editor.selected_layers().cloned().collect::>(); + + let document_network = document.network(); + let document_metadata = document.metadata(); // Select the first point within the threshold (in pixels) - if let Some(selected_points) = shape_editor.select_point(&document.network, &document.metadata, input.mouse.position, SELECTION_THRESHOLD, shift) { - self.start_dragging_point(selected_points, input, document, responses); - responses.add(OverlaysMessage::Draw); - + if let Some(selected_points) = shape_editor.change_point_selection(document_network, document_metadata, input.mouse.position, SELECTION_THRESHOLD, add_to_selection) { + if let Some(selected_points) = selected_points { + self.start_dragging_point(selected_points, input, document, responses); + responses.add(OverlaysMessage::Draw); + } PathToolFsmState::Dragging } - // We didn't find a point nearby, so consider selecting the nearest shape instead + // We didn't find a point nearby, so now we'll try to add a point into the closest path segment + else if let Some(closed_segment) = shape_editor.upper_closest_segment(document_network, document_metadata, input.mouse.position, SELECTION_TOLERANCE) { + if direct_insert_without_sliding { + self.start_insertion(responses, closed_segment); + self.end_insertion(shape_editor, responses, InsertEndKind::Add { shift: add_to_selection }) + } else { + self.start_insertion(responses, closed_segment) + } + } + // We didn't find a segment path, so consider selecting the nearest shape instead else if let Some(layer) = document.click(input.mouse.position, &document.network) { - if shift { + if add_to_selection { responses.add(NodeGraphMessage::SelectedNodesAdd { nodes: vec![layer.to_node()] }); } else { responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }); @@ -336,21 +416,59 @@ impl Fsm for PathToolFsmState { (_, PathToolMessage::Overlays(mut overlay_context)) => { path_overlays(document, shape_editor, &mut overlay_context); - if self == Self::DrawingBox { - overlay_context.quad(Quad::from_box([tool_data.drag_start_pos, tool_data.previous_mouse_position])) - } else if self == Self::Dragging { - tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); + match self { + Self::DrawingBox => { + overlay_context.quad(Quad::from_box([tool_data.drag_start_pos, tool_data.previous_mouse_position])); + } + Self::Dragging => { + tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); + } + Self::InsertPoint => { + let state = tool_data.update_insertion(shape_editor, document, responses, input.mouse.position); + + if let Some(closest_segment) = &tool_data.segment { + overlay_context.square(closest_segment.closest_point_to_viewport(), false, Some(COLOR_OVERLAY_YELLOW)); + } + + responses.add(PathToolMessage::SelectedPointUpdated); + return state; + } + _ => {} } responses.add(PathToolMessage::SelectedPointUpdated); - self } - // Mouse down - (_, PathToolMessage::DragStart { add_to_selection }) => { - let shift = input.keyboard.get(add_to_selection as usize); - tool_data.mouse_down(shift, shape_editor, document, input, responses) + // `Self::InsertPoint` case: + (Self::InsertPoint, PathToolMessage::MouseDown { .. } | PathToolMessage::Enter { .. }) => { + tool_data.double_click_handled = true; + let shift = input.keyboard.get(Key::Shift as usize); + tool_data.end_insertion(shape_editor, responses, InsertEndKind::Add { shift }) + } + (Self::InsertPoint, PathToolMessage::PointerMove { .. }) => { + responses.add(OverlaysMessage::Draw); + // `tool_data.update_insertion` would be called on `OverlaysMessage::Draw` + // we anyway should to call it on `::Draw` because we can change scale by ctrl+scroll without `::PointerMove` + self + } + (Self::InsertPoint, PathToolMessage::Escape | PathToolMessage::Delete | PathToolMessage::RightClick) => tool_data.end_insertion(shape_editor, responses, InsertEndKind::Abort), + (Self::InsertPoint, PathToolMessage::GRS { key: propagate }) => { + // MAYBE: use `InputMapperMessage::KeyDown(..)` instead + match propagate { + Key::KeyG => responses.add(TransformLayerMessage::BeginGrab), + Key::KeyR => responses.add(TransformLayerMessage::BeginRotate), + Key::KeyS => responses.add(TransformLayerMessage::BeginScale), + _ => warn!("Unexpected GRS key"), + } + tool_data.end_insertion(shape_editor, responses, InsertEndKind::Abort) + } + + // Mouse down + (_, PathToolMessage::MouseDown { ctrl, shift }) => { + let add_to_selection = input.keyboard.get(shift as usize); + let direct_insert_without_sliding = input.keyboard.get(ctrl as usize); + tool_data.mouse_down(shape_editor, document, input, responses, add_to_selection, direct_insert_without_sliding) } (PathToolFsmState::DrawingBox, PathToolMessage::PointerMove { .. }) => { tool_data.previous_mouse_position = input.mouse.position; @@ -407,7 +525,7 @@ impl Fsm for PathToolFsmState { let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == Some(point)); if clicked_selected { shape_editor.deselect_all(); - shape_editor.select_point(&document.network, &document.metadata, input.mouse.position, SELECTION_THRESHOLD, false); + shape_editor.change_point_selection(&document.network, &document.metadata, input.mouse.position, SELECTION_THRESHOLD, false); responses.add(OverlaysMessage::Draw); } } @@ -434,19 +552,15 @@ impl Fsm for PathToolFsmState { shape_editor.delete_point_and_break_path(&document.network, responses); PathToolFsmState::Ready } - (_, PathToolMessage::InsertPoint) => { - // First we try and flip the sharpness (if they have clicked on an anchor) - if !shape_editor.flip_sharp(&document.network, &document.metadata, input.mouse.position, SELECTION_TOLERANCE, responses) { - // If not, then we try and split the path that may have been clicked upon - shape_editor.split(&document.network, &document.metadata, input.mouse.position, SELECTION_TOLERANCE, responses); + (_, PathToolMessage::FlipSharp) => { + if !tool_data.double_click_handled { + shape_editor.flip_sharp(&document.network, &document.metadata, input.mouse.position, SELECTION_TOLERANCE, responses); + responses.add(PathToolMessage::SelectedPointUpdated); } - - responses.add(PathToolMessage::SelectedPointUpdated); self } (_, PathToolMessage::Abort) => { responses.add(OverlaysMessage::Draw); - PathToolFsmState::Ready } (_, PathToolMessage::PointerMove { .. }) => self, @@ -511,6 +625,10 @@ impl Fsm for PathToolFsmState { HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), HintInfo::keys([Key::Shift], "Extend Selection").prepend_plus(), ])]), + PathToolFsmState::InsertPoint => HintData(vec![ + HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Insert Point")]), + HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel Insertion").prepend_slash()]), + ]), }; responses.add(FrontendMessage::UpdateInputHints { hint_data }); diff --git a/libraries/bezier-rs/src/bezier/mod.rs b/libraries/bezier-rs/src/bezier/mod.rs index 286ccd18..5831ee55 100644 --- a/libraries/bezier-rs/src/bezier/mod.rs +++ b/libraries/bezier-rs/src/bezier/mod.rs @@ -31,6 +31,11 @@ pub enum BezierHandles { handle_end: DVec2, }, } +impl BezierHandles { + pub fn is_cubic(&self) -> bool { + matches!(self, Self::Cubic { .. }) + } +} #[cfg(feature = "dyn-any")] unsafe impl dyn_any::StaticType for BezierHandles { diff --git a/libraries/bezier-rs/src/subpath/structs.rs b/libraries/bezier-rs/src/subpath/structs.rs index 9bb753b2..b0cbe784 100644 --- a/libraries/bezier-rs/src/subpath/structs.rs +++ b/libraries/bezier-rs/src/subpath/structs.rs @@ -117,6 +117,18 @@ impl ManipulatorGroup std::mem::swap(&mut self.in_handle, &mut self.out_handle); self } + + pub fn has_in_handle(&self) -> bool { + self.in_handle.map(|handle| Self::has_handle(self.anchor, handle)).unwrap_or(false) + } + + pub fn has_out_handle(&self) -> bool { + self.out_handle.map(|handle| Self::has_handle(self.anchor, handle)).unwrap_or(false) + } + + fn has_handle(anchor: DVec2, handle: DVec2) -> bool { + !((handle.x - anchor.x).abs() < f64::EPSILON && (handle.y - anchor.y).abs() < f64::EPSILON) + } } #[derive(Copy, Clone)]