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