diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index f29c0fb2be..a684eba092 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -258,6 +258,8 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(MouseRight); action_dispatch=PenToolMessage::Confirm), entry!(KeyDown(Escape); action_dispatch=PenToolMessage::Confirm), entry!(KeyDown(Enter); action_dispatch=PenToolMessage::Confirm), + entry!(KeyDown(Delete); action_dispatch=PenToolMessage::RemovePreviousHandle), + entry!(KeyDown(Backspace); action_dispatch=PenToolMessage::RemovePreviousHandle), // // FreehandToolMessage entry!(PointerMove; action_dispatch=FreehandToolMessage::PointerMove), diff --git a/editor/src/messages/portfolio/document/overlays/utility_functions.rs b/editor/src/messages/portfolio/document/overlays/utility_functions.rs index c99d83e3b4..b71d3e3874 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_functions.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_functions.rs @@ -41,16 +41,16 @@ pub fn path_overlays(document: &DocumentMessageHandler, shape_editor: &mut Shape bezier_rs::BezierHandles::Quadratic { handle } if not_under_anchor(handle, bezier.start) && not_under_anchor(handle, bezier.end) => { overlay_context.line(handle, bezier.start, None); overlay_context.line(handle, bezier.end, None); - overlay_context.manipulator_handle(handle, is_selected(selected, ManipulatorPointId::PrimaryHandle(segment_id))); + overlay_context.manipulator_handle(handle, is_selected(selected, ManipulatorPointId::PrimaryHandle(segment_id)), None); } bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => { if not_under_anchor(handle_start, bezier.start) { overlay_context.line(handle_start, bezier.start, None); - overlay_context.manipulator_handle(handle_start, is_selected(selected, ManipulatorPointId::PrimaryHandle(segment_id))); + overlay_context.manipulator_handle(handle_start, is_selected(selected, ManipulatorPointId::PrimaryHandle(segment_id)), None); } if not_under_anchor(handle_end, bezier.end) { overlay_context.line(handle_end, bezier.end, None); - overlay_context.manipulator_handle(handle_end, is_selected(selected, ManipulatorPointId::EndHandle(segment_id))); + overlay_context.manipulator_handle(handle_end, is_selected(selected, ManipulatorPointId::EndHandle(segment_id)), None); } } _ => {} diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index f35f841852..edf903bdf5 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -127,7 +127,7 @@ impl OverlayContext { } } - pub fn manipulator_handle(&mut self, position: DVec2, selected: bool) { + pub fn manipulator_handle(&mut self, position: DVec2, selected: bool, color: Option<&str>) { let position = position.round() - DVec2::splat(0.5); self.render_context.begin_path(); @@ -137,7 +137,7 @@ impl OverlayContext { let fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE }; self.render_context.set_fill_style_str(fill); - self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE); + self.render_context.set_stroke_style_str(color.unwrap_or(COLOR_OVERLAY_BLUE)); self.render_context.fill(); self.render_context.stroke(); } diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index d53b904335..6ecdbcea1d 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -116,6 +116,20 @@ impl ClosestSegment { (stroke_width_sq + tolerance_sq) < dist_sq } + pub fn handle_positions(&self, document_metadata: &DocumentMetadata) -> (Option, Option) { + // Transform to viewport space + let transform = document_metadata.transform_to_viewport(self.layer); + + // Split the Bezier at the parameter `t` + let [first, second] = self.bezier.split(TValue::Parametric(self.t)); + + // Transform the handle positions to viewport space + let first_handle = first.handle_end().map(|handle| transform.transform_point2(handle)); + let second_handle = second.handle_start().map(|handle| transform.transform_point2(handle)); + + (first_handle, second_handle) + } + pub fn adjusted_insert(&self, responses: &mut VecDeque) -> PointId { let layer = self.layer; let [first, second] = self.bezier.split(TValue::Parametric(self.t)); diff --git a/editor/src/messages/tool/common_functionality/snapping.rs b/editor/src/messages/tool/common_functionality/snapping.rs index d23b2c54c0..9e402d41b6 100644 --- a/editor/src/messages/tool/common_functionality/snapping.rs +++ b/editor/src/messages/tool/common_functionality/snapping.rs @@ -461,10 +461,10 @@ impl SnapManager { overlay_context.line(viewport, target, None); } for &target in align.iter().flatten() { - overlay_context.manipulator_handle(target, false); + overlay_context.manipulator_handle(target, false, None); } if any_align { - overlay_context.manipulator_handle(viewport, false); + overlay_context.manipulator_handle(viewport, false, None); } if !any_align && ind.distribution_equal_distance_x.is_none() && ind.distribution_equal_distance_y.is_none() { diff --git a/editor/src/messages/tool/tool_message_handler.rs b/editor/src/messages/tool/tool_message_handler.rs index ca855b90d7..239f0acc09 100644 --- a/editor/src/messages/tool/tool_message_handler.rs +++ b/editor/src/messages/tool/tool_message_handler.rs @@ -76,8 +76,8 @@ impl MessageHandler> for ToolMessageHandler { self.tool_is_active = true; // Send the old and new tools a transition to their FSM Abort states - let mut send_abort_to_tool = |tool_type, update_hints_and_cursor: bool| { - if let Some(tool) = tool_data.tools.get_mut(&tool_type) { + let mut send_abort_to_tool = |old_tool: ToolType, new_tool: ToolType, update_hints_and_cursor: bool| { + if let Some(tool) = tool_data.tools.get_mut(&new_tool) { let mut data = ToolActionHandlerData { document, document_id, @@ -101,9 +101,14 @@ impl MessageHandler> for ToolMessageHandler { tool.process_message(ToolMessage::UpdateCursor, responses, &mut data); } } + + if matches!(old_tool, ToolType::Path | ToolType::Select) { + responses.add(TransformLayerMessage::CancelTransformOperation); + } }; - send_abort_to_tool(tool_type, true); - send_abort_to_tool(old_tool, false); + + send_abort_to_tool(old_tool, tool_type, true); + send_abort_to_tool(old_tool, old_tool, false); // Unsubscribe old tool from the broadcaster tool_data.tools.get(&tool_type).unwrap().deactivate(responses); diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 9e45a30c6e..a6676fb7d1 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -257,15 +257,15 @@ impl Fsm for GradientToolFsmState { let (start, end) = (transform.transform_point2(start), transform.transform_point2(end)); overlay_context.line(start, end, None); - overlay_context.manipulator_handle(start, dragging == Some(GradientDragTarget::Start)); - overlay_context.manipulator_handle(end, dragging == Some(GradientDragTarget::End)); + overlay_context.manipulator_handle(start, dragging == Some(GradientDragTarget::Start), None); + overlay_context.manipulator_handle(end, dragging == Some(GradientDragTarget::End), None); for (index, (position, _)) in stops.0.into_iter().enumerate() { if position.abs() < f64::EPSILON * 1000. || (1. - position).abs() < f64::EPSILON * 1000. { continue; } - overlay_context.manipulator_handle(start.lerp(end, position), dragging == Some(GradientDragTarget::Step(index))); + overlay_context.manipulator_handle(start.lerp(end, position), dragging == Some(GradientDragTarget::Step(index)), None); } } diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index ab582d5eaf..7f5858839f 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -1,5 +1,5 @@ use super::tool_prelude::*; -use crate::consts::{COLOR_OVERLAY_YELLOW, DRAG_THRESHOLD, HANDLE_ROTATE_SNAP_ANGLE, INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE, SELECTION_THRESHOLD, SELECTION_TOLERANCE}; +use crate::consts::{COLOR_OVERLAY_BLUE, DRAG_THRESHOLD, HANDLE_ROTATE_SNAP_ANGLE, 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::LayerNodeIdentifier; @@ -520,6 +520,7 @@ impl PathToolData { handle_angle } + #[allow(clippy::too_many_arguments)] fn apply_snapping( &mut self, handle_direction: DVec2, @@ -549,6 +550,7 @@ impl PathToolData { document.metadata().document_to_viewport.transform_vector2(snap_result.snapped_point_document - handle_position) } + #[allow(clippy::too_many_arguments)] fn drag( &mut self, equidistant: bool, @@ -622,7 +624,13 @@ impl Fsm for PathToolFsmState { let state = tool_data.update_insertion(shape_editor, document, responses, input); if let Some(closest_segment) = &tool_data.segment { - overlay_context.manipulator_anchor(closest_segment.closest_point_to_viewport(), false, Some(COLOR_OVERLAY_YELLOW)); + overlay_context.manipulator_anchor(closest_segment.closest_point_to_viewport(), false, Some(COLOR_OVERLAY_BLUE)); + if let (Some(handle1), Some(handle2)) = closest_segment.handle_positions(document.metadata()) { + overlay_context.line(closest_segment.closest_point_to_viewport(), handle1, Some(COLOR_OVERLAY_BLUE)); + overlay_context.line(closest_segment.closest_point_to_viewport(), handle2, Some(COLOR_OVERLAY_BLUE)); + overlay_context.manipulator_handle(handle1, false, Some(COLOR_OVERLAY_BLUE)); + overlay_context.manipulator_handle(handle2, false, Some(COLOR_OVERLAY_BLUE)); + } } responses.add(PathToolMessage::SelectedPointUpdated); @@ -648,19 +656,7 @@ impl Fsm for PathToolFsmState { 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 { - // TODO: Don't use `Key::G` directly, instead take it as a variable from the input mappings list like in all other places - Key::KeyG => responses.add(TransformLayerMessage::BeginGrab), - // TODO: Don't use `Key::R` directly, instead take it as a variable from the input mappings list like in all other places - Key::KeyR => responses.add(TransformLayerMessage::BeginRotate), - // TODO: Don't use `Key::S` directly, instead take it as a variable from the input mappings list like in all other places - Key::KeyS => responses.add(TransformLayerMessage::BeginScale), - _ => warn!("Unexpected GRS key"), - } - tool_data.end_insertion(shape_editor, responses, InsertEndKind::Abort) - } + (Self::InsertPoint, PathToolMessage::GRS { key: _ }) => PathToolFsmState::InsertPoint, // Mouse down ( _, diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 356200de3c..845276c002 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -62,6 +62,7 @@ pub enum PenToolMessage { Undo, UpdateOptions(PenOptionsUpdate), RecalculateLatestPointsPosition, + RemovePreviousHandle, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -175,6 +176,7 @@ impl<'a> MessageHandler> for PenTool PointerMove, Confirm, Abort, + RemovePreviousHandle, ), } } @@ -600,12 +602,12 @@ impl Fsm for PenToolFsmState { if self == PenToolFsmState::DraggingHandle && valid(next_anchor, handle_end) { // Draw the handle circle for the currently-being-dragged-out incoming handle (opposite the one currently being dragged out) - overlay_context.manipulator_handle(handle_end, false); + overlay_context.manipulator_handle(handle_end, false, None); } if valid(anchor_start, handle_start) { // Draw the handle circle for the most recently placed anchor's outgoing handle (which is currently influencing the currently-being-placed segment) - overlay_context.manipulator_handle(handle_start, false); + overlay_context.manipulator_handle(handle_start, false, None); } } else { // Draw the whole path and its manipulators when the user is clicking-and-dragging out from the most recently placed anchor to set its outgoing handle, during which it would otherwise not have its overlays drawn @@ -614,7 +616,7 @@ impl Fsm for PenToolFsmState { if self == PenToolFsmState::DraggingHandle && valid(next_anchor, next_handle_start) { // Draw the handle circle for the currently-being-dragged-out outgoing handle (the one currently being dragged out, under the user's cursor) - overlay_context.manipulator_handle(next_handle_start, false); + overlay_context.manipulator_handle(next_handle_start, false, None); } if self == PenToolFsmState::DraggingHandle { @@ -685,6 +687,15 @@ impl Fsm for PenToolFsmState { PenToolFsmState::PlacingAnchor } } + (PenToolFsmState::PlacingAnchor, PenToolMessage::RemovePreviousHandle) => { + if let Some(last_point) = tool_data.latest_points.last_mut() { + last_point.handle_start = last_point.pos; + responses.add(OverlaysMessage::Draw); + } else { + log::warn!("No latest point available to modify handle_start."); + } + self + } (PenToolFsmState::DraggingHandle, PenToolMessage::DragStop) => tool_data .finish_placing_handle(SnapData::new(document, input), transform, responses) .unwrap_or(PenToolFsmState::PlacingAnchor), diff --git a/editor/src/messages/tool/tool_messages/spline_tool.rs b/editor/src/messages/tool/tool_messages/spline_tool.rs index b680d81e06..94a1182b01 100644 --- a/editor/src/messages/tool/tool_messages/spline_tool.rs +++ b/editor/src/messages/tool/tool_messages/spline_tool.rs @@ -7,8 +7,9 @@ use crate::messages::tool::common_functionality::color_selector::{ToolColorOptio use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::snapping::SnapManager; -use graph_craft::document::{value::TaggedValue, NodeId, NodeInput}; +use graph_craft::document::{NodeId, NodeInput}; use graphene_core::Color; +use graphene_std::vector::{PointId, SegmentId, VectorModificationType}; #[derive(Default)] pub struct SplineTool { @@ -177,8 +178,14 @@ impl ToolTransition for SplineTool { #[derive(Clone, Debug, Default)] struct SplineToolData { - points: Vec, + /// Points that are inserted. + points: Vec<(PointId, DVec2)>, + /// Point to be inserted. next_point: DVec2, + /// Point that was inserted temporarily to show preview. + preview_point: Option, + /// Segment that was inserted temporarily to show preview. + preview_segment: Option, weight: f64, layer: Option, snap_manager: SnapManager, @@ -205,9 +212,11 @@ impl Fsm for SplineToolFsmState { tool_data.weight = tool_options.line_weight; - let node_type = resolve_document_node_type("Spline").expect("Spline node does not exist"); - let node = node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::VecDVec2(Vec::new()), false))]); - let nodes = vec![(NodeId(0), node)]; + let path_node_type = resolve_document_node_type("Path").expect("Path node does not exist"); + let path_node = path_node_type.default_node_template(); + let spline_node_type = resolve_document_node_type("Splines from Points").expect("Spline from Points node does not exist"); + let spline_node = spline_node_type.node_template_input_override([Some(NodeInput::node(NodeId(1), 0))]); + let nodes = vec![(NodeId(1), path_node), (NodeId(0), spline_node)]; let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses); tool_options.fill.apply_fill(layer, responses); @@ -228,12 +237,11 @@ impl Fsm for SplineToolFsmState { let transform = document.metadata().transform_to_viewport(layer); let pos = transform.inverse().transform_point2(snapped_position); - if tool_data.points.last().map_or(true, |last_pos| last_pos.distance(pos) > DRAG_THRESHOLD) { - tool_data.points.push(pos); + if tool_data.points.last().map_or(true, |last_pos| last_pos.1.distance(pos) > DRAG_THRESHOLD) { tool_data.next_point = pos; } - update_spline(document, tool_data, true, responses); + update_spline(tool_data, false, responses); SplineToolFsmState::Drawing } @@ -246,7 +254,7 @@ impl Fsm for SplineToolFsmState { let pos = transform.inverse().transform_point2(snapped_position); tool_data.next_point = pos; - update_spline(document, tool_data, true, responses); + update_spline(tool_data, true, responses); // Auto-panning let messages = [SplineToolMessage::PointerOutsideViewport.into(), SplineToolMessage::PointerMove.into()]; @@ -269,13 +277,15 @@ impl Fsm for SplineToolFsmState { } (SplineToolFsmState::Drawing, SplineToolMessage::Confirm | SplineToolMessage::Abort) => { if tool_data.points.len() >= 2 { - update_spline(document, tool_data, false, responses); + delete_preview(tool_data, responses); responses.add(DocumentMessage::EndTransaction); } else { responses.add(DocumentMessage::AbortTransaction); } tool_data.layer = None; + tool_data.preview_point = None; + tool_data.preview_segment = None; tool_data.points.clear(); tool_data.snap_manager.cleanup(responses); @@ -310,17 +320,49 @@ impl Fsm for SplineToolFsmState { } } -fn update_spline(document: &DocumentMessageHandler, tool_data: &SplineToolData, show_preview: bool, responses: &mut VecDeque) { - let mut points = tool_data.points.clone(); +fn update_spline(tool_data: &mut SplineToolData, show_preview: bool, responses: &mut VecDeque) { + delete_preview(tool_data, responses); + + let Some(layer) = tool_data.layer else { return }; + + let next_point_pos = tool_data.next_point; + let next_point_id = PointId::generate(); + let modification_type = VectorModificationType::InsertPoint { + id: next_point_id, + position: next_point_pos, + }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + + if let Some((last_point_id, _)) = tool_data.points.last() { + let points = [*last_point_id, next_point_id]; + let id = SegmentId::generate(); + let modification_type = VectorModificationType::InsertSegment { id, points, handles: [None, None] }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + + if show_preview { + tool_data.preview_segment = Some(id); + } + } + if show_preview { - points.push(tool_data.next_point) + tool_data.preview_point = Some(next_point_id); + } else { + tool_data.points.push((next_point_id, next_point_pos)); } - let value = TaggedValue::VecDVec2(points); +} +fn delete_preview(tool_data: &mut SplineToolData, responses: &mut VecDeque) { let Some(layer) = tool_data.layer else { return }; - let Some(node_id) = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface).upstream_node_id_from_name("Spline") else { - return; - }; - responses.add_front(NodeGraphMessage::SetInputValue { node_id, input_index: 1, value }); + if let Some(id) = tool_data.preview_point { + let modification_type = VectorModificationType::RemovePoint { id }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + } + if let Some(id) = tool_data.preview_segment { + let modification_type = VectorModificationType::RemoveSegment { id }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + } + + tool_data.preview_point = None; + tool_data.preview_segment = None; } 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 6b2f3ac525..477796c89c 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 @@ -43,6 +43,7 @@ type TransformData<'a> = (&'a DocumentMessageHandler, &'a InputPreprocessorMessa impl MessageHandler> for TransformLayerMessageHandler { fn process_message(&mut self, message: TransformLayerMessage, responses: &mut VecDeque, (document, input, tool_data, shape_editor): TransformData) { let using_path_tool = tool_data.active_tool_type == ToolType::Path; + let using_select_tool = tool_data.active_tool_type == ToolType::Select; // TODO: Add support for transforming layer not in the document network let selected_layers = document @@ -75,10 +76,18 @@ impl MessageHandler> for TransformLayer let viewspace = document.metadata().transform_to_viewport(selected_layers[0]); let mut point_count: usize = 0; - let get_location = |point: &ManipulatorPointId| point.get_position(&vector_data).map(|position| viewspace.transform_point2(position)); + let get_location = |point: &&ManipulatorPointId| point.get_position(&vector_data).map(|position| viewspace.transform_point2(position)); let points = shape_editor.selected_points(); - - *selected.pivot = points.filter_map(get_location).inspect(|_| point_count += 1).sum::() / point_count as f64; + let selected_points: Vec<&ManipulatorPointId> = points.collect(); + + if let [point] = selected_points.as_slice() { + if let ManipulatorPointId::PrimaryHandle(_) | ManipulatorPointId::EndHandle(_) = point { + let anchor_position = point.get_anchor_position(&vector_data).unwrap(); + *selected.pivot = viewspace.transform_point2(anchor_position); + } else { + *selected.pivot = selected_points.iter().filter_map(get_location).inspect(|_| point_count += 1).sum::() / point_count as f64; + } + } } } else { *selected.pivot = selected.mean_average_of_pivots(); @@ -104,12 +113,13 @@ impl MessageHandler> for TransformLayer responses.add(NodeGraphMessage::RunDocumentGraph); } TransformLayerMessage::BeginGrab => { - if let TransformOperation::Grabbing(_) = self.transform_operation { - return; - } + if (!using_path_tool && !using_select_tool) + || (using_path_tool && shape_editor.selected_points().next().is_none()) + || selected_layers.is_empty() + || matches!(self.transform_operation, TransformOperation::Grabbing(_)) + { + selected.original_transforms.clear(); - // Don't allow grab with no selected layers - if selected_layers.is_empty() { return; } @@ -120,13 +130,42 @@ impl MessageHandler> for TransformLayer selected.original_transforms.clear(); } TransformLayerMessage::BeginRotate => { - if let TransformOperation::Rotating(_) = self.transform_operation { + let selected_points: Vec<&ManipulatorPointId> = shape_editor.selected_points().collect(); + + if (!using_path_tool && !using_select_tool) + || (using_path_tool && selected_points.is_empty()) + || selected_layers.is_empty() + || matches!(self.transform_operation, TransformOperation::Rotating(_)) + { + selected.original_transforms.clear(); return; } - // Don't allow rotate with no selected layers - if selected_layers.is_empty() { + let Some(vector_data) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer)) else { + selected.original_transforms.clear(); return; + }; + + if let [point] = selected_points.as_slice() { + if matches!(point, ManipulatorPointId::Anchor(_)) { + if let Some([handle1, handle2]) = point.get_handle_pair(&vector_data) { + let handle1_length = handle1.length(&vector_data); + let handle2_length = handle2.length(&vector_data); + + if (handle1_length == 0. && handle2_length == 0.) || (handle1_length == f64::MAX && handle2_length == f64::MAX) { + return; + } + } + } else { + // TODO: Fix handle snap to anchor issue, see + + let handle_length = point.as_handle().map(|handle| handle.length(&vector_data)); + + if handle_length == Some(0.) { + selected.original_transforms.clear(); + return; + } + } } begin_operation(self.transform_operation, &mut self.typing, &mut self.mouse_position, &mut self.start_mouse); @@ -136,13 +175,41 @@ impl MessageHandler> for TransformLayer selected.original_transforms.clear(); } TransformLayerMessage::BeginScale => { - if let TransformOperation::Scaling(_) = self.transform_operation { + let selected_points: Vec<&ManipulatorPointId> = shape_editor.selected_points().collect(); + + if (using_path_tool && selected_points.is_empty()) + || (!using_path_tool && !using_select_tool) + || selected_layers.is_empty() + || matches!(self.transform_operation, TransformOperation::Scaling(_)) + { + selected.original_transforms.clear(); return; } - // Don't allow scale with no selected layers - if selected_layers.is_empty() { + let Some(vector_data) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer)) else { + selected.original_transforms.clear(); return; + }; + + if let [point] = selected_points.as_slice() { + if matches!(point, ManipulatorPointId::Anchor(_)) { + if let Some([handle1, handle2]) = point.get_handle_pair(&vector_data) { + let handle1_length = handle1.length(&vector_data); + let handle2_length = handle2.length(&vector_data); + + if (handle1_length == 0. && handle2_length == 0.) || (handle1_length == f64::MAX && handle2_length == f64::MAX) { + selected.original_transforms.clear(); + return; + } + } + } else { + let handle_length = point.as_handle().map(|handle| handle.length(&vector_data)); + + if handle_length == Some(0.) { + selected.original_transforms.clear(); + return; + } + } } begin_operation(self.transform_operation, &mut self.typing, &mut self.mouse_position, &mut self.start_mouse); @@ -215,6 +282,7 @@ impl MessageHandler> for TransformLayer } }; } + self.mouse_position = input.mouse.position; } TransformLayerMessage::SelectionChanged => { diff --git a/node-graph/gcore/src/vector/vector_data.rs b/node-graph/gcore/src/vector/vector_data.rs index 21e5630b05..93cadbbb52 100644 --- a/node-graph/gcore/src/vector/vector_data.rs +++ b/node-graph/gcore/src/vector/vector_data.rs @@ -306,6 +306,13 @@ impl ManipulatorPointId { } } + pub fn get_anchor_position(&self, vector_data: &VectorData) -> Option { + match self { + ManipulatorPointId::EndHandle(_) | ManipulatorPointId::PrimaryHandle(_) => self.get_anchor(vector_data).and_then(|id| vector_data.point_domain.position_from_id(id)), + _ => self.get_position(vector_data), + } + } + /// Attempt to get a pair of handles. For an anchor this is the first two handles connected. For a handle it is self and the first opposing handle. #[must_use] pub fn get_handle_pair(self, vector_data: &VectorData) -> Option<[HandleId; 2]> { @@ -396,6 +403,13 @@ impl HandleId { } } + /// Calculate the magnitude of the handle from the anchor. + pub fn length(self, vector_data: &VectorData) -> f64 { + let anchor_position = self.to_manipulator_point().get_anchor_position(vector_data).unwrap(); + let handle_position = self.to_manipulator_point().get_position(vector_data); + handle_position.map(|pos| (pos - anchor_position).length()).unwrap_or(f64::MAX) + } + /// Set the handle's position relative to the anchor which is the start anchor for the primary handle and end anchor for the end handle. #[must_use] pub fn set_relative_position(self, relative_position: DVec2) -> VectorModificationType {