From 5bca931813e456e2f6035844c21e77ee590b7728 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Thu, 14 Mar 2024 04:02:16 -0700 Subject: [PATCH] Rename handle mirroring to colinear --- .../messages/input_mapper/default_mapping.rs | 8 +- .../node_graph/graph_operation_message.rs | 4 +- .../graph_operation_message_handler.rs | 10 +- .../transform_utils.rs | 26 +-- .../document_node_types.rs | 2 +- .../portfolio/document/utility_types/misc.rs | 26 +-- .../graph_modification_utils.rs | 54 +++--- .../tool/common_functionality/shape_editor.rs | 175 +++++++++--------- .../snapping/layer_snapper.rs | 31 ++-- .../tool/tool_messages/ellipse_tool.rs | 2 +- .../messages/tool/tool_messages/path_tool.rs | 117 +++++++----- .../messages/tool/tool_messages/pen_tool.rs | 146 ++++++++------- .../tool/tool_messages/spline_tool.rs | 2 +- .../mouse-hint-lmb-double.svg | 2 +- .../mouse-hint-rmb-double.svg | 2 +- frontend/src/utility-functions/icons.ts | 2 + .../bezier-rs/src/subpath/manipulators.rs | 2 +- .../gcore/src/vector/generator_nodes.rs | 8 +- node-graph/gcore/src/vector/vector_data.rs | 10 +- .../src/vector/vector_data/attributes.rs | 2 +- 20 files changed, 326 insertions(+), 305 deletions(-) diff --git a/editor/src/messages/input_mapper/default_mapping.rs b/editor/src/messages/input_mapper/default_mapping.rs index d6cb6bd7..bb3ae6d0 100644 --- a/editor/src/messages/input_mapper/default_mapping.rs +++ b/editor/src/messages/input_mapper/default_mapping.rs @@ -200,11 +200,9 @@ pub fn default_mapping() -> Mapping { entry!(KeyDown(KeyA); modifiers=[Accel], action_dispatch=PathToolMessage::SelectAllAnchors), entry!(KeyDown(KeyA); modifiers=[Accel, Shift], action_dispatch=PathToolMessage::DeselectAllPoints), entry!(KeyDown(Backspace); action_dispatch=PathToolMessage::Delete), - entry!(KeyUp(Lmb); action_dispatch=PathToolMessage::DragStop { shift_mirror_distance: Shift }), - entry!(KeyDown(Enter); action_dispatch=PathToolMessage::Enter { - add_to_selection: Shift - }), - entry!(DoubleClick(MouseButton::Left); action_dispatch=PathToolMessage::FlipSharp), + entry!(KeyUp(Lmb); action_dispatch=PathToolMessage::DragStop { equidistant: Shift }), + entry!(KeyDown(Enter); action_dispatch=PathToolMessage::Enter { add_to_selection: Shift }), + entry!(DoubleClick(MouseButton::Left); action_dispatch=PathToolMessage::FlipSmoothSharp), 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/node_graph/graph_operation_message.rs b/editor/src/messages/portfolio/document/node_graph/graph_operation_message.rs index 959bd27c..334cf83f 100644 --- a/editor/src/messages/portfolio/document/node_graph/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/node_graph/graph_operation_message.rs @@ -132,8 +132,8 @@ pub enum VectorDataModification { RemoveManipulatorGroup { id: ManipulatorGroupId }, RemoveManipulatorPoint { point: ManipulatorPointId }, SetClosed { index: usize, closed: bool }, - SetManipulatorHandleMirroring { id: ManipulatorGroupId, mirror_angle: bool }, + SetManipulatorColinearHandlesState { id: ManipulatorGroupId, colinear: bool }, SetManipulatorPosition { point: ManipulatorPointId, position: DVec2 }, - ToggleManipulatorHandleMirroring { id: ManipulatorGroupId }, + ToggleManipulatorColinearHandlesState { id: ManipulatorGroupId }, UpdateSubpaths { subpaths: Vec> }, } diff --git a/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs index 4ee94536..47b26912 100644 --- a/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs @@ -462,8 +462,8 @@ impl<'a> ModifyInputsContext<'a> { let mut empty = false; self.modify_inputs("Shape", false, |inputs, _node_id, _metadata| { - let [subpaths, mirror_angle_groups] = inputs.as_mut_slice() else { - panic!("Shape does not have subpath and mirror angle inputs"); + let [subpaths, colinear_manipulators] = inputs.as_mut_slice() else { + panic!("Shape does not have both `subpath` and `colinear_manipulators` inputs"); }; let NodeInput::Value { @@ -474,16 +474,16 @@ impl<'a> ModifyInputsContext<'a> { return; }; let NodeInput::Value { - tagged_value: TaggedValue::ManipulatorGroupIds(mirror_angle_groups), + tagged_value: TaggedValue::ManipulatorGroupIds(colinear_manipulators), .. - } = mirror_angle_groups + } = colinear_manipulators else { return; }; [old_bounds_min, old_bounds_max] = transform_utils::nonzero_subpath_bounds(subpaths); - transform_utils::VectorModificationState { subpaths, mirror_angle_groups }.modify(modification); + transform_utils::VectorModificationState { subpaths, colinear_manipulators }.modify(modification); empty = !subpaths.iter().any(|subpath| !subpath.is_empty()); [new_bounds_min, new_bounds_max] = transform_utils::nonzero_subpath_bounds(subpaths); diff --git a/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler/transform_utils.rs b/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler/transform_utils.rs index 82a35b47..3879b4b3 100644 --- a/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler/transform_utils.rs +++ b/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler/transform_utils.rs @@ -199,7 +199,7 @@ pub fn nonzero_subpath_bounds(subpaths: &[Subpath]) -> [DVec pub struct VectorModificationState<'a> { pub subpaths: &'a mut Vec>, - pub mirror_angle_groups: &'a mut Vec, + pub colinear_manipulators: &'a mut Vec, } impl<'a> VectorModificationState<'a> { fn insert_start(&mut self, subpath_index: usize, manipulator_group: ManipulatorGroup) { @@ -246,19 +246,19 @@ impl<'a> VectorModificationState<'a> { } } - fn set_mirror(&mut self, id: ManipulatorGroupId, mirror_angle: bool) { - if !mirror_angle { - self.mirror_angle_groups.retain(|&mirrored_id| mirrored_id != id); - } else if !self.mirror_angle_groups.contains(&id) { - self.mirror_angle_groups.push(id); + fn set_manipulator_colinear_handles_state(&mut self, id: ManipulatorGroupId, colinear: bool) { + if !colinear { + self.colinear_manipulators.retain(|&manipulator_group_id| manipulator_group_id != id); + } else if !self.colinear_manipulators.contains(&id) { + self.colinear_manipulators.push(id); } } - fn toggle_mirror(&mut self, id: ManipulatorGroupId) { - if self.mirror_angle_groups.contains(&id) { - self.mirror_angle_groups.retain(|&mirrored_id| mirrored_id != id); + fn toggle_manipulator_colinear_handles_state(&mut self, id: ManipulatorGroupId) { + if self.colinear_manipulators.contains(&id) { + self.colinear_manipulators.retain(|&manipulator_group_id| manipulator_group_id != id); } else { - self.mirror_angle_groups.push(id); + self.colinear_manipulators.push(id); } } @@ -271,7 +271,7 @@ impl<'a> VectorModificationState<'a> { SelectedType::InHandle => manipulator.in_handle = Some(position), SelectedType::OutHandle => manipulator.out_handle = Some(position), } - if point.manipulator_type != SelectedType::Anchor && self.mirror_angle_groups.contains(&point.group) { + if point.manipulator_type != SelectedType::Anchor && self.colinear_manipulators.contains(&point.group) { let reflect = |opposite: DVec2| { (manipulator.anchor - position) .try_normalize() @@ -298,9 +298,9 @@ impl<'a> VectorModificationState<'a> { VectorDataModification::RemoveManipulatorGroup { id } => self.remove_group(id), VectorDataModification::RemoveManipulatorPoint { point } => self.remove_point(point), VectorDataModification::SetClosed { index, closed } => self.subpaths[index].set_closed(closed), - VectorDataModification::SetManipulatorHandleMirroring { id, mirror_angle } => self.set_mirror(id, mirror_angle), + VectorDataModification::SetManipulatorColinearHandlesState { id, colinear } => self.set_manipulator_colinear_handles_state(id, colinear), VectorDataModification::SetManipulatorPosition { point, position } => self.set_position(point, position), - VectorDataModification::ToggleManipulatorHandleMirroring { id } => self.toggle_mirror(id), + VectorDataModification::ToggleManipulatorColinearHandlesState { id } => self.toggle_manipulator_colinear_handles_state(id), VectorDataModification::UpdateSubpaths { subpaths } => *self.subpaths = subpaths, } } diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs index ea2e3b3d..37e44eac 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs @@ -2442,7 +2442,7 @@ fn static_nodes() -> Vec { }), inputs: vec![ DocumentInputType::value("Path Data", TaggedValue::Subpaths(vec![]), false), - DocumentInputType::value("Mirror", TaggedValue::ManipulatorGroupIds(vec![]), false), + DocumentInputType::value("Colinear Manipulators", TaggedValue::ManipulatorGroupIds(vec![]), false), ], outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)], ..Default::default() diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index 0f8afcc7..56940a7c 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -61,7 +61,7 @@ pub struct SnappingState { pub geometry_snapping: bool, pub grid_snapping: bool, pub bounds: BoundsSnapping, - pub nodes: NodeSnapping, + pub nodes: PointSnapping, pub grid: GridSnapping, pub tolerance: f64, pub artboards: bool, @@ -79,11 +79,11 @@ impl Default for SnappingState { edge_midpoints: false, centers: true, }, - nodes: NodeSnapping { + nodes: PointSnapping { paths: true, path_intersections: true, - sharp_nodes: true, - smooth_nodes: true, + point_handles_free: true, + point_handles_colinear: true, line_midpoints: true, normals: true, tangents: true, @@ -110,8 +110,8 @@ impl SnappingState { BoundingBoxSnapTarget::Center => self.bounds.centers, }, SnapTarget::Geometry(nodes) if self.geometry_snapping => match nodes { - GeometrySnapTarget::Smooth => self.nodes.smooth_nodes, - GeometrySnapTarget::Sharp => self.nodes.sharp_nodes, + GeometrySnapTarget::HandlesColinear => self.nodes.point_handles_colinear, + GeometrySnapTarget::HandlesFree => self.nodes.point_handles_free, GeometrySnapTarget::LineMidpoint => self.nodes.line_midpoints, GeometrySnapTarget::Path => self.nodes.paths, GeometrySnapTarget::Normal => self.nodes.normals, @@ -132,11 +132,11 @@ pub struct BoundsSnapping { pub centers: bool, } #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct NodeSnapping { +pub struct PointSnapping { pub paths: bool, pub path_intersections: bool, - pub sharp_nodes: bool, - pub smooth_nodes: bool, + pub point_handles_free: bool, + pub point_handles_colinear: bool, pub line_midpoints: bool, pub normals: bool, pub tangents: bool, @@ -227,8 +227,8 @@ pub enum BoardSnapSource { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GeometrySnapSource { - Smooth, - Sharp, + HandlesColinear, + HandlesFree, LineMidpoint, PathIntersection, Handle, @@ -258,8 +258,8 @@ pub enum BoundingBoxSnapTarget { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GeometrySnapTarget { - Smooth, - Sharp, + HandlesColinear, + HandlesFree, LineMidpoint, Path, Normal, 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 c88a9991..c774c56e 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -47,22 +47,20 @@ pub fn new_svg_layer(svg: String, transform: glam::DAffine2, id: NodeId, parent: LayerNodeIdentifier::new_unchecked(id) } -/// Batch set all of the manipulator groups to mirror on a specific layer -pub fn set_manipulator_mirror_angle(manipulator_groups: &[ManipulatorGroup], layer: LayerNodeIdentifier, mirror_angle: bool, responses: &mut VecDeque) { +/// Batch set all of the manipulator groups to set their colinear handle state on a specific layer +pub fn set_manipulator_colinear_handles_state(manipulator_groups: &[ManipulatorGroup], layer: LayerNodeIdentifier, colinear: bool, responses: &mut VecDeque) { for manipulator_group in manipulator_groups { responses.add(GraphOperationMessage::Vector { layer, - modification: VectorDataModification::SetManipulatorHandleMirroring { - id: manipulator_group.id, - mirror_angle, - }, + modification: VectorDataModification::SetManipulatorColinearHandlesState { id: manipulator_group.id, colinear }, }); } } /// Locate the subpaths from the shape nodes of a particular layer pub fn get_subpaths(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<&Vec>> { - if let TaggedValue::Subpaths(subpaths) = NodeGraphLayer::new(layer, document_network)?.find_input("Shape", 0)? { + let path_data_node_input_index = 0; + if let TaggedValue::Subpaths(subpaths) = NodeGraphLayer::new(layer, document_network).find_input("Shape", path_data_node_input_index)? { Some(subpaths) } else { None @@ -71,7 +69,8 @@ pub fn get_subpaths(layer: LayerNodeIdentifier, document_network: &NodeNetwork) /// Locate the final pivot from the transform (TODO: decide how the pivot should actually work) pub fn get_pivot(layer: LayerNodeIdentifier, network: &NodeNetwork) -> Option { - if let TaggedValue::DVec2(pivot) = NodeGraphLayer::new(layer, network)?.find_input("Transform", 5)? { + let pivot_node_input_index = 5; + if let TaggedValue::DVec2(pivot) = NodeGraphLayer::new(layer, network).find_input("Transform", pivot_node_input_index)? { Some(*pivot) } else { None @@ -84,10 +83,11 @@ pub fn get_viewport_pivot(layer: LayerNodeIdentifier, document_network: &NodeNet document_metadata.transform_to_viewport(layer).transform_point2(min + (max - min) * pivot) } -/// Get the currently mirrored handles for a particular layer from the shape node -pub fn get_mirror_handles(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<&Vec> { - if let TaggedValue::ManipulatorGroupIds(mirror_handles) = NodeGraphLayer::new(layer, document_network)?.find_input("Shape", 1)? { - Some(mirror_handles) +/// Get the manipulator groups that currently have colinear handles for a particular layer from the shape node +pub fn get_colinear_manipulators(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<&Vec> { + let colinear_manipulators_node_input_index = 1; + if let TaggedValue::ManipulatorGroupIds(manipulator_groups) = NodeGraphLayer::new(layer, document_network).find_input("Shape", colinear_manipulators_node_input_index)? { + Some(manipulator_groups) } else { None } @@ -95,7 +95,7 @@ pub fn get_mirror_handles(layer: LayerNodeIdentifier, document_network: &NodeNet /// Get the current gradient of a layer from the closest Fill node pub fn get_gradient(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option { - let inputs = NodeGraphLayer::new(layer, document_network)?.find_node_inputs("Fill")?; + let inputs = NodeGraphLayer::new(layer, document_network).find_node_inputs("Fill")?; let TaggedValue::FillType(FillType::Gradient) = inputs.get(1)?.as_value()? else { return None; }; @@ -125,7 +125,7 @@ pub fn get_gradient(layer: LayerNodeIdentifier, document_network: &NodeNetwork) /// Get the current fill of a layer from the closest Fill node pub fn get_fill_color(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option { - let inputs = NodeGraphLayer::new(layer, document_network)?.find_node_inputs("Fill")?; + let inputs = NodeGraphLayer::new(layer, document_network).find_node_inputs("Fill")?; let TaggedValue::Color(color) = inputs.get(2)?.as_value()? else { return None; }; @@ -134,7 +134,7 @@ pub fn get_fill_color(layer: LayerNodeIdentifier, document_network: &NodeNetwork /// Get the current blend mode of a layer from the closest Blend Mode node pub fn get_blend_mode(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option { - let inputs = NodeGraphLayer::new(layer, document_network)?.find_node_inputs("Blend Mode")?; + let inputs = NodeGraphLayer::new(layer, document_network).find_node_inputs("Blend Mode")?; let TaggedValue::BlendMode(blend_mode) = inputs.get(1)?.as_value()? else { return None; }; @@ -149,7 +149,7 @@ pub fn get_blend_mode(layer: LayerNodeIdentifier, document_network: &NodeNetwork /// - The default value of 100% if no Opacity node is present, but this function returns None in that case /// With those limitations in mind, the intention of this function is to show just the value already present in an upstream Opacity node so that value can be directly edited. pub fn get_opacity(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option { - let inputs = NodeGraphLayer::new(layer, document_network)?.find_node_inputs("Opacity")?; + let inputs = NodeGraphLayer::new(layer, document_network).find_node_inputs("Opacity")?; let TaggedValue::F64(opacity) = inputs.get(1)?.as_value()? else { return None; }; @@ -157,16 +157,16 @@ pub fn get_opacity(layer: LayerNodeIdentifier, document_network: &NodeNetwork) - } pub fn get_fill_id(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option { - NodeGraphLayer::new(layer, document_network)?.node_id("Fill") + NodeGraphLayer::new(layer, document_network).node_id("Fill") } pub fn get_text_id(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option { - NodeGraphLayer::new(layer, document_network)?.node_id("Text") + NodeGraphLayer::new(layer, document_network).node_id("Text") } /// Gets properties from the Text node pub fn get_text(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<(&String, &Font, f64)> { - let inputs = NodeGraphLayer::new(layer, document_network)?.find_node_inputs("Text")?; + let inputs = NodeGraphLayer::new(layer, document_network).find_node_inputs("Text")?; let NodeInput::Value { tagged_value: TaggedValue::String(text), .. @@ -195,7 +195,8 @@ pub fn get_text(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> O } pub fn get_stroke_width(layer: LayerNodeIdentifier, network: &NodeNetwork) -> Option { - if let TaggedValue::F64(width) = NodeGraphLayer::new(layer, network)?.find_input("Stroke", 2)? { + let weight_node_input_index = 2; + if let TaggedValue::F64(width) = NodeGraphLayer::new(layer, network).find_input("Stroke", weight_node_input_index)? { Some(*width) } else { None @@ -204,7 +205,7 @@ pub fn get_stroke_width(layer: LayerNodeIdentifier, network: &NodeNetwork) -> Op /// 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()) + NodeGraphLayer::new(layer, document_network).find_node_inputs(node_name).is_some() } /// Convert subpaths to an iterator of manipulator groups @@ -220,20 +221,16 @@ pub fn get_manipulator_from_id(subpaths: &[Subpath], id: Man /// An immutable reference to a layer within the document node graph for easy access. pub struct NodeGraphLayer<'a> { node_graph: &'a NodeNetwork, - _outwards_links: HashMap>, layer_node: NodeId, } impl<'a> NodeGraphLayer<'a> { /// Get the layer node from the document - pub fn new(layer: LayerNodeIdentifier, network: &'a NodeNetwork) -> Option { - let outwards_links = network.collect_outwards_links(); - - Some(Self { + pub fn new(layer: LayerNodeIdentifier, network: &'a NodeNetwork) -> Self { + Self { node_graph: network, - _outwards_links: outwards_links, layer_node: layer.to_node(), - }) + } } /// Return an iterator up the primary flow of the layer @@ -257,6 +254,7 @@ impl<'a> NodeGraphLayer<'a> { /// Find a specific input of a node within the layer's primary flow pub fn find_input(&self, node_name: &str, index: usize) -> Option<&'a TaggedValue> { + // TODO: Find a better way to accept a node input rather than using its index (which is quite unclear and fragile) self.find_node_inputs(node_name)?.get(index)?.as_value() } } diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index bbc07bdf..2c947874 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -1,11 +1,11 @@ use super::graph_modification_utils; -use super::snapping::{group_smooth, SnapCandidatePoint, SnapData, SnapManager, SnappedPoint}; +use super::snapping::{are_manipulator_handles_colinear, SnapCandidatePoint, SnapData, SnapManager, SnappedPoint}; 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}; use crate::messages::prelude::*; -use crate::messages::tool::common_functionality::graph_modification_utils::{get_manipulator_from_id, get_manipulator_groups, get_mirror_handles, get_subpaths}; +use crate::messages::tool::common_functionality::graph_modification_utils::{get_colinear_manipulators, get_manipulator_from_id, get_manipulator_groups, get_subpaths}; use bezier_rs::{Bezier, ManipulatorGroup, TValue}; use graph_craft::document::NodeNetwork; @@ -17,8 +17,8 @@ use glam::DVec2; #[derive(Debug, PartialEq, Copy, Clone)] pub enum ManipulatorAngle { - Smooth, - Sharp, + Colinear, + Free, Mixed, } @@ -277,10 +277,10 @@ impl ShapeState { } let source = if handle.is_handle() { SnapSource::Geometry(GeometrySnapSource::Handle) - } else if group_smooth(group, to_document, subpath, index) { - SnapSource::Geometry(GeometrySnapSource::Smooth) + } else if are_manipulator_handles_colinear(group, to_document, subpath, index) { + SnapSource::Geometry(GeometrySnapSource::HandlesColinear) } else { - SnapSource::Geometry(GeometrySnapSource::Sharp) + SnapSource::Geometry(GeometrySnapSource::HandlesFree) }; let Some(position) = handle.get_position(group) else { continue }; let mut point = SnapCandidatePoint::new_source(to_document.transform_point2(position) + mouse_delta, source); @@ -467,7 +467,7 @@ impl ShapeState { if point.manipulator_type.is_handle() { responses.add(GraphOperationMessage::Vector { layer, - modification: VectorDataModification::SetManipulatorHandleMirroring { id: group.id, mirror_angle: false }, + modification: VectorDataModification::SetManipulatorColinearHandlesState { id: group.id, colinear: false }, }); } @@ -490,28 +490,28 @@ impl ShapeState { Some(()) } - // Iterates over the selected manipulator groups, returning whether they have mixed, sharp, or smooth angles. - // If there are no points selected this function returns mixed. + /// Iterates over the selected manipulator groups, returning whether their handles have mixed, colinear, or free angles. + /// If there are no points selected this function returns mixed. pub fn selected_manipulator_angles(&self, document_network: &NodeNetwork) -> ManipulatorAngle { - // This iterator contains a bool indicating whether or not every selected point has a smooth manipulator angle. - let mut point_smoothness_status = self + // This iterator contains a bool indicating whether or not selected points' manipulator groups have colinear handles. + let mut points_colinear_status = self .selected_shape_state .iter() - .filter_map(|(&layer, selection_state)| Some((graph_modification_utils::get_mirror_handles(layer, document_network)?, selection_state))) - .flat_map(|(mirror, selection_state)| selection_state.selected_points.iter().map(|selected_point| mirror.contains(&selected_point.group))); + .filter_map(|(&layer, selection_state)| Some((graph_modification_utils::get_colinear_manipulators(layer, document_network)?, selection_state))) + .flat_map(|(colinear_manipulators, selection_state)| selection_state.selected_points.iter().map(|selected_point| colinear_manipulators.contains(&selected_point.group))); - let Some(first_is_smooth) = point_smoothness_status.next() else { return ManipulatorAngle::Mixed }; - - if point_smoothness_status.any(|point| first_is_smooth != point) { + let Some(first_is_colinear) = points_colinear_status.next() else { return ManipulatorAngle::Mixed }; + if points_colinear_status.any(|point| first_is_colinear != point) { return ManipulatorAngle::Mixed; } - match first_is_smooth { - false => ManipulatorAngle::Sharp, - true => ManipulatorAngle::Smooth, + + match first_is_colinear { + false => ManipulatorAngle::Free, + true => ManipulatorAngle::Colinear, } } - pub fn smooth_manipulator_group(&self, subpath: &bezier_rs::Subpath, index: usize, responses: &mut VecDeque, layer: LayerNodeIdentifier) { + pub fn convert_manipulator_handles_to_colinear(&self, subpath: &bezier_rs::Subpath, index: usize, responses: &mut VecDeque, layer: LayerNodeIdentifier) { let manipulator_groups = subpath.manipulator_groups(); let manipulator = manipulator_groups[index]; @@ -542,13 +542,10 @@ impl ShapeState { (None, None) => return, }; - // Mirror the angle but not the distance + // Set the manipulator to have colinear handles responses.add(GraphOperationMessage::Vector { layer, - modification: VectorDataModification::SetManipulatorHandleMirroring { - id: manipulator.id, - mirror_angle: true, - }, + modification: VectorDataModification::SetManipulatorColinearHandlesState { id: manipulator.id, colinear: true }, }); let (sin, cos) = handle_direction.sin_cos(); @@ -569,7 +566,6 @@ impl ShapeState { modification: VectorDataModification::SetManipulatorPosition { point, position: in_handle }, }); } - if let Some(out_handle) = length_next.map(|length| anchor_position - handle_vector * length) { let point = ManipulatorPointId::new(manipulator.id, SelectedType::OutHandle); responses.add(GraphOperationMessage::Vector { @@ -579,8 +575,11 @@ impl ShapeState { } } - /// Smooths the set of selected control points, assuming that the selected set is homogeneously sharp. - pub fn smooth_selected_groups(&self, responses: &mut VecDeque, document_network: &NodeNetwork) -> Option<()> { + /// Converts all selected points to colinear while moving the handles to ensure their 180° angle separation. + /// If only one handle is selected, the other handle will be moved to match the angle of the selected handle. + /// If both or neither handles are selected, the angle of both handles will be averaged from their current angles, weighted by their lengths. + /// Assumes all selected manipulators have handles that are already not colinear. + pub fn convert_selected_manipulators_to_colinear_handles(&self, responses: &mut VecDeque, document_network: &NodeNetwork) -> Option<()> { let mut skip_set = HashSet::new(); for (&layer, layer_state) in self.selected_shape_state.iter() { @@ -593,10 +592,6 @@ impl ShapeState { skip_set.insert(point.group); - let anchor_selected = layer_state.selected_points.contains(&ManipulatorPointId { - group: point.group, - manipulator_type: SelectedType::Anchor, - }); let out_selected = layer_state.selected_points.contains(&ManipulatorPointId { group: point.group, manipulator_type: SelectedType::OutHandle, @@ -607,8 +602,9 @@ impl ShapeState { }); let group = graph_modification_utils::get_manipulator_from_id(subpaths, point.group)?; - match (anchor_selected, out_selected, in_selected) { - (_, true, false) => { + match (out_selected, in_selected) { + // If the out handle is selected, only move the angle of the in handle + (true, false) => { let out_handle = ManipulatorPointId::new(point.group, SelectedType::OutHandle); if let Some(position) = group.out_handle { responses.add(GraphOperationMessage::Vector { @@ -617,7 +613,8 @@ impl ShapeState { }); } } - (_, false, true) => { + // If the in handle is selected, only move the angle of the out handle + (false, true) => { let in_handle = ManipulatorPointId::new(point.group, SelectedType::InHandle); if let Some(position) = group.in_handle { responses.add(GraphOperationMessage::Vector { @@ -626,7 +623,9 @@ impl ShapeState { }); } } - (_, _, _) => { + // If both or neither handles are selected, average the angles of the handles weighted proportional to their lengths + // TODO: This is bugged, it doesn't successfully average the angles + (_, _) => { let found = subpaths.iter().find_map(|subpath| { let group_slice = subpath.manipulator_groups(); let index = group_slice.iter().position(|manipulator| manipulator.id == group.id)?; @@ -635,7 +634,7 @@ impl ShapeState { }); if let Some((subpath, index)) = found { - self.smooth_manipulator_group(subpath, index, responses, layer); + self.convert_manipulator_handles_to_colinear(subpath, index, responses, layer); } } } @@ -646,10 +645,12 @@ impl ShapeState { } /// Move the selected points by dragging the mouse. - pub fn move_selected_points(&self, document_network: &NodeNetwork, document_metadata: &DocumentMetadata, delta: DVec2, mirror_distance: bool, responses: &mut VecDeque) { + pub fn move_selected_points(&self, document_network: &NodeNetwork, document_metadata: &DocumentMetadata, delta: DVec2, equidistant: bool, responses: &mut VecDeque) { for (&layer, state) in &self.selected_shape_state { let Some(subpaths) = get_subpaths(layer, document_network) else { continue }; - let Some(mirror_angle) = get_mirror_handles(layer, document_network) else { continue }; + let Some(colinear_manipulators) = get_colinear_manipulators(layer, document_network) else { + continue; + }; let transform = document_metadata.transform_to_viewport(layer); let delta = transform.inverse().transform_vector2(delta); @@ -677,20 +678,19 @@ impl ShapeState { move_point(ManipulatorPointId::new(point.group, SelectedType::OutHandle)); } - if mirror_distance && point.manipulator_type != SelectedType::Anchor { - let mut mirror = mirror_angle.contains(&point.group); + if equidistant && point.manipulator_type != SelectedType::Anchor { + let mut colinear = colinear_manipulators.contains(&point.group); - // If there is no opposing handle, we mirror even if mirror_angle doesn't contain the group - // and set angle mirroring to true. - if !mirror && point.manipulator_type.opposite().get_position(group).is_none() { + // If there is no opposing handle, we consider it colinear + if !colinear && point.manipulator_type.opposite().get_position(group).is_none() { responses.add(GraphOperationMessage::Vector { layer, - modification: VectorDataModification::SetManipulatorHandleMirroring { id: group.id, mirror_angle: true }, + modification: VectorDataModification::SetManipulatorColinearHandlesState { id: group.id, colinear: true }, }); - mirror = true; + colinear = true; } - if mirror { + if colinear { let Some(mut original_handle_position) = point.manipulator_type.get_position(group) else { continue; }; @@ -711,7 +711,7 @@ impl ShapeState { } } - /// Delete selected and mirrored handles with zero length when the drag stops. + /// Delete selected and colinear handles with zero length when the drag stops. pub fn delete_selected_handles_with_zero_length( &self, document_network: &NodeNetwork, @@ -721,7 +721,9 @@ impl ShapeState { ) { for (&layer, state) in &self.selected_shape_state { let Some(subpaths) = get_subpaths(layer, document_network) else { continue }; - let Some(mirror_angle) = get_mirror_handles(layer, document_network) else { continue }; + let Some(colinear_manipulators) = get_colinear_manipulators(layer, document_network) else { + continue; + }; let opposing_handle_lengths = opposing_handle_lengths.as_ref().and_then(|lengths| lengths.get(&layer)); @@ -749,9 +751,9 @@ impl ShapeState { modification: VectorDataModification::RemoveManipulatorPoint { point }, }); - // Remove opposing handle if it is not selected and is mirrored. + // Remove opposing handle if it is not selected and is colinear. let opposite_point = ManipulatorPointId::new(point.group, point.manipulator_type.opposite()); - if !state.is_selected(opposite_point) && mirror_angle.contains(&point.group) { + if !state.is_selected(opposite_point) && colinear_manipulators.contains(&point.group) { if let Some(lengths) = opposing_handle_lengths { if lengths.contains_key(&point.group) { responses.add(GraphOperationMessage::Vector { @@ -812,12 +814,14 @@ impl ShapeState { pub fn reset_opposing_handle_lengths(&self, document_network: &NodeNetwork, opposing_handle_lengths: &OpposingHandleLengths, responses: &mut VecDeque) { for (&layer, state) in &self.selected_shape_state { let Some(subpaths) = get_subpaths(layer, document_network) else { continue }; - let Some(mirror_angle) = get_mirror_handles(layer, document_network) else { continue }; + let Some(colinear_manipulators) = get_colinear_manipulators(layer, document_network) else { + continue; + }; let Some(opposing_handle_lengths) = opposing_handle_lengths.get(&layer) else { continue }; for subpath in subpaths { for manipulator_group in subpath.manipulator_groups() { - if !mirror_angle.contains(&manipulator_group.id) { + if !colinear_manipulators.contains(&manipulator_group.id) { continue; } @@ -1014,33 +1018,27 @@ impl ShapeState { broken_subpaths.push(bezier_rs::Subpath::new(final_segment, false)); } - responses.add(GraphOperationMessage::Vector { - layer, - modification: VectorDataModification::UpdateSubpaths { subpaths: broken_subpaths }, - }); + let modification = VectorDataModification::UpdateSubpaths { subpaths: broken_subpaths }; + responses.add(GraphOperationMessage::Vector { layer, modification }); } } - /// Toggle if the handles should mirror angle across the anchor position. - pub fn toggle_handle_mirroring_on_selected(&self, responses: &mut VecDeque) { + /// Toggle if the handles of the selected points should be colinear. + pub fn toggle_colinear_handles_state_on_selected(&self, responses: &mut VecDeque) { for (&layer, state) in &self.selected_shape_state { for point in &state.selected_points { - responses.add(GraphOperationMessage::Vector { - layer, - modification: VectorDataModification::ToggleManipulatorHandleMirroring { id: point.group }, - }) + let modification = VectorDataModification::ToggleManipulatorColinearHandlesState { id: point.group }; + responses.add(GraphOperationMessage::Vector { layer, modification }) } } } - /// Toggle if the handles should mirror angle across the anchor position. - pub fn set_handle_mirroring_on_selected(&self, mirror_angle: bool, responses: &mut VecDeque) { + /// Set whether the handles of the selected points should be colinear. + pub fn set_colinear_handles_state_on_selected(&self, colinear: bool, responses: &mut VecDeque) { for (&layer, state) in &self.selected_shape_state { for point in &state.selected_points { - responses.add(GraphOperationMessage::Vector { - layer, - modification: VectorDataModification::SetManipulatorHandleMirroring { id: point.group, mirror_angle }, - }); + let modification = VectorDataModification::SetManipulatorColinearHandlesState { id: point.group, colinear }; + responses.add(GraphOperationMessage::Vector { layer, modification }); } } } @@ -1160,8 +1158,10 @@ impl ShapeState { } } - /// Handles the flipping between sharp corner and smooth (which can be activated by double clicking on an anchor with the Path tool). - pub fn flip_sharp(&self, document_network: &NodeNetwork, document_metadata: &DocumentMetadata, position: glam::DVec2, tolerance: f64, responses: &mut VecDeque) -> bool { + /// Converts a nearby clicked anchor point's handles between sharp (zero-length handles) and smooth (pulled-apart handle(s)). + /// If both handles aren't zero-length, they are set that. If both are zero-length, they are stretched apart by a reasonable amount. + /// This can can be activated by double clicking on an anchor with the Path tool. + pub fn flip_smooth_sharp(&self, document_network: &NodeNetwork, document_metadata: &DocumentMetadata, position: glam::DVec2, tolerance: f64, responses: &mut VecDeque) -> bool { let mut process_layer = |layer| { let subpaths = get_subpaths(layer, document_network)?; @@ -1188,31 +1188,30 @@ impl ShapeState { // Check by comparing the handle positions to the anchor if this manipulator group is a point let already_sharp = match (manipulator.in_handle, manipulator.out_handle) { + // Check if both handles are zero-length (sharp) (Some(in_handle), Some(out_handle)) => anchor_position.abs_diff_eq(in_handle, 1e-10) && anchor_position.abs_diff_eq(out_handle, 1e-10), + // Check if the only one handle is zero-length (sharp) (Some(handle), None) | (None, Some(handle)) => anchor_position.abs_diff_eq(handle, 1e-10), + // No handles mean zero-length (sharp) (None, None) => true, }; if already_sharp { - self.smooth_manipulator_group(subpath, index, responses, layer); + self.convert_manipulator_handles_to_colinear(subpath, index, responses, layer); } else { + // Set in handle position to anchor position let point = ManipulatorPointId::new(manipulator.id, SelectedType::InHandle); - responses.add(GraphOperationMessage::Vector { - layer, - modification: VectorDataModification::SetManipulatorPosition { point, position: anchor_position }, - }); + let modification = VectorDataModification::SetManipulatorPosition { point, position: anchor_position }; + responses.add(GraphOperationMessage::Vector { layer, modification }); + + // Set out handle position to anchor position let point = ManipulatorPointId::new(manipulator.id, SelectedType::OutHandle); - responses.add(GraphOperationMessage::Vector { - layer, - modification: VectorDataModification::SetManipulatorPosition { point, position: anchor_position }, - }); - responses.add(GraphOperationMessage::Vector { - layer, - modification: VectorDataModification::SetManipulatorHandleMirroring { - id: manipulator.id, - mirror_angle: false, - }, - }); + let modification = VectorDataModification::SetManipulatorPosition { point, position: anchor_position }; + responses.add(GraphOperationMessage::Vector { layer, modification }); + + // Set the manipulator to have non-colinear handles + let modification = VectorDataModification::SetManipulatorColinearHandlesState { id: manipulator.id, colinear: false }; + responses.add(GraphOperationMessage::Vector { layer, modification }); }; Some(true) diff --git a/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs b/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs index 1c4984d2..27588031 100644 --- a/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs +++ b/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs @@ -327,10 +327,10 @@ impl SnapCandidatePoint { Self::new(document_point, source, SnapTarget::None) } pub fn handle(document_point: DVec2) -> Self { - Self::new_source(document_point, SnapSource::Geometry(GeometrySnapSource::Sharp)) + Self::new_source(document_point, SnapSource::Geometry(GeometrySnapSource::HandlesFree)) } pub fn handle_neighbors(document_point: DVec2, neighbors: impl Into>) -> Self { - let mut point = Self::new_source(document_point, SnapSource::Geometry(GeometrySnapSource::Sharp)); + let mut point = Self::new_source(document_point, SnapSource::Geometry(GeometrySnapSource::HandlesFree)); point.neighbors = neighbors.into(); point } @@ -399,34 +399,37 @@ fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath(group: &bezier_rs::ManipulatorGroup, to_document: DAffine2, subpath: &Subpath, index: usize) -> bool { +/// Returns true if both handles in a manipulator group are colinear, unless the anchor is an endpoint. Endpoint anchors are never considered colinear. +pub fn are_manipulator_handles_colinear(group: &bezier_rs::ManipulatorGroup, to_document: DAffine2, subpath: &Subpath, index: usize) -> bool { let anchor = group.anchor; let handle_in = group.in_handle.map(|handle| anchor - handle).filter(handle_not_under(to_document)); let handle_out = group.out_handle.map(|handle| handle - anchor).filter(handle_not_under(to_document)); - let at_end = !subpath.closed() && (index == 0 || index == subpath.len() - 1); + let anchor_is_endpoint = !subpath.closed() && (index == 0 || index == subpath.len() - 1); - handle_in.is_some_and(|handle_in| handle_out.is_some_and(|handle_out| handle_in.angle_between(handle_out) < 1e-5)) && !at_end + // Unless this is an endpoint, check if both handles are colinear (within an angular epsilon) + !anchor_is_endpoint && handle_in.is_some_and(|handle_in| handle_out.is_some_and(|handle_out| handle_in.angle_between(handle_out) < 1e-5)) } + pub fn get_layer_snap_points(layer: LayerNodeIdentifier, snap_data: &SnapData, points: &mut Vec) { let document = snap_data.document; if document.metadata().is_artboard(layer) { diff --git a/editor/src/messages/tool/tool_messages/ellipse_tool.rs b/editor/src/messages/tool/tool_messages/ellipse_tool.rs index aac778f1..4f76f1f0 100644 --- a/editor/src/messages/tool/tool_messages/ellipse_tool.rs +++ b/editor/src/messages/tool/tool_messages/ellipse_tool.rs @@ -205,7 +205,7 @@ impl Fsm for EllipseToolFsmState { let subpath = bezier_rs::Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE); let manipulator_groups = subpath.manipulator_groups().to_vec(); let layer = graph_modification_utils::new_vector_layer(vec![subpath], NodeId(generate_uuid()), document.new_layer_parent(), responses); - graph_modification_utils::set_manipulator_mirror_angle(&manipulator_groups, layer, true, responses); + graph_modification_utils::set_manipulator_colinear_handles_state(&manipulator_groups, layer, true, responses); shape_data.layer = Some(layer); let fill_color = tool_options.fill.active_color(); diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 67dfaad4..380a6581 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -4,7 +4,7 @@ use crate::messages::portfolio::document::overlays::utility_functions::path_over 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::auto_panning::AutoPanning; -use crate::messages::tool::common_functionality::graph_modification_utils::{get_manipulator_from_id, get_mirror_handles, get_subpaths}; +use crate::messages::tool::common_functionality::graph_modification_utils::{get_colinear_manipulators, get_manipulator_from_id, get_subpaths}; use crate::messages::tool::common_functionality::shape_editor::{ClosestSegment, ManipulatorAngle, ManipulatorPointInfo, OpposingHandleLengths, SelectedPointsInfo, ShapeState}; use crate::messages::tool::common_functionality::snapping::{SnapData, SnapManager}; @@ -34,19 +34,19 @@ pub enum PathToolMessage { Delete, DeleteAndBreakPath, DragStop { - shift_mirror_distance: Key, + equidistant: Key, }, Enter { add_to_selection: Key, }, Escape, - FlipSharp, + FlipSmoothSharp, GRS { // Should be `Key::KeyG` (Grab), `Key::KeyR` (Rotate), or `Key::KeyS` (Scale) key: Key, }, - ManipulatorAngleMakeSharp, - ManipulatorAngleMakeSmooth, + ManipulatorMakeHandlesFree, + ManipulatorMakeHandlesColinear, MouseDown { ctrl: Key, shift: Key, @@ -126,23 +126,37 @@ impl LayoutHolder for PathTool { let related_seperator = Separator::new(SeparatorType::Related).widget_holder(); let unrelated_seperator = Separator::new(SeparatorType::Unrelated).widget_holder(); - let manipulator_angle_options = vec![ - RadioEntryData::new("smooth").label("Smooth").on_update(|_| PathToolMessage::ManipulatorAngleMakeSmooth.into()), - RadioEntryData::new("sharp").label("Sharp").on_update(|_| PathToolMessage::ManipulatorAngleMakeSharp.into()), - ]; - let manipulator_angle_index = manipulator_angle.and_then(|angle| match angle { - ManipulatorAngle::Smooth => Some(0), - ManipulatorAngle::Sharp => Some(1), + let colinear_handles_tooltip = "Ensures both handles remain 180° apart"; + let colinear_handles_state = manipulator_angle.and_then(|angle| match angle { + ManipulatorAngle::Colinear => Some(true), + ManipulatorAngle::Free => Some(false), ManipulatorAngle::Mixed => None, - }); - - let manipulator_angle_radio = RadioInput::new(manipulator_angle_options) + }) + // TODO: Remove `unwrap_or_default` once checkboxes are capable of displaying a mixed state + .unwrap_or_default(); + let colinear_handle_checkbox = CheckboxInput::new(colinear_handles_state) .disabled(self.tool_data.selection_status.is_none()) - .selected_index(manipulator_angle_index) + .on_update(|&CheckboxInput { checked, .. }| { + if checked { + PathToolMessage::ManipulatorMakeHandlesColinear.into() + } else { + PathToolMessage::ManipulatorMakeHandlesFree.into() + } + }) + .tooltip(colinear_handles_tooltip) .widget_holder(); + let colinear_handles_label = TextLabel::new("Colinear Handles").tooltip(colinear_handles_tooltip).widget_holder(); Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { - widgets: vec![x_location, related_seperator, y_location, unrelated_seperator, manipulator_angle_radio], + widgets: vec![ + x_location, + related_seperator.clone(), + y_location, + unrelated_seperator, + colinear_handle_checkbox, + related_seperator, + colinear_handles_label, + ], }])) } } @@ -164,7 +178,7 @@ impl<'a> MessageHandler> for PathToo match self.fsm_state { Ready => actions!(PathToolMessageDiscriminant; - FlipSharp, + FlipSmoothSharp, MouseDown, Delete, NudgeSelectedPoints, @@ -177,7 +191,7 @@ impl<'a> MessageHandler> for PathToo Dragging => actions!(PathToolMessageDiscriminant; Escape, RightClick, - FlipSharp, + FlipSmoothSharp, DragStop, PointerMove, Delete, @@ -185,7 +199,7 @@ impl<'a> MessageHandler> for PathToo DeleteAndBreakPath, ), DrawingBox => actions!(PathToolMessageDiscriminant; - FlipSharp, + FlipSmoothSharp, DragStop, PointerMove, Delete, @@ -378,7 +392,7 @@ impl PathToolData { // Check if the alt key has just been pressed if alt && !self.alt_debounce { self.opposing_handle_lengths = None; - shape_editor.toggle_handle_mirroring_on_selected(responses); + shape_editor.toggle_colinear_handles_state_on_selected(responses); } self.alt_debounce = alt; @@ -545,21 +559,21 @@ impl Fsm for PathToolFsmState { PathToolFsmState::Ready } // Mouse up - (PathToolFsmState::DrawingBox, PathToolMessage::DragStop { shift_mirror_distance }) => { - let shift_pressed = input.keyboard.get(shift_mirror_distance as usize); + (PathToolFsmState::DrawingBox, PathToolMessage::DragStop { equidistant }) => { + let equidistant = input.keyboard.get(equidistant as usize); if tool_data.drag_start_pos == tool_data.previous_mouse_position { responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] }); } else { - shape_editor.select_all_in_quad(&document.network, &document.metadata, [tool_data.drag_start_pos, tool_data.previous_mouse_position], !shift_pressed); + shape_editor.select_all_in_quad(&document.network, &document.metadata, [tool_data.drag_start_pos, tool_data.previous_mouse_position], !equidistant); } responses.add(OverlaysMessage::Draw); responses.add(PathToolMessage::SelectedPointUpdated); PathToolFsmState::Ready } - (_, PathToolMessage::DragStop { shift_mirror_distance }) => { - let shift_pressed = input.keyboard.get(shift_mirror_distance as usize); + (_, PathToolMessage::DragStop { equidistant }) => { + let equidistant = input.keyboard.get(equidistant as usize); let nearest_point = shape_editor .find_nearest_point_indices(&document.network, &document.metadata, input.mouse.position, SELECTION_THRESHOLD) @@ -567,7 +581,7 @@ impl Fsm for PathToolFsmState { shape_editor.delete_selected_handles_with_zero_length(&document.network, &document.metadata, &tool_data.opposing_handle_lengths, responses); - if tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD && !shift_pressed { + if tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD && !equidistant { let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == Some(point)); if clicked_selected { shape_editor.deselect_all_points(); @@ -598,9 +612,9 @@ impl Fsm for PathToolFsmState { shape_editor.delete_point_and_break_path(&document.network, responses); PathToolFsmState::Ready } - (_, PathToolMessage::FlipSharp) => { + (_, PathToolMessage::FlipSmoothSharp) => { if !tool_data.double_click_handled { - shape_editor.flip_sharp(&document.network, &document.metadata, input.mouse.position, SELECTION_TOLERANCE, responses); + shape_editor.flip_smooth_sharp(&document.network, &document.metadata, input.mouse.position, SELECTION_TOLERANCE, responses); responses.add(PathToolMessage::SelectedPointUpdated); } self @@ -641,16 +655,16 @@ impl Fsm for PathToolFsmState { tool_data.selection_status = get_selection_status(&document.network, &document.metadata, shape_editor); self } - (_, PathToolMessage::ManipulatorAngleMakeSmooth) => { + (_, PathToolMessage::ManipulatorMakeHandlesColinear) => { responses.add(DocumentMessage::StartTransaction); - shape_editor.set_handle_mirroring_on_selected(true, responses); - shape_editor.smooth_selected_groups(responses, &document.network); + shape_editor.set_colinear_handles_state_on_selected(true, responses); + shape_editor.convert_selected_manipulators_to_colinear_handles(responses, &document.network); responses.add(DocumentMessage::CommitTransaction); PathToolFsmState::Ready } - (_, PathToolMessage::ManipulatorAngleMakeSharp) => { + (_, PathToolMessage::ManipulatorMakeHandlesFree) => { responses.add(DocumentMessage::StartTransaction); - shape_editor.set_handle_mirroring_on_selected(false, responses); + shape_editor.set_colinear_handles_state_on_selected(false, responses); responses.add(DocumentMessage::CommitTransaction); PathToolFsmState::Ready } @@ -663,6 +677,8 @@ impl Fsm for PathToolFsmState { PathToolFsmState::Ready => HintData(vec![ HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Select Point"), HintInfo::keys([Key::Shift], "Extend Selection").prepend_plus()]), HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Insert Point on Segment")]), + // TODO: Only show if at least one anchor is selected, and dynamically show either "Smooth" or "Sharp" based on the current state + HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDouble, "Make Anchor Smooth/Sharp")]), // TODO: Only show the following hints if at least one point is selected HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Drag Selected")]), HintGroup(vec![HintInfo::keys([Key::KeyG, Key::KeyR, Key::KeyS], "Grab/Rotate/Scale Selected")]), @@ -677,15 +693,13 @@ impl Fsm for PathToolFsmState { PathToolFsmState::Dragging => HintData(vec![ HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), HintGroup(vec![ - // TODO: Make hint dynamically say "Make Handle Smooth" or "Make Handle Sharp" based on its current state - // TODO: Switch this to the "S" key - // TODO: Only show this if a handle (not an anchor) is being dragged, and disable that shortcut so it can't be pressed even with the hint not shown - HintInfo::keys([Key::Alt], "Toggle Smooth/Sharp Handles"), - // TODO: Switch this to the "Alt" key (since it's equivalent to the "From Center" modifier when drawing a line) - // TODO: Show this only when a handle is being dragged - HintInfo::keys([Key::Shift], "Equidistant Handles (Smooth Only)"), - // TODO: Add "Snap 15°" modifier with the "Shift" key (only when a handle is being dragged) - // TODO: Add "Lock Angle" modifier with the "Ctrl" key (only when a handle is being dragged) + // TODO: Switch this to the "S" key. Also, make the hint dynamically say "Make Colinear" or "Make Not Colinear" based on its current state. And only + // TODO: show this hint if a handle (not an anchor) is being dragged, and disable that shortcut so it can't be pressed even with the hint not shown. + HintInfo::keys([Key::Alt], "Toggle Colinear Handles"), + // TODO: Switch this to the "Alt" key (since it's equivalent to the "From Center" modifier when drawing a line). And show this only when a handle is being dragged. + HintInfo::keys([Key::Shift], "Equidistant Handles"), + // TODO: Add "Snap 15°" modifier with the "Shift" key (only when a handle is being dragged). + // TODO: Add "Lock Angle" modifier with the "Ctrl" key (only when a handle is being dragged). ]), ]), PathToolFsmState::DrawingBox => HintData(vec![ @@ -761,28 +775,31 @@ fn get_selection_status(document_network: &NodeNetwork, document_metadata: &Docu let Some(layer) = selection_layers.find(|(_, v)| *v > 0).map(|(k, _)| k) else { return SelectionStatus::None; }; - let Some(subpaths) = get_subpaths(layer, document_network) else { return SelectionStatus::None; }; - let Some(mirror) = get_mirror_handles(layer, document_network) else { + let Some(colinear_manipulators) = get_colinear_manipulators(layer, document_network) else { return SelectionStatus::None; }; let Some(point) = shape_state.selected_points().next() else { return SelectionStatus::None; }; - - let Some(group) = get_manipulator_from_id(subpaths, point.group) else { + let Some(manipulator) = get_manipulator_from_id(subpaths, point.group) else { return SelectionStatus::None; }; - let Some(local_position) = point.manipulator_type.get_position(group) else { + let Some(local_position) = point.manipulator_type.get_position(manipulator) else { return SelectionStatus::None; }; - let manipulator_angle = if mirror.contains(&point.group) { ManipulatorAngle::Smooth } else { ManipulatorAngle::Sharp }; + let coordinates = document_metadata.transform_to_document(layer).transform_point2(local_position); + let manipulator_angle = if colinear_manipulators.contains(&point.group) { + ManipulatorAngle::Colinear + } else { + ManipulatorAngle::Free + }; return SelectionStatus::One(SingleSelectedPoint { - coordinates: document_metadata.transform_to_document(layer).transform_point2(local_position), + coordinates, layer, id: *point, manipulator_angle, diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 060262d5..941bef87 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -200,7 +200,7 @@ struct PenToolData { layer: Option, subpath_index: usize, snap_manager: SnapManager, - should_mirror: bool, + colinear_handles: bool, // Indicates that curve extension is occurring from the first point, rather than (more commonly) the last point from_start: bool, angle: f64, @@ -212,21 +212,16 @@ impl PenToolData { self.from_start = from_start; self.subpath_index = subpath_index; - // Stop the handles on the first point from mirroring - let Some(subpaths) = get_subpaths(layer, &document.network) else { - return; - }; + let Some(subpaths) = get_subpaths(layer, &document.network) else { return }; let manipulator_groups = subpaths[subpath_index].manipulator_groups(); - let Some(last_handle) = (if from_start { manipulator_groups.first() } else { manipulator_groups.last() }) else { - return; - }; + let first_or_last = if from_start { manipulator_groups.first() } else { manipulator_groups.last() }; + let Some(last_handle) = first_or_last else { return }; + let id = last_handle.id; + // Stop the handles on the first point from being colinear responses.add(GraphOperationMessage::Vector { layer, - modification: VectorDataModification::SetManipulatorHandleMirroring { - id: last_handle.id, - mirror_angle: false, - }, + modification: VectorDataModification::SetManipulatorColinearHandlesState { id, colinear: false }, }); } @@ -270,53 +265,56 @@ impl PenToolData { self.subpath_index = 0; } - // TODO: tooltip / user documentation? - /// If you place the anchor on top of the previous anchor then you break the mirror - fn check_break(&mut self, document: &DocumentMessageHandler, transform: DAffine2, responses: &mut VecDeque) -> Option<()> { - // Get subpath - let layer = self.layer?; - let subpath = &get_subpaths(layer, &document.network)?[self.subpath_index]; + /// If the user places the anchor on top of the previous anchor, it becomes sharp and the outgoing handle may be dragged. + fn bend_from_previous_point(&mut self, document: &DocumentMessageHandler, transform: DAffine2, responses: &mut VecDeque) { + (|| -> Option<()> { + // Get subpath + let layer = self.layer?; + let subpath = &get_subpaths(layer, &document.network)?[self.subpath_index]; - // Get the last manipulator group and the one previous to that - let mut manipulator_groups = subpath.manipulator_groups().iter(); - let last_manipulator_group = if self.from_start { manipulator_groups.next()? } else { manipulator_groups.next_back()? }; - let previous_manipulator_group = if self.from_start { manipulator_groups.next()? } else { manipulator_groups.next_back()? }; + // Get the last manipulator group and the one previous to that + let mut manipulator_groups = subpath.manipulator_groups().iter(); + let last_manipulator_group = if self.from_start { manipulator_groups.next()? } else { manipulator_groups.next_back()? }; + let previous_manipulator_group = if self.from_start { manipulator_groups.next()? } else { manipulator_groups.next_back()? }; - // Get correct handle types - let outwards_handle = if self.from_start { SelectedType::InHandle } else { SelectedType::OutHandle }; + // Get correct handle types + let outwards_handle = if self.from_start { SelectedType::InHandle } else { SelectedType::OutHandle }; - // Get manipulator points - let last_anchor = last_manipulator_group.anchor; - let previous_anchor = previous_manipulator_group.anchor; + // Get manipulator points + let last_anchor = last_manipulator_group.anchor; + let previous_anchor = previous_manipulator_group.anchor; - // Break the control - let transform = document.metadata.document_to_viewport * transform; - let on_top = transform.transform_point2(last_anchor).distance_squared(transform.transform_point2(previous_anchor)) < crate::consts::SNAP_POINT_TOLERANCE.powi(2); - if !on_top { - return None; - } - // Remove the point that has just been placed - responses.add(GraphOperationMessage::Vector { - layer, - modification: VectorDataModification::RemoveManipulatorGroup { id: last_manipulator_group.id }, - }); + // Break the control + let transform = document.metadata.document_to_viewport * transform; + let on_top = transform.transform_point2(last_anchor).distance_squared(transform.transform_point2(previous_anchor)) < crate::consts::SNAP_POINT_TOLERANCE.powi(2); + if !on_top { + return None; + } - // Move the in handle of the previous anchor to on top of the previous position - let point = ManipulatorPointId::new(previous_manipulator_group.id, outwards_handle); - responses.add(GraphOperationMessage::Vector { - layer, - modification: VectorDataModification::SetManipulatorPosition { point, position: previous_anchor }, - }); + // Remove the point that has just been placed + responses.add(GraphOperationMessage::Vector { + layer, + modification: VectorDataModification::RemoveManipulatorGroup { id: last_manipulator_group.id }, + }); - // Stop the handles on the last point from mirroring - let id = previous_manipulator_group.id; - responses.add(GraphOperationMessage::Vector { - layer, - modification: VectorDataModification::SetManipulatorHandleMirroring { id, mirror_angle: false }, - }); + // Move the in handle of the previous anchor to on top of the previous position + let point = ManipulatorPointId::new(previous_manipulator_group.id, outwards_handle); + responses.add(GraphOperationMessage::Vector { + layer, + modification: VectorDataModification::SetManipulatorPosition { point, position: previous_anchor }, + }); - self.should_mirror = false; - None + // Stop the handles on the last point from being colinear + let id = previous_manipulator_group.id; + responses.add(GraphOperationMessage::Vector { + layer, + modification: VectorDataModification::SetManipulatorColinearHandlesState { id, colinear: false }, + }); + + self.colinear_handles = false; + + None + })(); } fn finish_placing_handle(&mut self, document: &DocumentMessageHandler, transform: DAffine2, responses: &mut VecDeque) -> Option { @@ -357,11 +355,11 @@ impl PenToolData { modification: VectorDataModification::SetManipulatorPosition { point, position: last_in }, }); - // Stop the handles on the first point from mirroring + // Stop the handles on the first point from being colinear let id = first_manipulator_group.id; responses.add(GraphOperationMessage::Vector { layer, - modification: VectorDataModification::SetManipulatorHandleMirroring { id, mirror_angle: false }, + modification: VectorDataModification::SetManipulatorColinearHandlesState { id, colinear: false }, }); // Remove the point that has just been placed @@ -409,11 +407,11 @@ impl PenToolData { // Get manipulator points let last_anchor = last_manipulator_group.anchor; - let should_mirror = !modifiers.break_handle && self.should_mirror; + let colinear = !modifiers.break_handle && self.colinear_handles; snap_data.manipulators = vec![(self.layer?, last_manipulator_group.id)]; - let pos = self.compute_snapped_angle(snap_data, transform, modifiers.lock_angle, modifiers.snap_angle, should_mirror, mouse, Some(last_anchor), false); - if !pos.is_finite() { + let position = self.compute_snapped_angle(snap_data, transform, modifiers.lock_angle, modifiers.snap_angle, colinear, mouse, Some(last_anchor), false); + if !position.is_finite() { return Some(PenToolFsmState::DraggingHandle); } @@ -421,25 +419,25 @@ impl PenToolData { let point = ManipulatorPointId::new(last_manipulator_group.id, outwards_handle); responses.add(GraphOperationMessage::Vector { layer: self.layer?, - modification: VectorDataModification::SetManipulatorPosition { point, position: pos }, + modification: VectorDataModification::SetManipulatorPosition { point, position }, }); - // Mirror handle of last segment - if should_mirror { + // Place the previous anchor's in handle at the opposing position + if colinear { // Could also be written as `last_anchor.position * 2 - pos` but this way avoids overflow/underflow better - let pos = last_anchor - (pos - last_anchor); + let position = last_anchor - (position - last_anchor); let point = ManipulatorPointId::new(last_manipulator_group.id, inwards_handle); responses.add(GraphOperationMessage::Vector { layer: self.layer?, - modification: VectorDataModification::SetManipulatorPosition { point, position: pos }, + modification: VectorDataModification::SetManipulatorPosition { point, position }, }); } - // Update the mirror status of the currently modifying point + // Update the colinear handles status of the currently modifying point let id = last_manipulator_group.id; responses.add(GraphOperationMessage::Vector { layer: self.layer?, - modification: VectorDataModification::SetManipulatorHandleMirroring { id, mirror_angle: should_mirror }, + modification: VectorDataModification::SetManipulatorColinearHandlesState { id, colinear }, }); Some(PenToolFsmState::DraggingHandle) @@ -487,7 +485,7 @@ impl PenToolData { } /// Snap the angle of the line from relative to position if the key is pressed. - fn compute_snapped_angle(&mut self, snap_data: SnapData, transform: DAffine2, lock_angle: bool, snap_angle: bool, mirror: bool, mouse: DVec2, relative: Option, neighbor: bool) -> DVec2 { + fn compute_snapped_angle(&mut self, snap_data: SnapData, transform: DAffine2, lock_angle: bool, snap_angle: bool, colinear: bool, mouse: DVec2, relative: Option, neighbor: bool) -> DVec2 { let document = snap_data.document; let mut document_pos = document.metadata.document_to_viewport.inverse().transform_point2(mouse); let snap = &mut self.snap_manager; @@ -509,7 +507,7 @@ impl PenToolData { }; let near_point = SnapCandidatePoint::handle_neighbors(document_pos, neighbors.clone()); let far_point = SnapCandidatePoint::handle_neighbors(2. * relative - document_pos, neighbors); - if mirror { + if colinear { let snapped = snap.constrained_snap(&snap_data, &near_point, constraint, None); let snapped_far = snap.constrained_snap(&snap_data, &far_point, constraint, None); document_pos = if snapped_far.other_snap_better(&snapped) { @@ -523,7 +521,7 @@ impl PenToolData { document_pos = snapped.snapped_point_document; snap.update_indicator(snapped); } - } else if let Some(relative) = relative.map(|layer| transform.transform_point2(layer)).filter(|_| mirror) { + } else if let Some(relative) = relative.map(|layer| transform.transform_point2(layer)).filter(|_| colinear) { let snapped = snap.free_snap(&snap_data, &SnapCandidatePoint::handle_neighbors(document_pos, neighbors.clone()), None, false); let snapped_far = snap.free_snap(&snap_data, &SnapCandidatePoint::handle_neighbors(2. * relative - document_pos, neighbors), None, false); document_pos = if snapped_far.other_snap_better(&snapped) { @@ -641,8 +639,8 @@ impl Fsm for PenToolFsmState { (PenToolFsmState::Ready, PenToolMessage::DragStart) => { responses.add(DocumentMessage::StartTransaction); - // Disable this tool's mirroring - tool_data.should_mirror = false; + // Prevent the initial point from having a colinear in handle while dragging the out handle + tool_data.colinear_handles = false; // Perform extension of an existing path if let Some((layer, subpath_index, from_start)) = should_extend(document, input.mouse.position, crate::consts::SNAP_POINT_TOLERANCE) { @@ -663,11 +661,11 @@ impl Fsm for PenToolFsmState { } (PenToolFsmState::PlacingAnchor, PenToolMessage::DragStart) => { responses.add(DocumentMessage::StartTransaction); - tool_data.check_break(document, transform, responses); + tool_data.bend_from_previous_point(document, transform, responses); PenToolFsmState::DraggingHandle } (PenToolFsmState::DraggingHandle, PenToolMessage::DragStop) => { - tool_data.should_mirror = true; + tool_data.colinear_handles = true; tool_data.finish_placing_handle(document, transform, responses).unwrap_or(PenToolFsmState::PlacingAnchor) } (PenToolFsmState::DraggingHandle, PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }) => { @@ -677,6 +675,7 @@ impl Fsm for PenToolFsmState { break_handle: input.keyboard.key(break_handle), }; let snap_data = SnapData::new(document, input); + let state = tool_data .drag_handle(snap_data, transform, input.mouse.position, modifiers, responses) .unwrap_or(PenToolFsmState::Ready); @@ -772,6 +771,10 @@ impl Fsm for PenToolFsmState { ]), HintGroup(vec![HintInfo::keys([Key::Shift], "Snap 15°"), HintInfo::keys([Key::Control], "Lock Angle")]), HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Add Sharp Point"), HintInfo::mouse(MouseMotion::LmbDrag, "Add Smooth Point")]), + HintGroup(vec![ + HintInfo::mouse(MouseMotion::Lmb, ""), + HintInfo::mouse(MouseMotion::LmbDrag, "Bend from Prev. Point").prepend_slash(), + ]), ]), PenToolFsmState::DraggingHandle => HintData(vec![ HintGroup(vec![ @@ -780,6 +783,7 @@ impl Fsm for PenToolFsmState { HintInfo::keys([Key::Enter], "End Path").prepend_slash(), ]), HintGroup(vec![HintInfo::keys([Key::Shift], "Snap 15°"), HintInfo::keys([Key::Control], "Lock Angle")]), + // TODO: Only show this if the handle being dragged is colinear, so don't show this when bending from the previous point (by clicking and dragging from the previously placed anchor) HintGroup(vec![HintInfo::keys([Key::Alt], "Bend Handle")]), ]), }; diff --git a/editor/src/messages/tool/tool_messages/spline_tool.rs b/editor/src/messages/tool/tool_messages/spline_tool.rs index 2a1f2fdb..5146ae77 100644 --- a/editor/src/messages/tool/tool_messages/spline_tool.rs +++ b/editor/src/messages/tool/tool_messages/spline_tool.rs @@ -343,7 +343,7 @@ fn update_spline(tool_data: &SplineToolData, show_preview: bool, responses: &mut return; }; - graph_modification_utils::set_manipulator_mirror_angle(subpath.manipulator_groups(), layer, true, responses); + graph_modification_utils::set_manipulator_colinear_handles_state(subpath.manipulator_groups(), layer, true, responses); let subpaths = vec![subpath]; let modification = VectorDataModification::UpdateSubpaths { subpaths }; responses.add_front(GraphOperationMessage::Vector { layer, modification }); diff --git a/frontend/assets/icon-16px-two-tone/mouse-hint-lmb-double.svg b/frontend/assets/icon-16px-two-tone/mouse-hint-lmb-double.svg index c25da603..7b13e4d0 100644 --- a/frontend/assets/icon-16px-two-tone/mouse-hint-lmb-double.svg +++ b/frontend/assets/icon-16px-two-tone/mouse-hint-lmb-double.svg @@ -1,4 +1,4 @@ - + diff --git a/frontend/assets/icon-16px-two-tone/mouse-hint-rmb-double.svg b/frontend/assets/icon-16px-two-tone/mouse-hint-rmb-double.svg index 2a3cebeb..ba32a899 100644 --- a/frontend/assets/icon-16px-two-tone/mouse-hint-rmb-double.svg +++ b/frontend/assets/icon-16px-two-tone/mouse-hint-rmb-double.svg @@ -1,4 +1,4 @@ - + diff --git a/frontend/src/utility-functions/icons.ts b/frontend/src/utility-functions/icons.ts index 9c9e9bf8..6abb9c6d 100644 --- a/frontend/src/utility-functions/icons.ts +++ b/frontend/src/utility-functions/icons.ts @@ -231,6 +231,7 @@ import MouseHintLmb from "@graphite-frontend/assets/icon-16px-two-tone/mouse-hin import MouseHintMmbDrag from "@graphite-frontend/assets/icon-16px-two-tone/mouse-hint-mmb-drag.svg"; import MouseHintMmb from "@graphite-frontend/assets/icon-16px-two-tone/mouse-hint-mmb.svg"; import MouseHintNone from "@graphite-frontend/assets/icon-16px-two-tone/mouse-hint-none.svg"; +import MouseHintRmbDouble from "@graphite-frontend/assets/icon-16px-two-tone/mouse-hint-rmb-double.svg"; import MouseHintRmbDrag from "@graphite-frontend/assets/icon-16px-two-tone/mouse-hint-rmb-drag.svg"; import MouseHintRmb from "@graphite-frontend/assets/icon-16px-two-tone/mouse-hint-rmb.svg"; import MouseHintScrollDown from "@graphite-frontend/assets/icon-16px-two-tone/mouse-hint-scroll-down.svg"; @@ -245,6 +246,7 @@ const TWO_TONE_16PX = { MouseHintMmbDrag: { svg: MouseHintMmbDrag, size: 16 }, MouseHintNone: { svg: MouseHintNone, size: 16 }, MouseHintRmb: { svg: MouseHintRmb, size: 16 }, + MouseHintRmbDouble: { svg: MouseHintRmbDouble, size: 16 }, MouseHintRmbDrag: { svg: MouseHintRmbDrag, size: 16 }, MouseHintScrollDown: { svg: MouseHintScrollDown, size: 16 }, MouseHintScrollUp: { svg: MouseHintScrollUp, size: 16 }, diff --git a/libraries/bezier-rs/src/subpath/manipulators.rs b/libraries/bezier-rs/src/subpath/manipulators.rs index 336b81c8..a648a165 100644 --- a/libraries/bezier-rs/src/subpath/manipulators.rs +++ b/libraries/bezier-rs/src/subpath/manipulators.rs @@ -196,7 +196,7 @@ mod tests { } #[test] - fn insert_at_exisiting_manipulator_group_of_open_subpath() { + fn insert_at_existing_manipulator_group_of_open_subpath() { // This will do nothing to the subpath let mut subpath = set_up_open_subpath(); let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.75)); diff --git a/node-graph/gcore/src/vector/generator_nodes.rs b/node-graph/gcore/src/vector/generator_nodes.rs index bc84c42b..c74cade2 100644 --- a/node-graph/gcore/src/vector/generator_nodes.rs +++ b/node-graph/gcore/src/vector/generator_nodes.rs @@ -98,14 +98,14 @@ fn spline_generator(_input: (), positions: Vec) -> VectorData { // TODO(TrueDoctor): I removed the Arc requirement we should think about when it makes sense to use it vs making a generic value node #[derive(Debug, Clone)] -pub struct PathGenerator { - mirror: Mirror, +pub struct PathGenerator { + colinear_manipulators: ColinearManipulators, } #[node_macro::node_fn(PathGenerator)] -fn generate_path(path_data: Vec>, mirror: Vec) -> super::VectorData { +fn generate_path(path_data: Vec>, colinear_manipulators: Vec) -> super::VectorData { let mut vector_data = super::VectorData::from_subpaths(path_data); - vector_data.mirror_angle = mirror; + vector_data.colinear_manipulators = colinear_manipulators; vector_data } diff --git a/node-graph/gcore/src/vector/vector_data.rs b/node-graph/gcore/src/vector/vector_data.rs index f3dd461e..5dcb8700 100644 --- a/node-graph/gcore/src/vector/vector_data.rs +++ b/node-graph/gcore/src/vector/vector_data.rs @@ -18,9 +18,9 @@ pub struct VectorData { pub transform: DAffine2, pub style: PathStyle, pub alpha_blending: AlphaBlending, - /// A list of all manipulator groups (referenced in `subpaths`) that have smooth handles (where their handles are colinear, or locked to 180° angles from one another) - /// This gets read in `graph_operation_message_handler.rs` by calling `inputs.as_mut_slice()` (search for the string `"Shape does not have subpath and mirror angle inputs"` to find it). - pub mirror_angle: Vec, + /// A list of all manipulator groups (referenced in `subpaths`) that have colinear handles (where they're locked at 180° angles from one another). + /// This gets read in `graph_operation_message_handler.rs` by calling `inputs.as_mut_slice()` (search for the string `"Shape does not have both `subpath` and `colinear_manipulators` inputs"` to find it). + pub colinear_manipulators: Vec, pub point_domain: PointDomain, pub segment_domain: SegmentDomain, @@ -35,7 +35,7 @@ impl core::hash::Hash for VectorData { self.transform.to_cols_array().iter().for_each(|x| x.to_bits().hash(state)); self.style.hash(state); self.alpha_blending.hash(state); - self.mirror_angle.hash(state); + self.colinear_manipulators.hash(state); } } @@ -46,7 +46,7 @@ impl VectorData { transform: DAffine2::IDENTITY, style: PathStyle::new(Some(Stroke::new(Some(Color::BLACK), 0.)), super::style::Fill::None), alpha_blending: AlphaBlending::new(), - mirror_angle: Vec::new(), + colinear_manipulators: Vec::new(), point_domain: PointDomain::new(), segment_domain: SegmentDomain::new(), region_domain: RegionDomain::new(), diff --git a/node-graph/gcore/src/vector/vector_data/attributes.rs b/node-graph/gcore/src/vector/vector_data/attributes.rs index 88140ee0..5d82cbf3 100644 --- a/node-graph/gcore/src/vector/vector_data/attributes.rs +++ b/node-graph/gcore/src/vector/vector_data/attributes.rs @@ -364,7 +364,7 @@ impl crate::vector::ConcatElement for super::VectorData { self.region_domain.concat(&other.region_domain, transform * other.transform, &id_map); // TODO: properly deal with fills such as gradients self.style = other.style.clone(); - self.mirror_angle.extend(other.mirror_angle.iter().copied()); + self.colinear_manipulators.extend(other.colinear_manipulators.iter().copied()); self.alpha_blending = other.alpha_blending; } }