Rename handle mirroring to colinear

This commit is contained in:
Keavon Chambers 2024-03-14 04:02:16 -07:00
parent ea4f3d8bba
commit 5bca931813
20 changed files with 326 additions and 305 deletions

View File

@ -200,11 +200,9 @@ pub fn default_mapping() -> Mapping {
entry!(KeyDown(KeyA); modifiers=[Accel], action_dispatch=PathToolMessage::SelectAllAnchors), entry!(KeyDown(KeyA); modifiers=[Accel], action_dispatch=PathToolMessage::SelectAllAnchors),
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], action_dispatch=PathToolMessage::DeselectAllPoints), entry!(KeyDown(KeyA); modifiers=[Accel, Shift], action_dispatch=PathToolMessage::DeselectAllPoints),
entry!(KeyDown(Backspace); action_dispatch=PathToolMessage::Delete), entry!(KeyDown(Backspace); action_dispatch=PathToolMessage::Delete),
entry!(KeyUp(Lmb); action_dispatch=PathToolMessage::DragStop { shift_mirror_distance: Shift }), entry!(KeyUp(Lmb); action_dispatch=PathToolMessage::DragStop { equidistant: Shift }),
entry!(KeyDown(Enter); action_dispatch=PathToolMessage::Enter { entry!(KeyDown(Enter); action_dispatch=PathToolMessage::Enter { add_to_selection: Shift }),
add_to_selection: Shift entry!(DoubleClick(MouseButton::Left); action_dispatch=PathToolMessage::FlipSmoothSharp),
}),
entry!(DoubleClick(MouseButton::Left); action_dispatch=PathToolMessage::FlipSharp),
entry!(KeyDown(ArrowRight); action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: NUDGE_AMOUNT, delta_y: 0. }), entry!(KeyDown(ArrowRight); 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=[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 }), entry!(KeyDown(ArrowRight); modifiers=[ArrowUp], action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }),

View File

@ -132,8 +132,8 @@ pub enum VectorDataModification {
RemoveManipulatorGroup { id: ManipulatorGroupId }, RemoveManipulatorGroup { id: ManipulatorGroupId },
RemoveManipulatorPoint { point: ManipulatorPointId }, RemoveManipulatorPoint { point: ManipulatorPointId },
SetClosed { index: usize, closed: bool }, SetClosed { index: usize, closed: bool },
SetManipulatorHandleMirroring { id: ManipulatorGroupId, mirror_angle: bool }, SetManipulatorColinearHandlesState { id: ManipulatorGroupId, colinear: bool },
SetManipulatorPosition { point: ManipulatorPointId, position: DVec2 }, SetManipulatorPosition { point: ManipulatorPointId, position: DVec2 },
ToggleManipulatorHandleMirroring { id: ManipulatorGroupId }, ToggleManipulatorColinearHandlesState { id: ManipulatorGroupId },
UpdateSubpaths { subpaths: Vec<Subpath<ManipulatorGroupId>> }, UpdateSubpaths { subpaths: Vec<Subpath<ManipulatorGroupId>> },
} }

View File

@ -462,8 +462,8 @@ impl<'a> ModifyInputsContext<'a> {
let mut empty = false; let mut empty = false;
self.modify_inputs("Shape", false, |inputs, _node_id, _metadata| { self.modify_inputs("Shape", false, |inputs, _node_id, _metadata| {
let [subpaths, mirror_angle_groups] = inputs.as_mut_slice() else { let [subpaths, colinear_manipulators] = inputs.as_mut_slice() else {
panic!("Shape does not have subpath and mirror angle inputs"); panic!("Shape does not have both `subpath` and `colinear_manipulators` inputs");
}; };
let NodeInput::Value { let NodeInput::Value {
@ -474,16 +474,16 @@ impl<'a> ModifyInputsContext<'a> {
return; return;
}; };
let NodeInput::Value { let NodeInput::Value {
tagged_value: TaggedValue::ManipulatorGroupIds(mirror_angle_groups), tagged_value: TaggedValue::ManipulatorGroupIds(colinear_manipulators),
.. ..
} = mirror_angle_groups } = colinear_manipulators
else { else {
return; return;
}; };
[old_bounds_min, old_bounds_max] = transform_utils::nonzero_subpath_bounds(subpaths); [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()); empty = !subpaths.iter().any(|subpath| !subpath.is_empty());
[new_bounds_min, new_bounds_max] = transform_utils::nonzero_subpath_bounds(subpaths); [new_bounds_min, new_bounds_max] = transform_utils::nonzero_subpath_bounds(subpaths);

View File

@ -199,7 +199,7 @@ pub fn nonzero_subpath_bounds(subpaths: &[Subpath<ManipulatorGroupId>]) -> [DVec
pub struct VectorModificationState<'a> { pub struct VectorModificationState<'a> {
pub subpaths: &'a mut Vec<Subpath<ManipulatorGroupId>>, pub subpaths: &'a mut Vec<Subpath<ManipulatorGroupId>>,
pub mirror_angle_groups: &'a mut Vec<ManipulatorGroupId>, pub colinear_manipulators: &'a mut Vec<ManipulatorGroupId>,
} }
impl<'a> VectorModificationState<'a> { impl<'a> VectorModificationState<'a> {
fn insert_start(&mut self, subpath_index: usize, manipulator_group: ManipulatorGroup<ManipulatorGroupId>) { fn insert_start(&mut self, subpath_index: usize, manipulator_group: ManipulatorGroup<ManipulatorGroupId>) {
@ -246,19 +246,19 @@ impl<'a> VectorModificationState<'a> {
} }
} }
fn set_mirror(&mut self, id: ManipulatorGroupId, mirror_angle: bool) { fn set_manipulator_colinear_handles_state(&mut self, id: ManipulatorGroupId, colinear: bool) {
if !mirror_angle { if !colinear {
self.mirror_angle_groups.retain(|&mirrored_id| mirrored_id != id); self.colinear_manipulators.retain(|&manipulator_group_id| manipulator_group_id != id);
} else if !self.mirror_angle_groups.contains(&id) { } else if !self.colinear_manipulators.contains(&id) {
self.mirror_angle_groups.push(id); self.colinear_manipulators.push(id);
} }
} }
fn toggle_mirror(&mut self, id: ManipulatorGroupId) { fn toggle_manipulator_colinear_handles_state(&mut self, id: ManipulatorGroupId) {
if self.mirror_angle_groups.contains(&id) { if self.colinear_manipulators.contains(&id) {
self.mirror_angle_groups.retain(|&mirrored_id| mirrored_id != id); self.colinear_manipulators.retain(|&manipulator_group_id| manipulator_group_id != id);
} else { } 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::InHandle => manipulator.in_handle = Some(position),
SelectedType::OutHandle => manipulator.out_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| { let reflect = |opposite: DVec2| {
(manipulator.anchor - position) (manipulator.anchor - position)
.try_normalize() .try_normalize()
@ -298,9 +298,9 @@ impl<'a> VectorModificationState<'a> {
VectorDataModification::RemoveManipulatorGroup { id } => self.remove_group(id), VectorDataModification::RemoveManipulatorGroup { id } => self.remove_group(id),
VectorDataModification::RemoveManipulatorPoint { point } => self.remove_point(point), VectorDataModification::RemoveManipulatorPoint { point } => self.remove_point(point),
VectorDataModification::SetClosed { index, closed } => self.subpaths[index].set_closed(closed), 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::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, VectorDataModification::UpdateSubpaths { subpaths } => *self.subpaths = subpaths,
} }
} }

View File

@ -2442,7 +2442,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
}), }),
inputs: vec![ inputs: vec![
DocumentInputType::value("Path Data", TaggedValue::Subpaths(vec![]), false), 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)], outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)],
..Default::default() ..Default::default()

View File

@ -61,7 +61,7 @@ pub struct SnappingState {
pub geometry_snapping: bool, pub geometry_snapping: bool,
pub grid_snapping: bool, pub grid_snapping: bool,
pub bounds: BoundsSnapping, pub bounds: BoundsSnapping,
pub nodes: NodeSnapping, pub nodes: PointSnapping,
pub grid: GridSnapping, pub grid: GridSnapping,
pub tolerance: f64, pub tolerance: f64,
pub artboards: bool, pub artboards: bool,
@ -79,11 +79,11 @@ impl Default for SnappingState {
edge_midpoints: false, edge_midpoints: false,
centers: true, centers: true,
}, },
nodes: NodeSnapping { nodes: PointSnapping {
paths: true, paths: true,
path_intersections: true, path_intersections: true,
sharp_nodes: true, point_handles_free: true,
smooth_nodes: true, point_handles_colinear: true,
line_midpoints: true, line_midpoints: true,
normals: true, normals: true,
tangents: true, tangents: true,
@ -110,8 +110,8 @@ impl SnappingState {
BoundingBoxSnapTarget::Center => self.bounds.centers, BoundingBoxSnapTarget::Center => self.bounds.centers,
}, },
SnapTarget::Geometry(nodes) if self.geometry_snapping => match nodes { SnapTarget::Geometry(nodes) if self.geometry_snapping => match nodes {
GeometrySnapTarget::Smooth => self.nodes.smooth_nodes, GeometrySnapTarget::HandlesColinear => self.nodes.point_handles_colinear,
GeometrySnapTarget::Sharp => self.nodes.sharp_nodes, GeometrySnapTarget::HandlesFree => self.nodes.point_handles_free,
GeometrySnapTarget::LineMidpoint => self.nodes.line_midpoints, GeometrySnapTarget::LineMidpoint => self.nodes.line_midpoints,
GeometrySnapTarget::Path => self.nodes.paths, GeometrySnapTarget::Path => self.nodes.paths,
GeometrySnapTarget::Normal => self.nodes.normals, GeometrySnapTarget::Normal => self.nodes.normals,
@ -132,11 +132,11 @@ pub struct BoundsSnapping {
pub centers: bool, pub centers: bool,
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NodeSnapping { pub struct PointSnapping {
pub paths: bool, pub paths: bool,
pub path_intersections: bool, pub path_intersections: bool,
pub sharp_nodes: bool, pub point_handles_free: bool,
pub smooth_nodes: bool, pub point_handles_colinear: bool,
pub line_midpoints: bool, pub line_midpoints: bool,
pub normals: bool, pub normals: bool,
pub tangents: bool, pub tangents: bool,
@ -227,8 +227,8 @@ pub enum BoardSnapSource {
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GeometrySnapSource { pub enum GeometrySnapSource {
Smooth, HandlesColinear,
Sharp, HandlesFree,
LineMidpoint, LineMidpoint,
PathIntersection, PathIntersection,
Handle, Handle,
@ -258,8 +258,8 @@ pub enum BoundingBoxSnapTarget {
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GeometrySnapTarget { pub enum GeometrySnapTarget {
Smooth, HandlesColinear,
Sharp, HandlesFree,
LineMidpoint, LineMidpoint,
Path, Path,
Normal, Normal,

View File

@ -47,22 +47,20 @@ pub fn new_svg_layer(svg: String, transform: glam::DAffine2, id: NodeId, parent:
LayerNodeIdentifier::new_unchecked(id) LayerNodeIdentifier::new_unchecked(id)
} }
/// Batch set all of the manipulator groups to mirror on a specific layer /// Batch set all of the manipulator groups to set their colinear handle state on a specific layer
pub fn set_manipulator_mirror_angle(manipulator_groups: &[ManipulatorGroup<ManipulatorGroupId>], layer: LayerNodeIdentifier, mirror_angle: bool, responses: &mut VecDeque<Message>) { pub fn set_manipulator_colinear_handles_state(manipulator_groups: &[ManipulatorGroup<ManipulatorGroupId>], layer: LayerNodeIdentifier, colinear: bool, responses: &mut VecDeque<Message>) {
for manipulator_group in manipulator_groups { for manipulator_group in manipulator_groups {
responses.add(GraphOperationMessage::Vector { responses.add(GraphOperationMessage::Vector {
layer, layer,
modification: VectorDataModification::SetManipulatorHandleMirroring { modification: VectorDataModification::SetManipulatorColinearHandlesState { id: manipulator_group.id, colinear },
id: manipulator_group.id,
mirror_angle,
},
}); });
} }
} }
/// Locate the subpaths from the shape nodes of a particular layer /// Locate the subpaths from the shape nodes of a particular layer
pub fn get_subpaths(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<&Vec<Subpath<ManipulatorGroupId>>> { pub fn get_subpaths(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<&Vec<Subpath<ManipulatorGroupId>>> {
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) Some(subpaths)
} else { } else {
None 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) /// Locate the final pivot from the transform (TODO: decide how the pivot should actually work)
pub fn get_pivot(layer: LayerNodeIdentifier, network: &NodeNetwork) -> Option<DVec2> { pub fn get_pivot(layer: LayerNodeIdentifier, network: &NodeNetwork) -> Option<DVec2> {
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) Some(*pivot)
} else { } else {
None 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) 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 /// Get the manipulator groups that currently have colinear handles for a particular layer from the shape node
pub fn get_mirror_handles(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<&Vec<ManipulatorGroupId>> { pub fn get_colinear_manipulators(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<&Vec<ManipulatorGroupId>> {
if let TaggedValue::ManipulatorGroupIds(mirror_handles) = NodeGraphLayer::new(layer, document_network)?.find_input("Shape", 1)? { let colinear_manipulators_node_input_index = 1;
Some(mirror_handles) if let TaggedValue::ManipulatorGroupIds(manipulator_groups) = NodeGraphLayer::new(layer, document_network).find_input("Shape", colinear_manipulators_node_input_index)? {
Some(manipulator_groups)
} else { } else {
None 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 /// Get the current gradient of a layer from the closest Fill node
pub fn get_gradient(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<Gradient> { pub fn get_gradient(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<Gradient> {
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 { let TaggedValue::FillType(FillType::Gradient) = inputs.get(1)?.as_value()? else {
return None; 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 /// Get the current fill of a layer from the closest Fill node
pub fn get_fill_color(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<Color> { pub fn get_fill_color(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<Color> {
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 { let TaggedValue::Color(color) = inputs.get(2)?.as_value()? else {
return None; 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 /// 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<BlendMode> { pub fn get_blend_mode(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<BlendMode> {
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 { let TaggedValue::BlendMode(blend_mode) = inputs.get(1)?.as_value()? else {
return None; 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 /// - 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. /// 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<f64> { pub fn get_opacity(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<f64> {
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 { let TaggedValue::F64(opacity) = inputs.get(1)?.as_value()? else {
return None; 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<NodeId> { pub fn get_fill_id(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<NodeId> {
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<NodeId> { pub fn get_text_id(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<NodeId> {
NodeGraphLayer::new(layer, document_network)?.node_id("Text") NodeGraphLayer::new(layer, document_network).node_id("Text")
} }
/// Gets properties from the Text node /// Gets properties from the Text node
pub fn get_text(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<(&String, &Font, f64)> { 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 { let NodeInput::Value {
tagged_value: TaggedValue::String(text), 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<f64> { pub fn get_stroke_width(layer: LayerNodeIdentifier, network: &NodeNetwork) -> Option<f64> {
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) Some(*width)
} else { } else {
None 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. /// 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 { 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 /// Convert subpaths to an iterator of manipulator groups
@ -220,20 +221,16 @@ pub fn get_manipulator_from_id(subpaths: &[Subpath<ManipulatorGroupId>], id: Man
/// An immutable reference to a layer within the document node graph for easy access. /// An immutable reference to a layer within the document node graph for easy access.
pub struct NodeGraphLayer<'a> { pub struct NodeGraphLayer<'a> {
node_graph: &'a NodeNetwork, node_graph: &'a NodeNetwork,
_outwards_links: HashMap<NodeId, Vec<NodeId>>,
layer_node: NodeId, layer_node: NodeId,
} }
impl<'a> NodeGraphLayer<'a> { impl<'a> NodeGraphLayer<'a> {
/// Get the layer node from the document /// Get the layer node from the document
pub fn new(layer: LayerNodeIdentifier, network: &'a NodeNetwork) -> Option<Self> { pub fn new(layer: LayerNodeIdentifier, network: &'a NodeNetwork) -> Self {
let outwards_links = network.collect_outwards_links(); Self {
Some(Self {
node_graph: network, node_graph: network,
_outwards_links: outwards_links,
layer_node: layer.to_node(), layer_node: layer.to_node(),
}) }
} }
/// Return an iterator up the primary flow of the layer /// 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 /// 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> { 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() self.find_node_inputs(node_name)?.get(index)?.as_value()
} }
} }

View File

@ -1,11 +1,11 @@
use super::graph_modification_utils; 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::consts::{DRAG_THRESHOLD, INSERT_POINT_ON_SEGMENT_TOO_CLOSE_DISTANCE};
use crate::messages::portfolio::document::node_graph::VectorDataModification; 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::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
use crate::messages::portfolio::document::utility_types::misc::{GeometrySnapSource, SnapSource}; use crate::messages::portfolio::document::utility_types::misc::{GeometrySnapSource, SnapSource};
use crate::messages::prelude::*; 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 bezier_rs::{Bezier, ManipulatorGroup, TValue};
use graph_craft::document::NodeNetwork; use graph_craft::document::NodeNetwork;
@ -17,8 +17,8 @@ use glam::DVec2;
#[derive(Debug, PartialEq, Copy, Clone)] #[derive(Debug, PartialEq, Copy, Clone)]
pub enum ManipulatorAngle { pub enum ManipulatorAngle {
Smooth, Colinear,
Sharp, Free,
Mixed, Mixed,
} }
@ -277,10 +277,10 @@ impl ShapeState {
} }
let source = if handle.is_handle() { let source = if handle.is_handle() {
SnapSource::Geometry(GeometrySnapSource::Handle) SnapSource::Geometry(GeometrySnapSource::Handle)
} else if group_smooth(group, to_document, subpath, index) { } else if are_manipulator_handles_colinear(group, to_document, subpath, index) {
SnapSource::Geometry(GeometrySnapSource::Smooth) SnapSource::Geometry(GeometrySnapSource::HandlesColinear)
} else { } else {
SnapSource::Geometry(GeometrySnapSource::Sharp) SnapSource::Geometry(GeometrySnapSource::HandlesFree)
}; };
let Some(position) = handle.get_position(group) else { continue }; let Some(position) = handle.get_position(group) else { continue };
let mut point = SnapCandidatePoint::new_source(to_document.transform_point2(position) + mouse_delta, source); let mut point = SnapCandidatePoint::new_source(to_document.transform_point2(position) + mouse_delta, source);
@ -467,7 +467,7 @@ impl ShapeState {
if point.manipulator_type.is_handle() { if point.manipulator_type.is_handle() {
responses.add(GraphOperationMessage::Vector { responses.add(GraphOperationMessage::Vector {
layer, 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(()) Some(())
} }
// Iterates over the selected manipulator groups, returning whether they have mixed, sharp, or smooth angles. /// 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. /// If there are no points selected this function returns mixed.
pub fn selected_manipulator_angles(&self, document_network: &NodeNetwork) -> ManipulatorAngle { 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. // This iterator contains a bool indicating whether or not selected points' manipulator groups have colinear handles.
let mut point_smoothness_status = self let mut points_colinear_status = self
.selected_shape_state .selected_shape_state
.iter() .iter()
.filter_map(|(&layer, selection_state)| Some((graph_modification_utils::get_mirror_handles(layer, document_network)?, selection_state))) .filter_map(|(&layer, selection_state)| Some((graph_modification_utils::get_colinear_manipulators(layer, document_network)?, selection_state)))
.flat_map(|(mirror, selection_state)| selection_state.selected_points.iter().map(|selected_point| mirror.contains(&selected_point.group))); .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 }; let Some(first_is_colinear) = points_colinear_status.next() else { return ManipulatorAngle::Mixed };
if points_colinear_status.any(|point| first_is_colinear != point) {
if point_smoothness_status.any(|point| first_is_smooth != point) {
return ManipulatorAngle::Mixed; return ManipulatorAngle::Mixed;
} }
match first_is_smooth {
false => ManipulatorAngle::Sharp, match first_is_colinear {
true => ManipulatorAngle::Smooth, false => ManipulatorAngle::Free,
true => ManipulatorAngle::Colinear,
} }
} }
pub fn smooth_manipulator_group(&self, subpath: &bezier_rs::Subpath<ManipulatorGroupId>, index: usize, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier) { pub fn convert_manipulator_handles_to_colinear(&self, subpath: &bezier_rs::Subpath<ManipulatorGroupId>, index: usize, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier) {
let manipulator_groups = subpath.manipulator_groups(); let manipulator_groups = subpath.manipulator_groups();
let manipulator = manipulator_groups[index]; let manipulator = manipulator_groups[index];
@ -542,13 +542,10 @@ impl ShapeState {
(None, None) => return, (None, None) => return,
}; };
// Mirror the angle but not the distance // Set the manipulator to have colinear handles
responses.add(GraphOperationMessage::Vector { responses.add(GraphOperationMessage::Vector {
layer, layer,
modification: VectorDataModification::SetManipulatorHandleMirroring { modification: VectorDataModification::SetManipulatorColinearHandlesState { id: manipulator.id, colinear: true },
id: manipulator.id,
mirror_angle: true,
},
}); });
let (sin, cos) = handle_direction.sin_cos(); let (sin, cos) = handle_direction.sin_cos();
@ -569,7 +566,6 @@ impl ShapeState {
modification: VectorDataModification::SetManipulatorPosition { point, position: in_handle }, modification: VectorDataModification::SetManipulatorPosition { point, position: in_handle },
}); });
} }
if let Some(out_handle) = length_next.map(|length| anchor_position - handle_vector * length) { if let Some(out_handle) = length_next.map(|length| anchor_position - handle_vector * length) {
let point = ManipulatorPointId::new(manipulator.id, SelectedType::OutHandle); let point = ManipulatorPointId::new(manipulator.id, SelectedType::OutHandle);
responses.add(GraphOperationMessage::Vector { 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. /// Converts all selected points to colinear while moving the handles to ensure their 180° angle separation.
pub fn smooth_selected_groups(&self, responses: &mut VecDeque<Message>, document_network: &NodeNetwork) -> Option<()> { /// 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<Message>, document_network: &NodeNetwork) -> Option<()> {
let mut skip_set = HashSet::new(); let mut skip_set = HashSet::new();
for (&layer, layer_state) in self.selected_shape_state.iter() { for (&layer, layer_state) in self.selected_shape_state.iter() {
@ -593,10 +592,6 @@ impl ShapeState {
skip_set.insert(point.group); 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 { let out_selected = layer_state.selected_points.contains(&ManipulatorPointId {
group: point.group, group: point.group,
manipulator_type: SelectedType::OutHandle, manipulator_type: SelectedType::OutHandle,
@ -607,8 +602,9 @@ impl ShapeState {
}); });
let group = graph_modification_utils::get_manipulator_from_id(subpaths, point.group)?; let group = graph_modification_utils::get_manipulator_from_id(subpaths, point.group)?;
match (anchor_selected, out_selected, in_selected) { match (out_selected, in_selected) {
(_, true, false) => { // 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); let out_handle = ManipulatorPointId::new(point.group, SelectedType::OutHandle);
if let Some(position) = group.out_handle { if let Some(position) = group.out_handle {
responses.add(GraphOperationMessage::Vector { 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); let in_handle = ManipulatorPointId::new(point.group, SelectedType::InHandle);
if let Some(position) = group.in_handle { if let Some(position) = group.in_handle {
responses.add(GraphOperationMessage::Vector { 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 found = subpaths.iter().find_map(|subpath| {
let group_slice = subpath.manipulator_groups(); let group_slice = subpath.manipulator_groups();
let index = group_slice.iter().position(|manipulator| manipulator.id == group.id)?; let index = group_slice.iter().position(|manipulator| manipulator.id == group.id)?;
@ -635,7 +634,7 @@ impl ShapeState {
}); });
if let Some((subpath, index)) = found { 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. /// 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<Message>) { pub fn move_selected_points(&self, document_network: &NodeNetwork, document_metadata: &DocumentMetadata, delta: DVec2, equidistant: bool, responses: &mut VecDeque<Message>) {
for (&layer, state) in &self.selected_shape_state { for (&layer, state) in &self.selected_shape_state {
let Some(subpaths) = get_subpaths(layer, document_network) else { continue }; 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 transform = document_metadata.transform_to_viewport(layer);
let delta = transform.inverse().transform_vector2(delta); let delta = transform.inverse().transform_vector2(delta);
@ -677,20 +678,19 @@ impl ShapeState {
move_point(ManipulatorPointId::new(point.group, SelectedType::OutHandle)); move_point(ManipulatorPointId::new(point.group, SelectedType::OutHandle));
} }
if mirror_distance && point.manipulator_type != SelectedType::Anchor { if equidistant && point.manipulator_type != SelectedType::Anchor {
let mut mirror = mirror_angle.contains(&point.group); 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 // If there is no opposing handle, we consider it colinear
// and set angle mirroring to true. if !colinear && point.manipulator_type.opposite().get_position(group).is_none() {
if !mirror && point.manipulator_type.opposite().get_position(group).is_none() {
responses.add(GraphOperationMessage::Vector { responses.add(GraphOperationMessage::Vector {
layer, 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 { let Some(mut original_handle_position) = point.manipulator_type.get_position(group) else {
continue; 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( pub fn delete_selected_handles_with_zero_length(
&self, &self,
document_network: &NodeNetwork, document_network: &NodeNetwork,
@ -721,7 +721,9 @@ impl ShapeState {
) { ) {
for (&layer, state) in &self.selected_shape_state { for (&layer, state) in &self.selected_shape_state {
let Some(subpaths) = get_subpaths(layer, document_network) else { continue }; 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)); 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 }, 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()); 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 let Some(lengths) = opposing_handle_lengths {
if lengths.contains_key(&point.group) { if lengths.contains_key(&point.group) {
responses.add(GraphOperationMessage::Vector { 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<Message>) { pub fn reset_opposing_handle_lengths(&self, document_network: &NodeNetwork, opposing_handle_lengths: &OpposingHandleLengths, responses: &mut VecDeque<Message>) {
for (&layer, state) in &self.selected_shape_state { for (&layer, state) in &self.selected_shape_state {
let Some(subpaths) = get_subpaths(layer, document_network) else { continue }; 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 }; let Some(opposing_handle_lengths) = opposing_handle_lengths.get(&layer) else { continue };
for subpath in subpaths { for subpath in subpaths {
for manipulator_group in subpath.manipulator_groups() { for manipulator_group in subpath.manipulator_groups() {
if !mirror_angle.contains(&manipulator_group.id) { if !colinear_manipulators.contains(&manipulator_group.id) {
continue; continue;
} }
@ -1014,33 +1018,27 @@ impl ShapeState {
broken_subpaths.push(bezier_rs::Subpath::new(final_segment, false)); broken_subpaths.push(bezier_rs::Subpath::new(final_segment, false));
} }
responses.add(GraphOperationMessage::Vector { let modification = VectorDataModification::UpdateSubpaths { subpaths: broken_subpaths };
layer, responses.add(GraphOperationMessage::Vector { layer, modification });
modification: VectorDataModification::UpdateSubpaths { subpaths: broken_subpaths },
});
} }
} }
/// Toggle if the handles should mirror angle across the anchor position. /// Toggle if the handles of the selected points should be colinear.
pub fn toggle_handle_mirroring_on_selected(&self, responses: &mut VecDeque<Message>) { pub fn toggle_colinear_handles_state_on_selected(&self, responses: &mut VecDeque<Message>) {
for (&layer, state) in &self.selected_shape_state { for (&layer, state) in &self.selected_shape_state {
for point in &state.selected_points { for point in &state.selected_points {
responses.add(GraphOperationMessage::Vector { let modification = VectorDataModification::ToggleManipulatorColinearHandlesState { id: point.group };
layer, responses.add(GraphOperationMessage::Vector { layer, modification })
modification: VectorDataModification::ToggleManipulatorHandleMirroring { id: point.group },
})
} }
} }
} }
/// Toggle if the handles should mirror angle across the anchor position. /// Set whether the handles of the selected points should be colinear.
pub fn set_handle_mirroring_on_selected(&self, mirror_angle: bool, responses: &mut VecDeque<Message>) { pub fn set_colinear_handles_state_on_selected(&self, colinear: bool, responses: &mut VecDeque<Message>) {
for (&layer, state) in &self.selected_shape_state { for (&layer, state) in &self.selected_shape_state {
for point in &state.selected_points { for point in &state.selected_points {
responses.add(GraphOperationMessage::Vector { let modification = VectorDataModification::SetManipulatorColinearHandlesState { id: point.group, colinear };
layer, responses.add(GraphOperationMessage::Vector { layer, modification });
modification: VectorDataModification::SetManipulatorHandleMirroring { id: point.group, mirror_angle },
});
} }
} }
} }
@ -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). /// Converts a nearby clicked anchor point's handles between sharp (zero-length handles) and smooth (pulled-apart handle(s)).
pub fn flip_sharp(&self, document_network: &NodeNetwork, document_metadata: &DocumentMetadata, position: glam::DVec2, tolerance: f64, responses: &mut VecDeque<Message>) -> bool { /// 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<Message>) -> bool {
let mut process_layer = |layer| { let mut process_layer = |layer| {
let subpaths = get_subpaths(layer, document_network)?; 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 // 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) { 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), (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), (Some(handle), None) | (None, Some(handle)) => anchor_position.abs_diff_eq(handle, 1e-10),
// No handles mean zero-length (sharp)
(None, None) => true, (None, None) => true,
}; };
if already_sharp { if already_sharp {
self.smooth_manipulator_group(subpath, index, responses, layer); self.convert_manipulator_handles_to_colinear(subpath, index, responses, layer);
} else { } else {
// Set in handle position to anchor position
let point = ManipulatorPointId::new(manipulator.id, SelectedType::InHandle); let point = ManipulatorPointId::new(manipulator.id, SelectedType::InHandle);
responses.add(GraphOperationMessage::Vector { let modification = VectorDataModification::SetManipulatorPosition { point, position: anchor_position };
layer, responses.add(GraphOperationMessage::Vector { layer, modification });
modification: VectorDataModification::SetManipulatorPosition { point, position: anchor_position },
}); // Set out handle position to anchor position
let point = ManipulatorPointId::new(manipulator.id, SelectedType::OutHandle); let point = ManipulatorPointId::new(manipulator.id, SelectedType::OutHandle);
responses.add(GraphOperationMessage::Vector { let modification = VectorDataModification::SetManipulatorPosition { point, position: anchor_position };
layer, responses.add(GraphOperationMessage::Vector { layer, modification });
modification: VectorDataModification::SetManipulatorPosition { point, position: anchor_position },
}); // Set the manipulator to have non-colinear handles
responses.add(GraphOperationMessage::Vector { let modification = VectorDataModification::SetManipulatorColinearHandlesState { id: manipulator.id, colinear: false };
layer, responses.add(GraphOperationMessage::Vector { layer, modification });
modification: VectorDataModification::SetManipulatorHandleMirroring {
id: manipulator.id,
mirror_angle: false,
},
});
}; };
Some(true) Some(true)

View File

@ -327,10 +327,10 @@ impl SnapCandidatePoint {
Self::new(document_point, source, SnapTarget::None) Self::new(document_point, source, SnapTarget::None)
} }
pub fn handle(document_point: DVec2) -> Self { 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<Vec<DVec2>>) -> Self { pub fn handle_neighbors(document_point: DVec2, neighbors: impl Into<Vec<DVec2>>) -> 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.neighbors = neighbors.into();
point point
} }
@ -399,34 +399,37 @@ fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath<Poin
continue; continue;
} }
let smooth = group_smooth(group, to_document, subpath, index); let colinear = are_manipulator_handles_colinear(group, to_document, subpath, index);
if smooth && document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::Smooth)) { if colinear && document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::HandlesColinear)) {
// Smooth points // Colinear handles
points.push(SnapCandidatePoint::new( points.push(SnapCandidatePoint::new(
to_document.transform_point2(group.anchor), to_document.transform_point2(group.anchor),
SnapSource::Geometry(GeometrySnapSource::Smooth), SnapSource::Geometry(GeometrySnapSource::HandlesColinear),
SnapTarget::Geometry(GeometrySnapTarget::Smooth), SnapTarget::Geometry(GeometrySnapTarget::HandlesColinear),
)); ));
} else if !smooth && document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::Sharp)) { } else if !colinear && document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::HandlesFree)) {
// Sharp points // Free handles
points.push(SnapCandidatePoint::new( points.push(SnapCandidatePoint::new(
to_document.transform_point2(group.anchor), to_document.transform_point2(group.anchor),
SnapSource::Geometry(GeometrySnapSource::Sharp), SnapSource::Geometry(GeometrySnapSource::HandlesFree),
SnapTarget::Geometry(GeometrySnapTarget::Sharp), SnapTarget::Geometry(GeometrySnapTarget::HandlesFree),
)); ));
} }
} }
} }
pub fn group_smooth<Id: bezier_rs::Identifier>(group: &bezier_rs::ManipulatorGroup<Id>, to_document: DAffine2, subpath: &Subpath<Id>, 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<Id: bezier_rs::Identifier>(group: &bezier_rs::ManipulatorGroup<Id>, to_document: DAffine2, subpath: &Subpath<Id>, index: usize) -> bool {
let anchor = group.anchor; let anchor = group.anchor;
let handle_in = group.in_handle.map(|handle| anchor - handle).filter(handle_not_under(to_document)); 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 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<SnapCandidatePoint>) { pub fn get_layer_snap_points(layer: LayerNodeIdentifier, snap_data: &SnapData, points: &mut Vec<SnapCandidatePoint>) {
let document = snap_data.document; let document = snap_data.document;
if document.metadata().is_artboard(layer) { if document.metadata().is_artboard(layer) {

View File

@ -205,7 +205,7 @@ impl Fsm for EllipseToolFsmState {
let subpath = bezier_rs::Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE); let subpath = bezier_rs::Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE);
let manipulator_groups = subpath.manipulator_groups().to_vec(); 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); 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); shape_data.layer = Some(layer);
let fill_color = tool_options.fill.active_color(); let fill_color = tool_options.fill.active_color();

View File

@ -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::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
use crate::messages::tool::common_functionality::auto_panning::AutoPanning; 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::shape_editor::{ClosestSegment, ManipulatorAngle, ManipulatorPointInfo, OpposingHandleLengths, SelectedPointsInfo, ShapeState};
use crate::messages::tool::common_functionality::snapping::{SnapData, SnapManager}; use crate::messages::tool::common_functionality::snapping::{SnapData, SnapManager};
@ -34,19 +34,19 @@ pub enum PathToolMessage {
Delete, Delete,
DeleteAndBreakPath, DeleteAndBreakPath,
DragStop { DragStop {
shift_mirror_distance: Key, equidistant: Key,
}, },
Enter { Enter {
add_to_selection: Key, add_to_selection: Key,
}, },
Escape, Escape,
FlipSharp, FlipSmoothSharp,
GRS { GRS {
// Should be `Key::KeyG` (Grab), `Key::KeyR` (Rotate), or `Key::KeyS` (Scale) // Should be `Key::KeyG` (Grab), `Key::KeyR` (Rotate), or `Key::KeyS` (Scale)
key: Key, key: Key,
}, },
ManipulatorAngleMakeSharp, ManipulatorMakeHandlesFree,
ManipulatorAngleMakeSmooth, ManipulatorMakeHandlesColinear,
MouseDown { MouseDown {
ctrl: Key, ctrl: Key,
shift: Key, shift: Key,
@ -126,23 +126,37 @@ impl LayoutHolder for PathTool {
let related_seperator = Separator::new(SeparatorType::Related).widget_holder(); let related_seperator = Separator::new(SeparatorType::Related).widget_holder();
let unrelated_seperator = Separator::new(SeparatorType::Unrelated).widget_holder(); let unrelated_seperator = Separator::new(SeparatorType::Unrelated).widget_holder();
let manipulator_angle_options = vec![ let colinear_handles_tooltip = "Ensures both handles remain 180° apart";
RadioEntryData::new("smooth").label("Smooth").on_update(|_| PathToolMessage::ManipulatorAngleMakeSmooth.into()), let colinear_handles_state = manipulator_angle.and_then(|angle| match angle {
RadioEntryData::new("sharp").label("Sharp").on_update(|_| PathToolMessage::ManipulatorAngleMakeSharp.into()), ManipulatorAngle::Colinear => Some(true),
]; ManipulatorAngle::Free => Some(false),
let manipulator_angle_index = manipulator_angle.and_then(|angle| match angle {
ManipulatorAngle::Smooth => Some(0),
ManipulatorAngle::Sharp => Some(1),
ManipulatorAngle::Mixed => None, ManipulatorAngle::Mixed => None,
}); })
// TODO: Remove `unwrap_or_default` once checkboxes are capable of displaying a mixed state
let manipulator_angle_radio = RadioInput::new(manipulator_angle_options) .unwrap_or_default();
let colinear_handle_checkbox = CheckboxInput::new(colinear_handles_state)
.disabled(self.tool_data.selection_status.is_none()) .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(); .widget_holder();
let colinear_handles_label = TextLabel::new("Colinear Handles").tooltip(colinear_handles_tooltip).widget_holder();
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { 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<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
match self.fsm_state { match self.fsm_state {
Ready => actions!(PathToolMessageDiscriminant; Ready => actions!(PathToolMessageDiscriminant;
FlipSharp, FlipSmoothSharp,
MouseDown, MouseDown,
Delete, Delete,
NudgeSelectedPoints, NudgeSelectedPoints,
@ -177,7 +191,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
Dragging => actions!(PathToolMessageDiscriminant; Dragging => actions!(PathToolMessageDiscriminant;
Escape, Escape,
RightClick, RightClick,
FlipSharp, FlipSmoothSharp,
DragStop, DragStop,
PointerMove, PointerMove,
Delete, Delete,
@ -185,7 +199,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
DeleteAndBreakPath, DeleteAndBreakPath,
), ),
DrawingBox => actions!(PathToolMessageDiscriminant; DrawingBox => actions!(PathToolMessageDiscriminant;
FlipSharp, FlipSmoothSharp,
DragStop, DragStop,
PointerMove, PointerMove,
Delete, Delete,
@ -378,7 +392,7 @@ impl PathToolData {
// Check if the alt key has just been pressed // Check if the alt key has just been pressed
if alt && !self.alt_debounce { if alt && !self.alt_debounce {
self.opposing_handle_lengths = None; 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; self.alt_debounce = alt;
@ -545,21 +559,21 @@ impl Fsm for PathToolFsmState {
PathToolFsmState::Ready PathToolFsmState::Ready
} }
// Mouse up // Mouse up
(PathToolFsmState::DrawingBox, PathToolMessage::DragStop { shift_mirror_distance }) => { (PathToolFsmState::DrawingBox, PathToolMessage::DragStop { equidistant }) => {
let shift_pressed = input.keyboard.get(shift_mirror_distance as usize); let equidistant = input.keyboard.get(equidistant as usize);
if tool_data.drag_start_pos == tool_data.previous_mouse_position { if tool_data.drag_start_pos == tool_data.previous_mouse_position {
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] }); responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] });
} else { } 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(OverlaysMessage::Draw);
responses.add(PathToolMessage::SelectedPointUpdated); responses.add(PathToolMessage::SelectedPointUpdated);
PathToolFsmState::Ready PathToolFsmState::Ready
} }
(_, PathToolMessage::DragStop { shift_mirror_distance }) => { (_, PathToolMessage::DragStop { equidistant }) => {
let shift_pressed = input.keyboard.get(shift_mirror_distance as usize); let equidistant = input.keyboard.get(equidistant as usize);
let nearest_point = shape_editor let nearest_point = shape_editor
.find_nearest_point_indices(&document.network, &document.metadata, input.mouse.position, SELECTION_THRESHOLD) .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); 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)); let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == Some(point));
if clicked_selected { if clicked_selected {
shape_editor.deselect_all_points(); shape_editor.deselect_all_points();
@ -598,9 +612,9 @@ impl Fsm for PathToolFsmState {
shape_editor.delete_point_and_break_path(&document.network, responses); shape_editor.delete_point_and_break_path(&document.network, responses);
PathToolFsmState::Ready PathToolFsmState::Ready
} }
(_, PathToolMessage::FlipSharp) => { (_, PathToolMessage::FlipSmoothSharp) => {
if !tool_data.double_click_handled { 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); responses.add(PathToolMessage::SelectedPointUpdated);
} }
self self
@ -641,16 +655,16 @@ impl Fsm for PathToolFsmState {
tool_data.selection_status = get_selection_status(&document.network, &document.metadata, shape_editor); tool_data.selection_status = get_selection_status(&document.network, &document.metadata, shape_editor);
self self
} }
(_, PathToolMessage::ManipulatorAngleMakeSmooth) => { (_, PathToolMessage::ManipulatorMakeHandlesColinear) => {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
shape_editor.set_handle_mirroring_on_selected(true, responses); shape_editor.set_colinear_handles_state_on_selected(true, responses);
shape_editor.smooth_selected_groups(responses, &document.network); shape_editor.convert_selected_manipulators_to_colinear_handles(responses, &document.network);
responses.add(DocumentMessage::CommitTransaction); responses.add(DocumentMessage::CommitTransaction);
PathToolFsmState::Ready PathToolFsmState::Ready
} }
(_, PathToolMessage::ManipulatorAngleMakeSharp) => { (_, PathToolMessage::ManipulatorMakeHandlesFree) => {
responses.add(DocumentMessage::StartTransaction); 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); responses.add(DocumentMessage::CommitTransaction);
PathToolFsmState::Ready PathToolFsmState::Ready
} }
@ -663,6 +677,8 @@ impl Fsm for PathToolFsmState {
PathToolFsmState::Ready => HintData(vec![ 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, "Select Point"), HintInfo::keys([Key::Shift], "Extend Selection").prepend_plus()]),
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Insert Point on Segment")]), 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 // TODO: Only show the following hints if at least one point is selected
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Drag Selected")]), HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Drag Selected")]),
HintGroup(vec![HintInfo::keys([Key::KeyG, Key::KeyR, Key::KeyS], "Grab/Rotate/Scale 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![ PathToolFsmState::Dragging => HintData(vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
HintGroup(vec![ 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. Also, make the hint dynamically say "Make Colinear" or "Make Not Colinear" based on its current state. And only
// TODO: Switch this to the "S" key // 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.
// 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 Colinear Handles"),
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). And show this only when a handle is being dragged.
// TODO: Switch this to the "Alt" key (since it's equivalent to the "From Center" modifier when drawing a line) HintInfo::keys([Key::Shift], "Equidistant Handles"),
// TODO: Show this only when a handle is being dragged // TODO: Add "Snap 15°" modifier with the "Shift" key (only when a handle is being dragged).
HintInfo::keys([Key::Shift], "Equidistant Handles (Smooth Only)"), // TODO: Add "Lock Angle" modifier with the "Ctrl" key (only when a handle is being dragged).
// 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![ 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 { let Some(layer) = selection_layers.find(|(_, v)| *v > 0).map(|(k, _)| k) else {
return SelectionStatus::None; return SelectionStatus::None;
}; };
let Some(subpaths) = get_subpaths(layer, document_network) else { let Some(subpaths) = get_subpaths(layer, document_network) else {
return SelectionStatus::None; 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; return SelectionStatus::None;
}; };
let Some(point) = shape_state.selected_points().next() else { let Some(point) = shape_state.selected_points().next() else {
return SelectionStatus::None; return SelectionStatus::None;
}; };
let Some(manipulator) = get_manipulator_from_id(subpaths, point.group) else {
let Some(group) = get_manipulator_from_id(subpaths, point.group) else {
return SelectionStatus::None; 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; 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 { return SelectionStatus::One(SingleSelectedPoint {
coordinates: document_metadata.transform_to_document(layer).transform_point2(local_position), coordinates,
layer, layer,
id: *point, id: *point,
manipulator_angle, manipulator_angle,

View File

@ -200,7 +200,7 @@ struct PenToolData {
layer: Option<LayerNodeIdentifier>, layer: Option<LayerNodeIdentifier>,
subpath_index: usize, subpath_index: usize,
snap_manager: SnapManager, 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 // Indicates that curve extension is occurring from the first point, rather than (more commonly) the last point
from_start: bool, from_start: bool,
angle: f64, angle: f64,
@ -212,21 +212,16 @@ impl PenToolData {
self.from_start = from_start; self.from_start = from_start;
self.subpath_index = subpath_index; 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 manipulator_groups = subpaths[subpath_index].manipulator_groups();
let Some(last_handle) = (if from_start { manipulator_groups.first() } else { manipulator_groups.last() }) else { let first_or_last = if from_start { manipulator_groups.first() } else { manipulator_groups.last() };
return; 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 { responses.add(GraphOperationMessage::Vector {
layer, layer,
modification: VectorDataModification::SetManipulatorHandleMirroring { modification: VectorDataModification::SetManipulatorColinearHandlesState { id, colinear: false },
id: last_handle.id,
mirror_angle: false,
},
}); });
} }
@ -270,53 +265,56 @@ impl PenToolData {
self.subpath_index = 0; self.subpath_index = 0;
} }
// TODO: tooltip / user documentation? /// If the user places the anchor on top of the previous anchor, it becomes sharp and the outgoing handle may be dragged.
/// If you place the anchor on top of the previous anchor then you break the mirror fn bend_from_previous_point(&mut self, document: &DocumentMessageHandler, transform: DAffine2, responses: &mut VecDeque<Message>) {
fn check_break(&mut self, document: &DocumentMessageHandler, transform: DAffine2, responses: &mut VecDeque<Message>) -> Option<()> { (|| -> Option<()> {
// Get subpath // Get subpath
let layer = self.layer?; let layer = self.layer?;
let subpath = &get_subpaths(layer, &document.network)?[self.subpath_index]; let subpath = &get_subpaths(layer, &document.network)?[self.subpath_index];
// Get the last manipulator group and the one previous to that // Get the last manipulator group and the one previous to that
let mut manipulator_groups = subpath.manipulator_groups().iter(); 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 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()? }; let previous_manipulator_group = if self.from_start { manipulator_groups.next()? } else { manipulator_groups.next_back()? };
// Get correct handle types // Get correct handle types
let outwards_handle = if self.from_start { SelectedType::InHandle } else { SelectedType::OutHandle }; let outwards_handle = if self.from_start { SelectedType::InHandle } else { SelectedType::OutHandle };
// Get manipulator points // Get manipulator points
let last_anchor = last_manipulator_group.anchor; let last_anchor = last_manipulator_group.anchor;
let previous_anchor = previous_manipulator_group.anchor; let previous_anchor = previous_manipulator_group.anchor;
// Break the control // Break the control
let transform = document.metadata.document_to_viewport * transform; 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); 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 { if !on_top {
return None; return None;
} }
// Remove the point that has just been placed
responses.add(GraphOperationMessage::Vector {
layer,
modification: VectorDataModification::RemoveManipulatorGroup { id: last_manipulator_group.id },
});
// Move the in handle of the previous anchor to on top of the previous position // Remove the point that has just been placed
let point = ManipulatorPointId::new(previous_manipulator_group.id, outwards_handle); responses.add(GraphOperationMessage::Vector {
responses.add(GraphOperationMessage::Vector { layer,
layer, modification: VectorDataModification::RemoveManipulatorGroup { id: last_manipulator_group.id },
modification: VectorDataModification::SetManipulatorPosition { point, position: previous_anchor }, });
});
// Stop the handles on the last point from mirroring // Move the in handle of the previous anchor to on top of the previous position
let id = previous_manipulator_group.id; let point = ManipulatorPointId::new(previous_manipulator_group.id, outwards_handle);
responses.add(GraphOperationMessage::Vector { responses.add(GraphOperationMessage::Vector {
layer, layer,
modification: VectorDataModification::SetManipulatorHandleMirroring { id, mirror_angle: false }, modification: VectorDataModification::SetManipulatorPosition { point, position: previous_anchor },
}); });
self.should_mirror = false; // Stop the handles on the last point from being colinear
None 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<Message>) -> Option<PenToolFsmState> { fn finish_placing_handle(&mut self, document: &DocumentMessageHandler, transform: DAffine2, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> {
@ -357,11 +355,11 @@ impl PenToolData {
modification: VectorDataModification::SetManipulatorPosition { point, position: last_in }, 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; let id = first_manipulator_group.id;
responses.add(GraphOperationMessage::Vector { responses.add(GraphOperationMessage::Vector {
layer, layer,
modification: VectorDataModification::SetManipulatorHandleMirroring { id, mirror_angle: false }, modification: VectorDataModification::SetManipulatorColinearHandlesState { id, colinear: false },
}); });
// Remove the point that has just been placed // Remove the point that has just been placed
@ -409,11 +407,11 @@ impl PenToolData {
// Get manipulator points // Get manipulator points
let last_anchor = last_manipulator_group.anchor; 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)]; 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); let position = self.compute_snapped_angle(snap_data, transform, modifiers.lock_angle, modifiers.snap_angle, colinear, mouse, Some(last_anchor), false);
if !pos.is_finite() { if !position.is_finite() {
return Some(PenToolFsmState::DraggingHandle); return Some(PenToolFsmState::DraggingHandle);
} }
@ -421,25 +419,25 @@ impl PenToolData {
let point = ManipulatorPointId::new(last_manipulator_group.id, outwards_handle); let point = ManipulatorPointId::new(last_manipulator_group.id, outwards_handle);
responses.add(GraphOperationMessage::Vector { responses.add(GraphOperationMessage::Vector {
layer: self.layer?, layer: self.layer?,
modification: VectorDataModification::SetManipulatorPosition { point, position: pos }, modification: VectorDataModification::SetManipulatorPosition { point, position },
}); });
// Mirror handle of last segment // Place the previous anchor's in handle at the opposing position
if should_mirror { if colinear {
// Could also be written as `last_anchor.position * 2 - pos` but this way avoids overflow/underflow better // 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); let point = ManipulatorPointId::new(last_manipulator_group.id, inwards_handle);
responses.add(GraphOperationMessage::Vector { responses.add(GraphOperationMessage::Vector {
layer: self.layer?, 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; let id = last_manipulator_group.id;
responses.add(GraphOperationMessage::Vector { responses.add(GraphOperationMessage::Vector {
layer: self.layer?, layer: self.layer?,
modification: VectorDataModification::SetManipulatorHandleMirroring { id, mirror_angle: should_mirror }, modification: VectorDataModification::SetManipulatorColinearHandlesState { id, colinear },
}); });
Some(PenToolFsmState::DraggingHandle) Some(PenToolFsmState::DraggingHandle)
@ -487,7 +485,7 @@ impl PenToolData {
} }
/// Snap the angle of the line from relative to position if the key is pressed. /// 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<DVec2>, 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<DVec2>, neighbor: bool) -> DVec2 {
let document = snap_data.document; let document = snap_data.document;
let mut document_pos = document.metadata.document_to_viewport.inverse().transform_point2(mouse); let mut document_pos = document.metadata.document_to_viewport.inverse().transform_point2(mouse);
let snap = &mut self.snap_manager; let snap = &mut self.snap_manager;
@ -509,7 +507,7 @@ impl PenToolData {
}; };
let near_point = SnapCandidatePoint::handle_neighbors(document_pos, neighbors.clone()); let near_point = SnapCandidatePoint::handle_neighbors(document_pos, neighbors.clone());
let far_point = SnapCandidatePoint::handle_neighbors(2. * relative - document_pos, neighbors); 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 = snap.constrained_snap(&snap_data, &near_point, constraint, None);
let snapped_far = snap.constrained_snap(&snap_data, &far_point, constraint, None); let snapped_far = snap.constrained_snap(&snap_data, &far_point, constraint, None);
document_pos = if snapped_far.other_snap_better(&snapped) { document_pos = if snapped_far.other_snap_better(&snapped) {
@ -523,7 +521,7 @@ impl PenToolData {
document_pos = snapped.snapped_point_document; document_pos = snapped.snapped_point_document;
snap.update_indicator(snapped); 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 = 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); 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) { document_pos = if snapped_far.other_snap_better(&snapped) {
@ -641,8 +639,8 @@ impl Fsm for PenToolFsmState {
(PenToolFsmState::Ready, PenToolMessage::DragStart) => { (PenToolFsmState::Ready, PenToolMessage::DragStart) => {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
// Disable this tool's mirroring // Prevent the initial point from having a colinear in handle while dragging the out handle
tool_data.should_mirror = false; tool_data.colinear_handles = false;
// Perform extension of an existing path // 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) { 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) => { (PenToolFsmState::PlacingAnchor, PenToolMessage::DragStart) => {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
tool_data.check_break(document, transform, responses); tool_data.bend_from_previous_point(document, transform, responses);
PenToolFsmState::DraggingHandle PenToolFsmState::DraggingHandle
} }
(PenToolFsmState::DraggingHandle, PenToolMessage::DragStop) => { (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) tool_data.finish_placing_handle(document, transform, responses).unwrap_or(PenToolFsmState::PlacingAnchor)
} }
(PenToolFsmState::DraggingHandle, PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }) => { (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), break_handle: input.keyboard.key(break_handle),
}; };
let snap_data = SnapData::new(document, input); let snap_data = SnapData::new(document, input);
let state = tool_data let state = tool_data
.drag_handle(snap_data, transform, input.mouse.position, modifiers, responses) .drag_handle(snap_data, transform, input.mouse.position, modifiers, responses)
.unwrap_or(PenToolFsmState::Ready); .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::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, "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![ PenToolFsmState::DraggingHandle => HintData(vec![
HintGroup(vec![ HintGroup(vec![
@ -780,6 +783,7 @@ impl Fsm for PenToolFsmState {
HintInfo::keys([Key::Enter], "End Path").prepend_slash(), HintInfo::keys([Key::Enter], "End Path").prepend_slash(),
]), ]),
HintGroup(vec![HintInfo::keys([Key::Shift], "Snap 15°"), HintInfo::keys([Key::Control], "Lock Angle")]), 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")]), HintGroup(vec![HintInfo::keys([Key::Alt], "Bend Handle")]),
]), ]),
}; };

View File

@ -343,7 +343,7 @@ fn update_spline(tool_data: &SplineToolData, show_preview: bool, responses: &mut
return; 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 subpaths = vec![subpath];
let modification = VectorDataModification::UpdateSubpaths { subpaths }; let modification = VectorDataModification::UpdateSubpaths { subpaths };
responses.add_front(GraphOperationMessage::Vector { layer, modification }); responses.add_front(GraphOperationMessage::Vector { layer, modification });

View File

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path class="bright" d="M6,1C4.3,1,3,2.3,3,4v4h5V1H6z M7,6v1H4c0,0,0-0.6,0.2-1c0.3-0.6,0.8-1.1,1.3-1.4c1.1-0.9,0.8-1.9,0.1-1.7C5,3,4.8,3.6,4.9,4.1H4C3.9,2.9,4.7,2,5.8,2c1.1,0,1.5,1.2,1,2.3C6.4,5.1,5.5,5.5,5.4,6H7z" /> <path class="bright" d="M4.5,3.8c-0.2-0.7,0-1.4,1-1.6C6.7,2,7.2,3.4,5.4,4.6c-0.8,0.5-1.7,1.2-2.1,2C3,7.2,3,8,3,8h5V6C7.5,6.6,5.4,6.6,5.4,6.6C5.5,5.8,7,5.4,7.6,4.3C8.5,2.7,7.7,1,6,1C4.2,1,2.5,2,3.2,4.6L4.5,3.8z" />
<path class="dim" d="M10,1H9v1h1c1.1,0,2,0.9,2,2v6c0,2.21-1.79,4-4,4s-4-1.79-4-4V9H3v1c0,2.76,2.24,5,5,5s5-2.24,5-5V4C13,2.35,11.65,1,10,1z" /> <path class="dim" d="M10,1H9v1h1c1.1,0,2,0.9,2,2v6c0,2.21-1.79,4-4,4s-4-1.79-4-4V9H3v1c0,2.76,2.24,5,5,5s5-2.24,5-5V4C13,2.35,11.65,1,10,1z" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 432 B

After

Width:  |  Height:  |  Size: 428 B

View File

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path class="bright" d="M10,1H8v7h5V4C13,2.3,11.7,1,10,1z M12,7H9.1c0,0,0-0.6,0.2-1c0.2-0.6,0.8-1.1,1.3-1.5c0.9-0.8,0.4-1.8-0.4-1.6C9.6,3.1,9.9,4.1,9.9,4.1H9.1c-0.3-1,0.1-2.2,1.3-2.1c1,0.1,1.6,1.1,1.4,2.1C11.6,5,10.6,5.4,10.5,6H12V7z" /> <path class="bright" d="M10.4,6.6c0.1-0.8,2.1-1.2,2.4-2.4C13.3,2.8,12,1,10.3,1c-2.2,0-2.8,2.2-2,3.8l1.1-0.9c0,0-0.6-1.3,0.5-1.7c1-0.4,2.5,1.2,0.7,2.2C9.9,5,8.7,5.7,8.3,6.6C8,7.2,8,8,8,8h5V6C12.5,6.6,10.4,6.6,10.4,6.6z" />
<path class="dim" d="M6,1h1v1H6C4.9,2,4,2.9,4,4v6c0,2.21,1.79,4,4,4s4-1.79,4-4V9h1v1c0,2.76-2.24,5-5,5s-5-2.24-5-5V4C3,2.35,4.35,1,6,1z" /> <path class="dim" d="M6,1h1v1H6C4.9,2,4,2.9,4,4v6c0,2.21,1.79,4,4,4s4-1.79,4-4V9h1v1c0,2.76-2.24,5-5,5s-5-2.24-5-5V4C3,2.35,4.35,1,6,1z" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 448 B

After

Width:  |  Height:  |  Size: 432 B

View File

@ -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 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 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 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 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 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"; 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 }, MouseHintMmbDrag: { svg: MouseHintMmbDrag, size: 16 },
MouseHintNone: { svg: MouseHintNone, size: 16 }, MouseHintNone: { svg: MouseHintNone, size: 16 },
MouseHintRmb: { svg: MouseHintRmb, size: 16 }, MouseHintRmb: { svg: MouseHintRmb, size: 16 },
MouseHintRmbDouble: { svg: MouseHintRmbDouble, size: 16 },
MouseHintRmbDrag: { svg: MouseHintRmbDrag, size: 16 }, MouseHintRmbDrag: { svg: MouseHintRmbDrag, size: 16 },
MouseHintScrollDown: { svg: MouseHintScrollDown, size: 16 }, MouseHintScrollDown: { svg: MouseHintScrollDown, size: 16 },
MouseHintScrollUp: { svg: MouseHintScrollUp, size: 16 }, MouseHintScrollUp: { svg: MouseHintScrollUp, size: 16 },

View File

@ -196,7 +196,7 @@ mod tests {
} }
#[test] #[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 // This will do nothing to the subpath
let mut subpath = set_up_open_subpath(); let mut subpath = set_up_open_subpath();
let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.75)); let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.75));

View File

@ -98,14 +98,14 @@ fn spline_generator(_input: (), positions: Vec<DVec2>) -> 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 // 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)] #[derive(Debug, Clone)]
pub struct PathGenerator<Mirror> { pub struct PathGenerator<ColinearManipulators> {
mirror: Mirror, colinear_manipulators: ColinearManipulators,
} }
#[node_macro::node_fn(PathGenerator)] #[node_macro::node_fn(PathGenerator)]
fn generate_path(path_data: Vec<Subpath<ManipulatorGroupId>>, mirror: Vec<ManipulatorGroupId>) -> super::VectorData { fn generate_path(path_data: Vec<Subpath<ManipulatorGroupId>>, colinear_manipulators: Vec<ManipulatorGroupId>) -> super::VectorData {
let mut vector_data = super::VectorData::from_subpaths(path_data); let mut vector_data = super::VectorData::from_subpaths(path_data);
vector_data.mirror_angle = mirror; vector_data.colinear_manipulators = colinear_manipulators;
vector_data vector_data
} }

View File

@ -18,9 +18,9 @@ pub struct VectorData {
pub transform: DAffine2, pub transform: DAffine2,
pub style: PathStyle, pub style: PathStyle,
pub alpha_blending: AlphaBlending, 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) /// 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 subpath and mirror angle inputs"` to find it). /// 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 mirror_angle: Vec<ManipulatorGroupId>, pub colinear_manipulators: Vec<ManipulatorGroupId>,
pub point_domain: PointDomain, pub point_domain: PointDomain,
pub segment_domain: SegmentDomain, 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.transform.to_cols_array().iter().for_each(|x| x.to_bits().hash(state));
self.style.hash(state); self.style.hash(state);
self.alpha_blending.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, transform: DAffine2::IDENTITY,
style: PathStyle::new(Some(Stroke::new(Some(Color::BLACK), 0.)), super::style::Fill::None), style: PathStyle::new(Some(Stroke::new(Some(Color::BLACK), 0.)), super::style::Fill::None),
alpha_blending: AlphaBlending::new(), alpha_blending: AlphaBlending::new(),
mirror_angle: Vec::new(), colinear_manipulators: Vec::new(),
point_domain: PointDomain::new(), point_domain: PointDomain::new(),
segment_domain: SegmentDomain::new(), segment_domain: SegmentDomain::new(),
region_domain: RegionDomain::new(), region_domain: RegionDomain::new(),

View File

@ -364,7 +364,7 @@ impl crate::vector::ConcatElement for super::VectorData {
self.region_domain.concat(&other.region_domain, transform * other.transform, &id_map); self.region_domain.concat(&other.region_domain, transform * other.transform, &id_map);
// TODO: properly deal with fills such as gradients // TODO: properly deal with fills such as gradients
self.style = other.style.clone(); 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; self.alpha_blending = other.alpha_blending;
} }
} }