Add nondestructive vector editing (#1676)

* Initial vector modify node

* Initial extraction of data from monitor nodes

* Migrate to point id

* Start converting to modify node

* Non destructive spline tool (tout le reste est cassé)

* Fix unconnected modify node

* Fix freehand tool

* Pen tool

* Migrate demo art

* Select points

* Fix the demo artwork

* Fix the X and Y inputs for path tool

* G1 continous toggle

* Delete points

* Fix test

* Insert point

* Improve robustness of handles

* Fix GRS shortcuts on path

* Dragging points

* Fix build

* Preserve opposing handle lengths

* Update demo art and snapping

* Fix polygon tool

* Double click end anchor

* Improve dragging

* Fix text shifting

* Select only connected verts

* Colinear alt

* Cleanup

* Fix imports

* Improve pen tool avoiding handle placement

* Improve disolve

* Remove pivot widget from Transform node properties

* Fix demo art

* Fix bugs

* Re-save demo artwork

* Code review

* Serialize hashmap as tuple vec to enable deserialize_inputs

* Fix migrate

* Add document upgrade function to editor_api.rs

* Finalize document upgrading

* Rename to the Path node

* Remove smoothing from Freehand tool

* Upgrade demo artwork

* Propertly disable raw-rs tests

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
Co-authored-by: Adam <adamgerhant@gmail.com>
Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
James Lindsay
2024-07-05 13:42:40 -07:00
committed by GitHub
co-authored by Keavon Chambers Adam Dennis Kobert
parent fd3613018a
commit 1652c713a6
96 changed files with 3343 additions and 2622 deletions
@@ -1,5 +1,6 @@
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::prelude::Message;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::prelude::*;
use graphene_core::Color;
use graphene_std::vector::style::FillChoice;
@@ -58,6 +59,20 @@ impl ToolColorOptions {
}
}
pub fn apply_fill(&self, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
if let Some(color) = self.active_color() {
let fill = graphene_core::vector::style::Fill::solid(color);
responses.add(GraphOperationMessage::FillSet { layer, fill });
}
}
pub fn apply_stroke(&self, weight: f64, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
if let Some(color) = self.active_color() {
let stroke = graphene_core::vector::style::Stroke::new(Some(color), weight);
responses.add(GraphOperationMessage::StrokeSet { layer, stroke });
}
}
pub fn create_widgets(
&self,
label_text: impl Into<String>,
@@ -1,20 +1,18 @@
use crate::messages::portfolio::document::graph_operation::utility_types::VectorDataModification;
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
use crate::messages::prelude::*;
use bezier_rs::{ManipulatorGroup, Subpath};
use bezier_rs::Subpath;
use graph_craft::document::{value::TaggedValue, DocumentNode, NodeId, NodeInput, NodeNetwork};
use graphene_core::raster::{BlendMode, ImageFrame};
use graphene_core::text::Font;
use graphene_core::uuid::ManipulatorGroupId;
use graphene_core::vector::style::Gradient;
use graphene_core::vector::PointId;
use graphene_core::Color;
use glam::DVec2;
use std::collections::VecDeque;
/// Create a new vector layer from a vector of [`bezier_rs::Subpath`].
pub fn new_vector_layer(subpaths: Vec<Subpath<ManipulatorGroupId>>, id: NodeId, parent: LayerNodeIdentifier, responses: &mut VecDeque<Message>) -> LayerNodeIdentifier {
pub fn new_vector_layer(subpaths: Vec<Subpath<PointId>>, id: NodeId, parent: LayerNodeIdentifier, responses: &mut VecDeque<Message>) -> LayerNodeIdentifier {
let insert_index = -1;
responses.add(GraphOperationMessage::NewVectorLayer { id, subpaths, parent, insert_index });
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![id] });
@@ -46,25 +44,16 @@ 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 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::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>>> {
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
}
pub fn new_custom(id: NodeId, nodes: HashMap<NodeId, DocumentNode>, parent: LayerNodeIdentifier, responses: &mut VecDeque<Message>) -> LayerNodeIdentifier {
responses.add(GraphOperationMessage::NewCustomLayer {
id,
nodes,
parent,
insert_index: -1,
alias: String::new(),
});
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![id] });
LayerNodeIdentifier::new_unchecked(id)
}
/// Locate the final pivot from the transform (TODO: decide how the pivot should actually work)
@@ -83,16 +72,6 @@ pub fn get_viewport_pivot(layer: LayerNodeIdentifier, document_network: &NodeNet
document_metadata.transform_to_viewport(layer).transform_point2(min + (max - min) * pivot)
}
/// 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
}
}
/// Get the current gradient of a layer from the closest Fill node
pub fn get_gradient(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<Gradient> {
let fill_index = 1;
@@ -191,16 +170,6 @@ pub fn is_layer_fed_by_node_of_name(layer: LayerNodeIdentifier, document_network
NodeGraphLayer::new(layer, document_network).find_node_inputs(node_name).is_some()
}
/// Convert subpaths to an iterator of manipulator groups
pub fn get_manipulator_groups(subpaths: &[Subpath<ManipulatorGroupId>]) -> impl Iterator<Item = &bezier_rs::ManipulatorGroup<ManipulatorGroupId>> + DoubleEndedIterator {
subpaths.iter().flat_map(|subpath| subpath.manipulator_groups())
}
/// Find a manipulator group with a specific id from several subpaths
pub fn get_manipulator_from_id(subpaths: &[Subpath<ManipulatorGroupId>], id: ManipulatorGroupId) -> Option<&bezier_rs::ManipulatorGroup<ManipulatorGroupId>> {
subpaths.iter().find_map(|subpath| subpath.manipulator_from_id(id))
}
/// An immutable reference to a layer within the document node graph for easy access.
pub struct NodeGraphLayer<'a> {
node_graph: &'a NodeNetwork,
@@ -29,13 +29,12 @@ impl Resize {
root_transform.transform_point2(self.drag_start)
}
pub fn calculate_transform(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key, skip_rerender: bool) -> Option<Message> {
let Some(layer) = self.layer else {
return None;
};
pub fn calculate_points(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key) -> Option<[DVec2; 2]> {
let layer = self.layer?;
if layer == LayerNodeIdentifier::ROOT_PARENT {
log::error!("Resize layer cannot be ROOT_PARENT");
return None;
}
if !document.network().nodes.contains_key(&layer.to_node()) {
@@ -87,9 +86,14 @@ impl Resize {
self.snap_manager.update_indicator(snapped);
}
Some(points_viewport)
}
pub fn calculate_transform(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key, skip_rerender: bool) -> Option<Message> {
let points_viewport = self.calculate_points(document, input, center, lock_ratio)?;
Some(
GraphOperationMessage::TransformSet {
layer,
layer: self.layer?,
transform: DAffine2::from_scale_angle_translation(points_viewport[1] - points_viewport[0], 0., points_viewport[0]),
transform_in: TransformIn::Viewport,
skip_rerender,
File diff suppressed because it is too large Load Diff
@@ -1,17 +1,20 @@
mod grid_snapper;
mod layer_snapper;
mod snap_results;
pub use {grid_snapper::*, layer_snapper::*, snap_results::*};
use crate::consts::COLOR_OVERLAY_BLUE;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::misc::{BoundingBoxSnapTarget, GeometrySnapTarget, GridSnapTarget, SnapTarget};
use crate::messages::prelude::*;
use bezier_rs::{Subpath, TValue};
use glam::{DAffine2, DVec2};
use graphene_core::renderer::Quad;
use graphene_core::uuid::ManipulatorGroupId;
use graphene_core::vector::PointId;
use glam::{DAffine2, DVec2};
use std::cmp::Ordering;
pub use {grid_snapper::*, layer_snapper::*, snap_results::*};
/// Handles snapping and snap overlays
#[derive(Debug, Clone, Default)]
@@ -150,7 +153,7 @@ pub struct SnapData<'a> {
pub document: &'a DocumentMessageHandler,
pub input: &'a InputPreprocessorMessageHandler,
pub ignore: &'a [LayerNodeIdentifier],
pub manipulators: Vec<(LayerNodeIdentifier, ManipulatorGroupId)>,
pub manipulators: Vec<(LayerNodeIdentifier, PointId)>,
pub candidates: Option<&'a Vec<LayerNodeIdentifier>>,
}
impl<'a> SnapData<'a> {
@@ -172,7 +175,7 @@ impl<'a> SnapData<'a> {
fn ignore_bounds(&self, layer: LayerNodeIdentifier) -> bool {
self.manipulators.iter().any(|&(ignore, _)| ignore == layer)
}
fn ignore_manipulator(&self, layer: LayerNodeIdentifier, manipulator: impl Into<ManipulatorGroupId>) -> bool {
fn ignore_manipulator(&self, layer: LayerNodeIdentifier, manipulator: impl Into<PointId>) -> bool {
self.manipulators.contains(&(layer, manipulator.into()))
}
}
@@ -277,10 +280,8 @@ impl SnapManager {
}
}
if let Some(root) = snap_data.document.network.get_root_node() {
if snap_data.document.network.nodes.get(&root.id).expect("Root should always be a node in find_candidates").is_layer {
add_candidates(LayerNodeIdentifier::new(root.id, &snap_data.document.network), snap_data, quad, &mut candidates);
}
for layer in LayerNodeIdentifier::ROOT_PARENT.children(&document.metadata) {
add_candidates(layer, snap_data, quad, &mut candidates);
}
if candidates.len() > 10 {
@@ -333,7 +334,7 @@ impl SnapManager {
if let Some(ind) = &self.indicator {
for curve in &ind.curves {
let Some(curve) = curve else { continue };
overlay_context.outline::<ManipulatorGroupId>([Subpath::from_bezier(curve)].iter(), to_viewport);
overlay_context.outline([Subpath::from_bezier(curve)].iter(), to_viewport);
}
if let Some(quad) = ind.target_bounds {
overlay_context.quad(to_viewport * quad);
@@ -8,8 +8,7 @@ use crate::messages::prelude::*;
use bezier_rs::{Bezier, Identifier, Subpath, TValue};
use glam::{DAffine2, DVec2};
use graphene_core::renderer::Quad;
use graphene_core::uuid::ManipulatorGroupId;
use graphene_std::vector::PointId;
use graphene_core::vector::PointId;
#[derive(Clone, Debug, Default)]
pub struct LayerSnapper {
@@ -33,7 +32,7 @@ impl LayerSnapper {
self.paths_to_snap.push(SnapCandidatePath {
document_curve,
layer,
start: ManipulatorGroupId::new(),
start: PointId::new(),
target,
bounds: Some(bounds),
});
@@ -133,7 +132,7 @@ impl LayerSnapper {
let direction = constraint.direction().normalize_or_zero();
let start = constrained_point - tolerance * direction;
let end = constrained_point + tolerance * direction;
Subpath::<ManipulatorGroupId>::new_line(start, end)
Subpath::<PointId>::new_line(start, end)
};
for path in &self.paths_to_snap {
@@ -296,7 +295,7 @@ fn normals_and_tangents(path: &SnapCandidatePath, normals: bool, tangents: bool,
struct SnapCandidatePath {
document_curve: Bezier,
layer: LayerNodeIdentifier,
start: ManipulatorGroupId,
start: PointId,
target: SnapTarget,
bounds: Option<Quad>,
}
@@ -418,8 +417,7 @@ fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath<Poin
}
}
/// 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 {
pub fn are_manipulator_handles_colinear(group: &bezier_rs::ManipulatorGroup<PointId>, to_document: DAffine2, subpath: &Subpath<PointId>, 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));
@@ -3,7 +3,7 @@ use crate::messages::portfolio::document::utility_types::misc::{SnapSource, Snap
use bezier_rs::Bezier;
use glam::DVec2;
use graphene_core::renderer::Quad;
use graphene_core::uuid::ManipulatorGroupId;
use graphene_core::vector::PointId;
#[derive(Clone, Debug, Default)]
pub struct SnapResults {
@@ -79,7 +79,7 @@ pub struct SnappedLine {
#[derive(Clone, Debug)]
pub struct SnappedCurve {
pub layer: LayerNodeIdentifier,
pub start: ManipulatorGroupId,
pub start: PointId,
pub point: SnappedPoint,
pub document_curve: Bezier,
}
@@ -126,8 +126,8 @@ impl SelectedEdges {
for point in points {
let old_position = point.document_point;
let bounds_space = bounds_to_doc.inverse().transform_point2(point.document_point);
let normalised = (bounds_space - self.bounds[0]) / (self.bounds[1] - self.bounds[0]);
let updated = normalised * (max - min) + min;
let normalized = (bounds_space - self.bounds[0]) / (self.bounds[1] - self.bounds[0]);
let updated = normalized * (max - min) + min;
point.document_point = bounds_to_doc.transform_point2(updated);
let mut snapped = if constrain {
let constraint = SnapConstraint::Line {
@@ -1,31 +1,27 @@
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::get_subpaths;
use graphene_std::vector::PointId;
use glam::DVec2;
/// Determines if a path should be extended. Returns the path and if it is extending from the start, if applicable.
pub fn should_extend(document: &DocumentMessageHandler, pos: DVec2, tolerance: f64) -> Option<(LayerNodeIdentifier, usize, bool)> {
pub fn should_extend(document: &DocumentMessageHandler, goal: DVec2, tolerance: f64) -> Option<(LayerNodeIdentifier, PointId, DVec2)> {
let mut best = None;
let mut best_distance_squared = tolerance * tolerance;
for layer in document.selected_nodes.selected_layers(document.metadata()) {
let viewspace = document.metadata().transform_to_viewport(layer);
let subpaths = get_subpaths(layer, &document.network)?;
for (subpath_index, subpath) in subpaths.iter().enumerate() {
if subpath.closed() {
continue;
}
let vector_data = document.metadata.compute_modified_vector(layer, document.network())?;
for id in vector_data.single_connected_points() {
let Some(point) = vector_data.point_domain.position_from_id(id) else { continue };
for (manipulator_group, from_start) in [(subpath.manipulator_groups().first(), true), (subpath.manipulator_groups().last(), false)] {
let Some(manipulator_group) = manipulator_group else { break };
let distance_squared = viewspace.transform_point2(point).distance_squared(goal);
let distance_squared = viewspace.transform_point2(manipulator_group.anchor).distance_squared(pos);
if distance_squared < best_distance_squared {
best = Some((layer, subpath_index, from_start));
best_distance_squared = distance_squared;
}
if distance_squared < best_distance_squared {
best = Some((layer, id, point));
best_distance_squared = distance_squared;
}
}
}
@@ -1,5 +1,6 @@
use super::tool_prelude::*;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_types::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
@@ -7,9 +8,8 @@ use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::resize::Resize;
use crate::messages::tool::common_functionality::snapping::SnapData;
use graph_craft::document::NodeId;
use graph_craft::document::{value::TaggedValue, NodeId, NodeInput};
use graphene_core::uuid::generate_uuid;
use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::Color;
#[derive(Default)]
@@ -201,10 +201,18 @@ impl Fsm for EllipseToolFsmState {
responses.add(DocumentMessage::StartTransaction);
// Create a new ellipse vector shape
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(true), responses);
graph_modification_utils::set_manipulator_colinear_handles_state(&manipulator_groups, layer, true, responses);
let nodes = {
let node_type = resolve_document_node_type("Ellipse").expect("Ellipse node does not exist");
let node = node_type.to_document_node_default_inputs(
[None, Some(NodeInput::value(TaggedValue::F64(0.5), false)), Some(NodeInput::value(TaggedValue::F64(0.5), false))],
Default::default(),
);
HashMap::from([(NodeId(0), node)])
};
let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, document.new_layer_parent(true), responses);
tool_options.fill.apply_fill(layer, responses);
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
shape_data.layer = Some(layer);
responses.add(GraphOperationMessage::TransformSet {
@@ -214,22 +222,18 @@ impl Fsm for EllipseToolFsmState {
skip_rerender: false,
});
let fill_color = tool_options.fill.active_color();
responses.add(GraphOperationMessage::FillSet {
layer,
fill: if let Some(color) = fill_color { Fill::Solid(color) } else { Fill::None },
});
responses.add(GraphOperationMessage::StrokeSet {
layer,
stroke: Stroke::new(tool_options.stroke.active_color(), tool_options.line_weight),
});
EllipseToolFsmState::Drawing
}
(EllipseToolFsmState::Drawing, EllipseToolMessage::PointerMove { center, lock_ratio }) => {
if let Some(message) = shape_data.calculate_transform(document, input, center, lock_ratio, false) {
responses.add(message);
if let Some([start, end]) = shape_data.calculate_points(document, input, center, lock_ratio) {
if let Some(layer) = shape_data.layer {
responses.add(GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_scale_angle_translation(end - start, 0., (start + end) / 2.),
transform_in: TransformIn::Viewport,
skip_rerender: false,
});
}
}
// Auto-panning
@@ -1,5 +1,5 @@
use super::tool_prelude::*;
use crate::messages::portfolio::document::graph_operation::utility_types::VectorDataModification;
use crate::messages::portfolio::document::node_graph::document_node_types::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_functions::path_endpoint_overlays;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
@@ -9,10 +9,10 @@ use crate::messages::tool::common_functionality::utility_functions::should_exten
use graph_craft::document::NodeId;
use graphene_core::uuid::generate_uuid;
use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::vector::VectorModificationType;
use graphene_core::Color;
use graphene_std::vector::{PointId, SegmentId};
use bezier_rs::ManipulatorGroup;
use glam::DVec2;
#[derive(Default)]
@@ -176,7 +176,7 @@ impl ToolTransition for FreehandTool {
#[derive(Clone, Debug, Default)]
struct FreehandToolData {
extend_from_start: bool,
last_point: DVec2,
end_point: Option<(DVec2, PointId)>,
dragged: bool,
weight: f64,
layer: Option<LayerNodeIdentifier>,
@@ -211,64 +211,44 @@ impl Fsm for FreehandToolFsmState {
tool_data.extend_from_start = false;
tool_data.weight = tool_options.line_weight;
if let Some((layer, subpath_index, from_start)) = should_extend(document, input.mouse.position, crate::consts::SNAP_POINT_TOLERANCE) {
let transform = document.metadata().transform_to_viewport(layer);
let pos = transform.inverse().transform_point2(input.mouse.position);
let manipulator_group = ManipulatorGroup::new_anchor(pos);
let modification = if from_start {
tool_data.extend_from_start = true;
VectorDataModification::AddStartManipulatorGroup { subpath_index, manipulator_group }
} else {
VectorDataModification::AddEndManipulatorGroup { subpath_index, manipulator_group }
};
tool_data.dragged = true;
tool_data.last_point = pos;
// Extend an endpoint of the selected path
if let Some((layer, _, position)) = should_extend(document, input.mouse.position, crate::consts::SNAP_POINT_TOLERANCE) {
tool_data.layer = Some(layer);
responses.add(GraphOperationMessage::Vector { layer, modification });
} else {
responses.add(DocumentMessage::DeselectAllLayers);
extend_path_with_next_segment(tool_data, position, responses);
let parent = document.new_layer_parent(true);
let transform = document.metadata().transform_to_viewport(parent);
let pos = transform.inverse().transform_point2(input.mouse.position);
let subpath = bezier_rs::Subpath::from_anchors([pos], false);
let layer = graph_modification_utils::new_vector_layer(vec![subpath], NodeId(generate_uuid()), parent, responses);
tool_data.last_point = pos;
tool_data.layer = Some(layer);
responses.add(GraphOperationMessage::FillSet {
layer,
fill: if let Some(color) = tool_options.fill.active_color() { Fill::Solid(color) } else { Fill::None },
});
responses.add(GraphOperationMessage::StrokeSet {
layer,
stroke: Stroke::new(tool_options.stroke.active_color(), tool_data.weight),
});
return FreehandToolFsmState::Drawing;
}
responses.add(DocumentMessage::DeselectAllLayers);
let parent = document.new_layer_parent(true);
let nodes = {
let node_type = resolve_document_node_type("Path").expect("Path node does not exist");
let node = node_type.to_document_node_default_inputs([], Default::default());
HashMap::from([(NodeId(0), node)])
};
let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, parent, responses);
tool_options.fill.apply_fill(layer, responses);
tool_options.stroke.apply_stroke(tool_data.weight, layer, responses);
tool_data.layer = Some(layer);
let transform = document.metadata().transform_to_viewport(layer);
let position = transform.inverse().transform_point2(input.mouse.position);
extend_path_with_next_segment(tool_data, position, responses);
FreehandToolFsmState::Drawing
}
(FreehandToolFsmState::Drawing, FreehandToolMessage::PointerMove) => {
if let Some(layer) = tool_data.layer {
let transform = document.metadata().transform_to_viewport(layer);
let pos = transform.inverse().transform_point2(input.mouse.position);
let position = transform.inverse().transform_point2(input.mouse.position);
if tool_data.last_point != pos {
let manipulator_group = ManipulatorGroup::new_anchor(pos);
let modification = if tool_data.extend_from_start {
VectorDataModification::AddStartManipulatorGroup { subpath_index: 0, manipulator_group }
} else {
VectorDataModification::AddEndManipulatorGroup { subpath_index: 0, manipulator_group }
};
responses.add(GraphOperationMessage::Vector { layer, modification });
tool_data.dragged = true;
tool_data.last_point = pos;
}
extend_path_with_next_segment(tool_data, position, responses);
}
FreehandToolFsmState::Drawing
@@ -276,6 +256,8 @@ impl Fsm for FreehandToolFsmState {
(FreehandToolFsmState::Drawing, FreehandToolMessage::DragStop) => {
if tool_data.dragged {
responses.add(DocumentMessage::CommitTransaction);
} else {
responses.add(DocumentMessage::DocumentHistoryBackward);
}
tool_data.layer = None;
@@ -312,3 +294,34 @@ impl Fsm for FreehandToolFsmState {
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
}
}
fn extend_path_with_next_segment(tool_data: &mut FreehandToolData, position: DVec2, responses: &mut VecDeque<Message>) {
if !tool_data.end_point.map_or(true, |(last_pos, _)| position != last_pos) || !position.is_finite() {
return;
}
let Some(layer) = tool_data.layer else { return };
let id = PointId::generate();
responses.add(GraphOperationMessage::Vector {
layer,
modification_type: VectorModificationType::InsertPoint { id, position },
});
if let Some((_, previous_position)) = tool_data.end_point {
let next_id = SegmentId::generate();
let points = [previous_position, id];
responses.add(GraphOperationMessage::Vector {
layer,
modification_type: VectorModificationType::InsertSegment {
id: next_id,
points,
handles: [None, None],
},
});
}
tool_data.dragged = true;
tool_data.end_point = Some((position, id));
}
@@ -259,7 +259,7 @@ impl Fsm for GradientToolFsmState {
let Gradient { start, end, stops, .. } = gradient;
let (start, end) = (transform.transform_point2(start), transform.transform_point2(end));
overlay_context.line(start, end, None, None);
overlay_context.line(start, end);
overlay_context.manipulator_handle(start, dragging == Some(GradientDragTarget::Start));
overlay_context.manipulator_handle(end, dragging == Some(GradientDragTarget::End));
@@ -1,6 +1,7 @@
use super::tool_prelude::*;
use crate::consts::LINE_ROTATE_SNAP_ANGLE;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_types::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
@@ -8,9 +9,8 @@ use crate::messages::tool::common_functionality::color_selector::{ToolColorOptio
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager};
use graph_craft::document::NodeId;
use graph_craft::document::{value::TaggedValue, NodeId, NodeInput};
use graphene_core::uuid::generate_uuid;
use graphene_core::vector::style::Stroke;
use graphene_core::Color;
#[derive(Default)]
@@ -177,11 +177,24 @@ impl Fsm for LineToolFsmState {
let snapped = tool_data.snap_manager.free_snap(&SnapData::new(document, input), &point, None, false);
tool_data.drag_start = snapped.snapped_point_document;
let subpath = bezier_rs::Subpath::new_line(DVec2::ZERO, DVec2::X);
responses.add(DocumentMessage::StartTransaction);
let layer = graph_modification_utils::new_vector_layer(vec![subpath], NodeId(generate_uuid()), document.new_layer_parent(true), responses);
let nodes = {
let node_type = resolve_document_node_type("Line").expect("Line node does not exist");
let node = node_type.to_document_node_default_inputs(
[
None,
Some(NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), false)),
Some(NodeInput::value(TaggedValue::DVec2(DVec2::X), false)),
],
Default::default(),
);
HashMap::from([(NodeId(0), node)])
};
let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, document.new_layer_parent(false), responses);
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
tool_data.layer = Some(layer);
responses.add(GraphOperationMessage::TransformSet {
layer,
@@ -190,11 +203,6 @@ impl Fsm for LineToolFsmState {
skip_rerender: false,
});
responses.add(GraphOperationMessage::StrokeSet {
layer,
stroke: Stroke::new(tool_options.stroke.active_color(), tool_options.line_weight),
});
tool_data.layer = Some(layer);
tool_data.weight = tool_options.line_weight;
@@ -4,13 +4,12 @@ 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_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};
use graph_craft::document::NodeNetwork;
use graphene_core::renderer::Quad;
use graphene_core::vector::{ManipulatorPointId, SelectedType};
use graphene_core::vector::ManipulatorPointId;
use std::vec;
@@ -92,10 +91,7 @@ impl LayoutHolder for PathTool {
let (x, y) = coordinates.map(|point| (Some(point.x), Some(point.y))).unwrap_or((None, None));
let selection_status = &self.tool_data.selection_status;
let manipulator_angle = selection_status
.as_multiple()
.map(|multiple| multiple.manipulator_angle)
.or_else(|| selection_status.as_one().map(|point| point.manipulator_angle));
let manipulator_angle = selection_status.angle();
let x_location = NumberInput::new(x)
.unit(" px")
@@ -105,8 +101,11 @@ impl LayoutHolder for PathTool {
.min(-((1_u64 << std::f64::MANTISSA_DIGITS) as f64))
.max((1_u64 << std::f64::MANTISSA_DIGITS) as f64)
.on_update(move |number_input: &NumberInput| {
let new_x = number_input.value.unwrap_or(x.unwrap());
PathToolMessage::SelectedPointXChanged { new_x }.into()
if let Some(new_x) = number_input.value.or(x) {
PathToolMessage::SelectedPointXChanged { new_x }.into()
} else {
Message::NoOp
}
})
.widget_holder();
@@ -118,8 +117,11 @@ impl LayoutHolder for PathTool {
.min(-((1_u64 << std::f64::MANTISSA_DIGITS) as f64))
.max((1_u64 << std::f64::MANTISSA_DIGITS) as f64)
.on_update(move |number_input: &NumberInput| {
let new_y = number_input.value.unwrap_or(y.unwrap());
PathToolMessage::SelectedPointYChanged { new_y }.into()
if let Some(new_y) = number_input.value.or(y) {
PathToolMessage::SelectedPointYChanged { new_y }.into()
} else {
Message::NoOp
}
})
.widget_holder();
@@ -273,7 +275,7 @@ impl PathToolData {
fn update_insertion(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>, mouse_position: DVec2) -> PathToolFsmState {
if let Some(closed_segment) = &mut self.segment {
closed_segment.update_closest_point(&document.metadata, mouse_position);
if closed_segment.too_far(mouse_position, INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE) {
if closed_segment.too_far(mouse_position, INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE, &document.metadata) {
self.end_insertion(shape_editor, responses, InsertEndKind::Abort)
} else {
PathToolFsmState::InsertPoint
@@ -318,6 +320,8 @@ impl PathToolData {
let document_network = document.network();
let document_metadata = document.metadata();
self.drag_start_pos = input.mouse.position;
// Select the first point within the threshold (in pixels)
if let Some(selected_points) = shape_editor.change_point_selection(document_network, document_metadata, input.mouse.position, SELECTION_THRESHOLD, add_to_selection) {
if let Some(selected_points) = selected_points {
@@ -345,7 +349,7 @@ impl PathToolData {
}
self.drag_start_pos = input.mouse.position;
self.previous_mouse_position = document.metadata.document_to_viewport.inverse().transform_point2(input.mouse.position);
shape_editor.select_all_anchors_in_layer(&document.network, layer);
shape_editor.select_connected_anchors(document, layer, input.mouse.position);
PathToolFsmState::Dragging
}
@@ -370,16 +374,12 @@ impl PathToolData {
// Do not snap against handles when anchor is selected
let mut additional_selected_points = Vec::new();
for point in selected_points.points.iter() {
if point.point_id.manipulator_type == SelectedType::Anchor {
additional_selected_points.push(ManipulatorPointInfo {
layer: point.layer,
point_id: ManipulatorPointId::new(point.point_id.group, SelectedType::InHandle),
});
additional_selected_points.push(ManipulatorPointInfo {
layer: point.layer,
point_id: ManipulatorPointId::new(point.point_id.group, SelectedType::OutHandle),
});
}
let Some(anchor) = point.point_id.as_anchor() else { continue };
let connected = selected_points.vector_data.segment_domain.all_connected(anchor).map(|handle| handle.to_manipulator_point());
let filtered = connected.filter(|point| point.get_position(&selected_points.vector_data).is_some());
let point_info = filtered.map(|point_id| ManipulatorPointInfo { layer: point.layer, point_id });
additional_selected_points.extend(point_info);
}
selected_points.points.extend(additional_selected_points);
@@ -387,27 +387,39 @@ impl PathToolData {
self.previous_mouse_position = viewport_to_document.transform_point2(input.mouse.position - selected_points.offset);
}
fn drag(&mut self, shift: bool, alt: bool, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
fn update_colinear(&mut self, shift: bool, alt: bool, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) -> bool {
// Check if the alt key has just been pressed
if alt && !self.alt_debounce {
self.opposing_handle_lengths = None;
shape_editor.toggle_colinear_handles_state_on_selected(responses);
let colinear = self.selection_status.angle().map_or(false, |angle| match angle {
ManipulatorAngle::Colinear => true,
ManipulatorAngle::Free => false,
ManipulatorAngle::Mixed => false,
});
if colinear {
shape_editor.disable_colinear_handles_state_on_selected(&document.metadata, &document.network, responses);
} else {
shape_editor.convert_selected_manipulators_to_colinear_handles(responses, document);
}
self.alt_debounce = true;
return true;
}
self.alt_debounce = alt;
if shift {
if self.opposing_handle_lengths.is_none() {
self.opposing_handle_lengths = Some(shape_editor.opposing_handle_lengths(&document.network));
self.opposing_handle_lengths = Some(shape_editor.opposing_handle_lengths(document));
}
} else if let Some(opposing_handle_lengths) = &self.opposing_handle_lengths {
shape_editor.reset_opposing_handle_lengths(&document.network, opposing_handle_lengths, responses);
self.opposing_handle_lengths = None;
}
false
}
fn drag(&mut self, equidistant: bool, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
// Move the selected points with the mouse
let previous_mouse = document.metadata.document_to_viewport.transform_point2(self.previous_mouse_position);
let snapped_delta = shape_editor.snap(&mut self.snap_manager, document, input, previous_mouse);
shape_editor.move_selected_points(&document.network, &document.metadata, snapped_delta, shift, responses);
let handle_lengths = if equidistant { None } else { self.opposing_handle_lengths.take() };
shape_editor.move_selected_points(handle_lengths, &document, snapped_delta, equidistant, responses);
self.previous_mouse_position += document.metadata.document_to_viewport.inverse().transform_vector2(snapped_delta);
}
}
@@ -431,7 +443,6 @@ impl Fsm for PathToolFsmState {
responses.add(OverlaysMessage::Draw);
responses.add(PathToolMessage::SelectedPointUpdated);
// This can happen in any state (which is why we return self)
self
}
(_, PathToolMessage::Overlays(mut overlay_context)) => {
@@ -503,7 +514,9 @@ impl Fsm for PathToolFsmState {
(PathToolFsmState::Dragging, PathToolMessage::PointerMove { alt, shift }) => {
let alt_state = input.keyboard.get(alt as usize);
let shift_state = input.keyboard.get(shift as usize);
tool_data.drag(shift_state, alt_state, shape_editor, document, input, responses);
if !tool_data.update_colinear(shift_state, alt_state, shape_editor, document, responses) {
tool_data.drag(shift_state, shape_editor, document, input, responses);
}
// Auto-panning
let messages = [PathToolMessage::PointerOutsideViewport { alt, shift }.into(), PathToolMessage::PointerMove { alt, shift }.into()];
@@ -521,9 +534,9 @@ impl Fsm for PathToolFsmState {
}
(PathToolFsmState::Dragging, PathToolMessage::PointerOutsideViewport { shift, .. }) => {
// Auto-panning
if let Some(delta) = tool_data.auto_panning.shift_viewport(input, responses) {
if tool_data.auto_panning.shift_viewport(input, responses).is_some() {
let shift_state = input.keyboard.get(shift as usize);
shape_editor.move_selected_points(&document.network, &document.metadata, -delta, shift_state, responses);
tool_data.drag(shift_state, shape_editor, document, input, responses);
}
PathToolFsmState::Dragging
@@ -575,18 +588,16 @@ impl Fsm for PathToolFsmState {
(_, 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)
.map(|(_, nearest_point)| nearest_point);
let nearest_point = shape_editor.find_nearest_point_indices(&document.network, &document.metadata, input.mouse.position, SELECTION_THRESHOLD);
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 && !equidistant {
let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == Some(point));
if clicked_selected {
shape_editor.deselect_all_points();
shape_editor.change_point_selection(&document.network, &document.metadata, input.mouse.position, SELECTION_THRESHOLD, false);
responses.add(OverlaysMessage::Draw);
if let Some((layer, nearest_point)) = nearest_point {
if tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD && !equidistant {
let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == point);
if clicked_selected {
shape_editor.deselect_all_points();
shape_editor.selected_shape_state.entry(layer).or_default().select_point(nearest_point);
responses.add(OverlaysMessage::Draw);
}
}
}
@@ -599,17 +610,17 @@ impl Fsm for PathToolFsmState {
(_, PathToolMessage::Delete) => {
// Delete the selected points and clean up overlays
responses.add(DocumentMessage::StartTransaction);
shape_editor.delete_selected_points(responses);
shape_editor.delete_selected_points(&document, responses);
responses.add(PathToolMessage::SelectionChanged);
PathToolFsmState::Ready
}
(_, PathToolMessage::BreakPath) => {
shape_editor.break_path_at_selected_point(&document.network, responses);
shape_editor.break_path_at_selected_point(document, responses);
PathToolFsmState::Ready
}
(_, PathToolMessage::DeleteAndBreakPath) => {
shape_editor.delete_point_and_break_path(&document.network, responses);
shape_editor.delete_point_and_break_path(document, responses);
PathToolFsmState::Ready
}
(_, PathToolMessage::FlipSmoothSharp) => {
@@ -625,12 +636,12 @@ impl Fsm for PathToolFsmState {
}
(_, PathToolMessage::PointerMove { .. }) => self,
(_, PathToolMessage::NudgeSelectedPoints { delta_x, delta_y }) => {
shape_editor.move_selected_points(&document.network, &document.metadata, (delta_x, delta_y).into(), true, responses);
shape_editor.move_selected_points(tool_data.opposing_handle_lengths.take(), &document, (delta_x, delta_y).into(), true, responses);
PathToolFsmState::Ready
}
(_, PathToolMessage::SelectAllAnchors) => {
shape_editor.select_all_anchors_in_selected_layers(&document.network);
shape_editor.select_all_anchors_in_selected_layers(document);
responses.add(OverlaysMessage::Draw);
PathToolFsmState::Ready
}
@@ -641,13 +652,13 @@ impl Fsm for PathToolFsmState {
}
(_, PathToolMessage::SelectedPointXChanged { new_x }) => {
if let Some(&SingleSelectedPoint { coordinates, id, layer, .. }) = tool_data.selection_status.as_one() {
shape_editor.reposition_control_point(&id, responses, &document.network, &document.metadata, DVec2::new(new_x, coordinates.y), layer);
shape_editor.reposition_control_point(&id, &document.network, &document.metadata, DVec2::new(new_x, coordinates.y), layer, responses);
}
PathToolFsmState::Ready
}
(_, PathToolMessage::SelectedPointYChanged { new_y }) => {
if let Some(&SingleSelectedPoint { coordinates, id, layer, .. }) = tool_data.selection_status.as_one() {
shape_editor.reposition_control_point(&id, responses, &document.network, &document.metadata, DVec2::new(coordinates.x, new_y), layer);
shape_editor.reposition_control_point(&id, &document.network, &document.metadata, DVec2::new(coordinates.x, new_y), layer, responses);
}
PathToolFsmState::Ready
}
@@ -657,14 +668,14 @@ impl Fsm for PathToolFsmState {
}
(_, PathToolMessage::ManipulatorMakeHandlesColinear) => {
responses.add(DocumentMessage::StartTransaction);
shape_editor.set_colinear_handles_state_on_selected(true, responses);
shape_editor.convert_selected_manipulators_to_colinear_handles(responses, &document.network);
shape_editor.convert_selected_manipulators_to_colinear_handles(responses, document);
responses.add(DocumentMessage::CommitTransaction);
responses.add(PathToolMessage::SelectionChanged);
PathToolFsmState::Ready
}
(_, PathToolMessage::ManipulatorMakeHandlesFree) => {
responses.add(DocumentMessage::StartTransaction);
shape_editor.set_colinear_handles_state_on_selected(false, responses);
shape_editor.disable_colinear_handles_state_on_selected(&document.metadata, &document.network, responses);
responses.add(DocumentMessage::CommitTransaction);
PathToolFsmState::Ready
}
@@ -743,10 +754,11 @@ impl SelectionStatus {
}
}
fn as_multiple(&self) -> Option<&MultipleSelectedPoints> {
fn angle(&self) -> Option<ManipulatorAngle> {
match self {
SelectionStatus::Multiple(multiple) => Some(multiple),
_ => None,
Self::None => None,
Self::One(one) => Some(one.manipulator_angle),
Self::Multiple(one) => Some(one.manipulator_angle),
}
}
}
@@ -775,33 +787,23 @@ 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 {
let Some(vector_data) = document_metadata.compute_modified_vector(layer, document_network) else {
return SelectionStatus::None;
};
let Some(colinear_manipulators) = get_colinear_manipulators(layer, document_network) else {
let Some(&point) = shape_state.selected_points().next() else {
return SelectionStatus::None;
};
let Some(point) = shape_state.selected_points().next() else {
return SelectionStatus::None;
};
let Some(manipulator) = get_manipulator_from_id(subpaths, point.group) else {
return SelectionStatus::None;
};
let Some(local_position) = point.manipulator_type.get_position(manipulator) else {
let Some(local_position) = point.get_position(&vector_data) else {
return SelectionStatus::None;
};
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
};
let manipulator_angle = if vector_data.colinear(point) { ManipulatorAngle::Colinear } else { ManipulatorAngle::Free };
return SelectionStatus::One(SingleSelectedPoint {
coordinates,
layer,
id: *point,
id: point,
manipulator_angle,
});
};
@@ -809,7 +811,7 @@ fn get_selection_status(document_network: &NodeNetwork, document_metadata: &Docu
// Check to see if multiple manipulator groups are selected
if total_selected_points > 1 {
return SelectionStatus::Multiple(MultipleSelectedPoints {
manipulator_angle: shape_state.selected_manipulator_angles(document_network),
manipulator_angle: shape_state.selected_manipulator_angles(document_network, document_metadata),
});
}
+244 -358
View File
@@ -1,21 +1,22 @@
use super::tool_prelude::*;
use crate::consts::HIDE_HANDLE_DISTANCE;
use crate::consts::LINE_ROTATE_SNAP_ANGLE;
use crate::messages::portfolio::document::graph_operation::utility_types::VectorDataModification;
use crate::messages::portfolio::document::node_graph::document_node_types::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_functions::path_overlays;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::graph_modification_utils::get_subpaths;
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager};
use crate::messages::tool::common_functionality::utility_functions::should_extend;
use bezier_rs::{Bezier, BezierHandles};
use graph_craft::document::NodeId;
use graphene_core::uuid::{generate_uuid, ManipulatorGroupId};
use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::vector::{ManipulatorPointId, SelectedType};
use graphene_core::uuid::generate_uuid;
use graphene_core::vector::{PointId, VectorModificationType};
use graphene_core::Color;
use graphene_std::vector::{HandleId, SegmentId};
#[derive(Default)]
pub struct PenTool {
@@ -187,310 +188,175 @@ impl ToolTransition for PenTool {
}
}
}
#[derive(Default)]
#[derive(Clone, Debug, Default)]
struct ModifierState {
snap_angle: bool,
lock_angle: bool,
break_handle: bool,
}
#[derive(Clone, Debug)]
struct LastPoint {
id: PointId,
pos: DVec2,
in_segment: Option<SegmentId>,
handle_start: DVec2,
}
#[derive(Clone, Debug, Default)]
struct PenToolData {
weight: f64,
layer: Option<LayerNodeIdentifier>,
subpath_index: usize,
snap_manager: SnapManager,
colinear_handles: bool,
// Indicates that curve extension is occurring from the first point, rather than (more commonly) the last point
from_start: bool,
latest_points: Vec<LastPoint>,
point_index: usize,
handle_end: Option<DVec2>,
next_point: DVec2,
next_handle_start: DVec2,
g1_continuous: bool,
angle: f64,
auto_panning: AutoPanning,
modifiers: ModifierState,
}
impl PenToolData {
fn extend_subpath(&mut self, layer: LayerNodeIdentifier, subpath_index: usize, from_start: bool, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
self.layer = Some(layer);
self.from_start = from_start;
self.subpath_index = subpath_index;
let Some(subpaths) = get_subpaths(layer, &document.network) else { return };
let manipulator_groups = subpaths[subpath_index].manipulator_groups();
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;
let modification = VectorDataModification::SetManipulatorColinearHandlesState { id, colinear: false };
// Stop the handles on the first point from being colinear
responses.add(GraphOperationMessage::Vector { layer, modification });
fn latest_point(&self) -> Option<&LastPoint> {
self.latest_points.get(self.point_index)
}
fn create_new_path(
&mut self,
document: &DocumentMessageHandler,
line_weight: f64,
stroke_color: Option<Color>,
fill_color: Option<Color>,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) {
let parent = document.new_layer_parent(true);
// Deselect layers because we are now creating a new layer
responses.add(DocumentMessage::DeselectAllLayers);
fn latest_point_mut(&mut self) -> Option<&mut LastPoint> {
self.latest_points.get_mut(self.point_index)
}
// Get the position and set properties
let transform = document.metadata().transform_to_document(parent);
let point = SnapCandidatePoint::handle(document.metadata.document_to_viewport.inverse().transform_point2(input.mouse.position));
let snapped = self.snap_manager.free_snap(&SnapData::new(document, input), &point, None, false);
let start_position = transform.inverse().transform_point2(snapped.snapped_point_document);
self.snap_manager.update_indicator(snapped);
self.weight = line_weight;
// Create the initial shape with a `bez_path` (only contains a moveto initially)
let subpath = bezier_rs::Subpath::new(vec![bezier_rs::ManipulatorGroup::new(start_position, Some(start_position), Some(start_position))], false);
let layer = graph_modification_utils::new_vector_layer(vec![subpath], NodeId(generate_uuid()), parent, responses);
self.layer = Some(layer);
responses.add(GraphOperationMessage::FillSet {
layer,
fill: if let Some(color) = fill_color { Fill::Solid(color) } else { Fill::None },
});
responses.add(GraphOperationMessage::StrokeSet {
layer,
stroke: Stroke::new(stroke_color, line_weight),
});
self.from_start = false;
self.subpath_index = 0;
fn add_point(&mut self, point: LastPoint) {
self.point_index = (self.point_index + 1).min(self.latest_points.len());
self.latest_points.truncate(self.point_index);
self.latest_points.push(point);
}
/// 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 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;
// 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 },
});
// 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 },
});
// 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> {
// 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 first manipulator group
let first_manipulator_group = if self.from_start {
subpath.manipulator_groups().last()?
} else {
subpath.manipulator_groups().first()?
};
// Get correct handle types
let inwards_handle = if self.from_start { SelectedType::OutHandle } else { SelectedType::InHandle };
let outwards_handle = if self.from_start { SelectedType::InHandle } else { SelectedType::OutHandle };
// Get manipulator points
let last_anchor = last_manipulator_group.anchor;
let first_anchor = first_manipulator_group.anchor;
let last_in = inwards_handle.get_position(last_manipulator_group)?;
let transform = document.metadata.document_to_viewport * transform;
let transformed_distance_between_squared = transform.transform_point2(last_anchor).distance_squared(transform.transform_point2(first_anchor));
let snap_point_tolerance_squared = crate::consts::SNAP_POINT_TOLERANCE.powi(2);
let should_close_path = transformed_distance_between_squared < snap_point_tolerance_squared && previous_manipulator_group.is_some();
if should_close_path {
// Move the in handle of the first point to where the user has placed it
let point = ManipulatorPointId::new(first_manipulator_group.id, inwards_handle);
responses.add(GraphOperationMessage::Vector {
layer,
modification: VectorDataModification::SetManipulatorPosition { point, position: last_in },
});
// Stop the handles on the first point from being colinear
let id = first_manipulator_group.id;
responses.add(GraphOperationMessage::Vector {
layer,
modification: VectorDataModification::SetManipulatorColinearHandlesState { id, colinear: false },
});
// Remove the point that has just been placed
responses.add(GraphOperationMessage::Vector {
layer,
modification: VectorDataModification::RemoveManipulatorGroup { id: last_manipulator_group.id },
});
// Push a close path node
responses.add(GraphOperationMessage::Vector {
layer,
modification: VectorDataModification::SetClosed { index: 0, closed: true },
});
responses.add(DocumentMessage::CommitTransaction);
// Clean up tool data
self.layer = None;
self.snap_manager.cleanup(responses);
// Return to ready state
return Some(PenToolFsmState::Ready);
}
// Add a new manipulator for the next anchor that we will place
if let Some(out_handle) = outwards_handle.get_position(last_manipulator_group) {
responses.add(add_manipulator_group(self.layer, self.from_start, bezier_rs::ManipulatorGroup::new_anchor(out_handle)));
}
Some(PenToolFsmState::PlacingAnchor)
}
fn drag_handle(&mut self, mut snap_data: SnapData, transform: DAffine2, mouse: DVec2, modifiers: ModifierState, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> {
fn bend_from_previous_point(&mut self, snap_data: SnapData, transform: DAffine2) {
self.g1_continuous = true;
let document = snap_data.document;
// Get subpath
let subpath = &get_subpaths(self.layer?, &document.network)?[self.subpath_index];
self.next_handle_start = self.next_point;
// Get the last manipulator group
let manipulator_groups = subpath.manipulator_groups();
let last_manipulator_group = if self.from_start { manipulator_groups.first()? } else { manipulator_groups.last()? };
// Get correct handle types
let inwards_handle = if self.from_start { SelectedType::OutHandle } else { SelectedType::InHandle };
let outwards_handle = if self.from_start { SelectedType::InHandle } else { SelectedType::OutHandle };
// Get manipulator points
let last_anchor = last_manipulator_group.anchor;
let colinear = !modifiers.break_handle && self.colinear_handles;
snap_data.manipulators = vec![(self.layer?, last_manipulator_group.id)];
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);
// Break the control
let Some(last_pos) = self.latest_point().map(|point| point.pos) else { return };
let transform = document.metadata.document_to_viewport * transform;
let on_top = transform.transform_point2(self.next_point).distance_squared(transform.transform_point2(last_pos)) < crate::consts::SNAP_POINT_TOLERANCE.powi(2);
if on_top {
if let Some(point) = self.latest_point_mut() {
point.in_segment = None;
}
self.handle_end = None;
}
}
// Update points on current segment (to show preview of new handle)
let point = ManipulatorPointId::new(last_manipulator_group.id, outwards_handle);
responses.add(GraphOperationMessage::Vector {
layer: self.layer?,
modification: VectorDataModification::SetManipulatorPosition { point, position },
fn finish_placing_handle(&mut self, snap_data: SnapData, transform: DAffine2, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> {
let document = snap_data.document;
let next_handle_start = self.next_handle_start;
let handle_start = self.latest_point()?.handle_start;
let mouse = snap_data.input.mouse.position;
let Some(handle_end) = self.handle_end else {
self.handle_end = Some(next_handle_start);
self.place_anchor(snap_data, transform, mouse, responses);
self.latest_point_mut()?.handle_start = next_handle_start;
return None;
};
let next_point = self.next_point;
self.place_anchor(snap_data, transform, mouse, responses);
let handles = [handle_start - self.latest_point()?.pos, handle_end - next_point].map(Some);
// Get close path
let mut end = None;
let layer = self.layer?;
let vector_data = document.metadata.compute_modified_vector(layer, &document.network)?;
let start = self.latest_point()?.id;
let transform = document.metadata.document_to_viewport * transform;
for id in vector_data.single_connected_points().filter(|&point| point != start) {
let Some(pos) = vector_data.point_domain.position_from_id(id) else { continue };
let transformed_distance_between_squared = transform.transform_point2(pos).distance_squared(transform.transform_point2(next_point));
let snap_point_tolerance_squared = crate::consts::SNAP_POINT_TOLERANCE.powi(2);
if transformed_distance_between_squared < snap_point_tolerance_squared {
end = Some(id);
}
}
let close_subpath = end.is_some();
// Generate new point if not closing
let end = end.unwrap_or_else(|| {
let end = PointId::generate();
let modification_type = VectorModificationType::InsertPoint { id: end, position: next_point };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
end
});
// 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 position = last_anchor - (position - last_anchor);
let point = ManipulatorPointId::new(last_manipulator_group.id, inwards_handle);
let points = [start, end];
let id = SegmentId::generate();
let modification_type = VectorModificationType::InsertSegment { id, points, handles };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
// Mirror
if let Some(last_segment) = self.latest_point().and_then(|point| point.in_segment) {
responses.add(GraphOperationMessage::Vector {
layer: self.layer?,
modification: VectorDataModification::SetManipulatorPosition { point, position },
layer,
modification_type: VectorModificationType::SetG1Continuous {
handles: [HandleId::end(last_segment), HandleId::primary(id)],
enabled: true,
},
});
}
if !close_subpath {
self.add_point(LastPoint {
id: end,
pos: next_point,
in_segment: self.g1_continuous.then_some(id),
handle_start: next_handle_start,
});
}
Some(if close_subpath { PenToolFsmState::Ready } else { PenToolFsmState::PlacingAnchor })
}
// 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::SetManipulatorColinearHandlesState { id, colinear },
});
fn drag_handle(&mut self, snap_data: SnapData, transform: DAffine2, mouse: DVec2, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> {
let colinear = !self.modifiers.break_handle && self.handle_end.is_some();
self.next_handle_start = self.compute_snapped_angle(snap_data, transform, colinear, mouse, Some(self.next_point), false);
if let Some(handle_end) = self.handle_end.as_mut().filter(|_| colinear) {
*handle_end = self.next_point * 2. - self.next_handle_start;
self.g1_continuous = true;
} else {
self.g1_continuous = false;
}
responses.add(OverlaysMessage::Draw);
Some(PenToolFsmState::DraggingHandle)
}
fn place_anchor(&mut self, mut snap_data: SnapData, transform: DAffine2, mouse: DVec2, modifiers: ModifierState, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> {
let document = snap_data.document;
// 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 first manipulator group
let manipulator_groups = subpath.manipulator_groups();
let first_manipulator_group = if self.from_start { manipulator_groups.last()? } else { manipulator_groups.first()? };
// Get manipulator points
let first_anchor = first_manipulator_group.anchor;
let previous_anchor = previous_manipulator_group.map(|group| group.anchor);
let pos = if let Some(last_anchor) = previous_anchor.filter(|&a| mouse.distance_squared(transform.transform_point2(a)) < crate::consts::SNAP_POINT_TOLERANCE.powi(2)) {
// Snap to the previously placed point (to show break control)
last_anchor
} else if mouse.distance_squared(transform.transform_point2(first_anchor)) < crate::consts::SNAP_POINT_TOLERANCE.powi(2) {
// Snap to the first point (to show close path)
first_anchor
} else {
snap_data.manipulators = vec![(self.layer?, last_manipulator_group.id)];
self.compute_snapped_angle(snap_data, transform, modifiers.lock_angle, modifiers.snap_angle, false, mouse, previous_anchor, true)
};
for manipulator_type in [SelectedType::Anchor, SelectedType::InHandle, SelectedType::OutHandle] {
let point = ManipulatorPointId::new(last_manipulator_group.id, manipulator_type);
responses.add(GraphOperationMessage::Vector {
layer,
modification: VectorDataModification::SetManipulatorPosition { point, position: pos },
});
fn place_anchor(&mut self, snap_data: SnapData, transform: DAffine2, mouse: DVec2, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> {
let relative = self.latest_point().map(|point| point.pos);
self.next_point = self.compute_snapped_angle(snap_data, transform, false, mouse, relative, true);
if let Some(handle_end) = self.handle_end.as_mut() {
*handle_end = self.next_point;
self.next_handle_start = self.next_point;
}
responses.add(OverlaysMessage::Draw);
Some(PenToolFsmState::PlacingAnchor)
}
/// 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, colinear: bool, mouse: DVec2, relative: Option<DVec2>, neighbor: bool) -> DVec2 {
fn compute_snapped_angle(&mut self, snap_data: SnapData, transform: DAffine2, colinear: bool, mouse: DVec2, relative: Option<DVec2>, neighbor: bool) -> DVec2 {
let ModifierState { snap_angle, lock_angle, .. } = self.modifiers;
let document = snap_data.document;
let mut document_pos = document.metadata.document_to_viewport.inverse().transform_point2(mouse);
let snap = &mut self.snap_manager;
let neighbors = relative.filter(|_| neighbor).map_or(Vec::new(), |neighbor| vec![neighbor]);
if let Some(relative) = relative.map(|layer| transform.transform_point2(layer)).filter(|_| snap_angle || lock_angle) {
if let Some(relative) = relative
.map(|layer| transform.transform_point2(layer))
.filter(|&relative| (snap_angle || lock_angle) && (relative - document_pos).length_squared() > f64::EPSILON * 100.)
{
let resolution = LINE_ROTATE_SNAP_ANGLE.to_radians();
let angle = if lock_angle {
@@ -540,56 +406,11 @@ impl PenToolData {
if let Some(relative) = relative.map(|layer| transform.transform_point2(layer)) {
if (relative - document_pos) != DVec2::ZERO {
self.angle = -(relative - document_pos).angle_between(DVec2::X)
} else {
self.angle = 0.0;
}
}
transform.inverse().transform_point2(document_pos)
}
fn finish_transaction(&mut self, fsm: PenToolFsmState, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) -> Option<DocumentMessage> {
// Get subpath
let subpath = &get_subpaths(self.layer?, &document.network)?[self.subpath_index];
// Abort if only one manipulator group has been placed
if fsm == PenToolFsmState::PlacingAnchor && subpath.len() < 3 {
return None;
}
// Get the last manipulator group and the one previous to that
let mut manipulator_groups = subpath.manipulator_groups().iter();
let mut 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 };
// If placing anchor we should abort if there are less than three manipulators (as the last one gets deleted)
let Some(previous_manipulator_group) = previous_manipulator_group else {
return Some(DocumentMessage::AbortTransaction);
};
// Clean up if there are two or more manipulators
// Remove the unplaced anchor if in anchor placing mode
if fsm == PenToolFsmState::PlacingAnchor {
responses.add(GraphOperationMessage::Vector {
layer: self.layer?,
modification: VectorDataModification::RemoveManipulatorGroup { id: last_manipulator_group.id },
});
last_manipulator_group = previous_manipulator_group;
}
// Remove the out handle
let point = ManipulatorPointId::new(last_manipulator_group.id, outwards_handle);
let position = last_manipulator_group.anchor;
responses.add(GraphOperationMessage::Vector {
layer: self.layer?,
modification: VectorDataModification::SetManipulatorPosition { point, position },
});
Some(DocumentMessage::CommitTransaction)
}
}
impl Fsm for PenToolFsmState {
@@ -628,9 +449,52 @@ impl Fsm for PenToolFsmState {
responses.add(OverlaysMessage::Draw);
self
}
(_, PenToolMessage::Overlays(mut overlay_context)) => {
(PenToolFsmState::Ready, PenToolMessage::Overlays(mut overlay_context)) => {
path_overlays(document, shape_editor, &mut overlay_context);
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
self
}
(_, PenToolMessage::Overlays(mut overlay_context)) => {
let transform = document.metadata.document_to_viewport * transform;
if let (Some((start, handle_start)), Some(handle_end)) = (tool_data.latest_point().map(|point| (point.pos, point.handle_start)), tool_data.handle_end) {
let handles = BezierHandles::Cubic { handle_start, handle_end };
let bezier = Bezier {
start,
handles,
end: tool_data.next_point,
};
overlay_context.outline_bezier(bezier, transform);
}
let valid = |point: DVec2, handle: DVec2| point.distance_squared(handle) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE;
let next_point = transform.transform_point2(tool_data.next_point);
let next_handle_start = transform.transform_point2(tool_data.next_handle_start);
overlay_context.line(next_point, next_handle_start);
let start = tool_data.latest_point().map(|point| transform.transform_point2(point.pos));
let handle_start = tool_data.latest_point().map(|point| transform.transform_point2(point.handle_start));
let handle_end = tool_data.handle_end.map(|point| transform.transform_point2(point));
if let (Some(start), Some(handle_start), Some(handle_end)) = (start, handle_start, handle_end) {
overlay_context.line(start, handle_start);
overlay_context.line(next_point, handle_end);
path_overlays(document, shape_editor, &mut overlay_context);
if self == PenToolFsmState::DraggingHandle && valid(next_point, handle_end) {
overlay_context.manipulator_handle(handle_end, false);
}
if valid(start, handle_start) {
overlay_context.manipulator_handle(handle_start, false);
}
} else {
path_overlays(document, shape_editor, &mut overlay_context);
}
if self == PenToolFsmState::DraggingHandle && valid(next_point, next_handle_start) {
overlay_context.manipulator_handle(next_handle_start, false);
}
overlay_context.manipulator_anchor(next_point, false, None);
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
self
}
@@ -644,46 +508,71 @@ impl Fsm for PenToolFsmState {
(PenToolFsmState::Ready, PenToolMessage::DragStart) => {
responses.add(DocumentMessage::StartTransaction);
// 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) {
tool_data.extend_subpath(layer, subpath_index, from_start, document, responses);
if let Some((layer, point, position)) = should_extend(document, input.mouse.position, crate::consts::SNAP_POINT_TOLERANCE) {
tool_data.add_point(LastPoint {
id: point,
pos: position,
in_segment: None,
handle_start: position,
});
tool_data.layer = Some(layer);
tool_data.next_point = position;
tool_data.next_handle_start = position;
} else {
tool_data.create_new_path(
document,
tool_options.line_weight,
tool_options.stroke.active_color(),
tool_options.fill.active_color(),
input,
responses,
);
// New path layer
let nodes = {
let node_type = resolve_document_node_type("Path").expect("Path node does not exist");
HashMap::from([(NodeId(0), node_type.to_document_node_default_inputs([], Default::default()))])
};
let parent = document.new_layer_parent(true);
let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, parent, responses);
tool_options.fill.apply_fill(layer, responses);
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
tool_data.layer = Some(layer);
// Generate first point
let id = PointId::generate();
let transform = document.metadata().transform_to_document(parent);
let point = SnapCandidatePoint::handle(document.metadata.document_to_viewport.inverse().transform_point2(input.mouse.position));
let snapped = tool_data.snap_manager.free_snap(&SnapData::new(document, input), &point, None, false);
let pos = transform.inverse().transform_point2(snapped.snapped_point_document);
let modification_type = VectorModificationType::InsertPoint { id, position: pos };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
tool_data.add_point(LastPoint {
id,
pos,
in_segment: None,
handle_start: pos,
});
tool_data.next_point = pos;
tool_data.next_handle_start = pos;
}
tool_data.handle_end = None;
// Enter the dragging handle state while the mouse is held down, allowing the user to move the mouse and position the handle
PenToolFsmState::DraggingHandle
}
(PenToolFsmState::PlacingAnchor, PenToolMessage::DragStart) => {
responses.add(DocumentMessage::StartTransaction);
tool_data.bend_from_previous_point(document, transform, responses);
if tool_data.handle_end.is_some() {
responses.add(DocumentMessage::StartTransaction);
}
tool_data.bend_from_previous_point(SnapData::new(document, input), transform);
PenToolFsmState::DraggingHandle
}
(PenToolFsmState::DraggingHandle, PenToolMessage::DragStop) => {
tool_data.colinear_handles = true;
tool_data.finish_placing_handle(document, transform, responses).unwrap_or(PenToolFsmState::PlacingAnchor)
}
(PenToolFsmState::DraggingHandle, PenToolMessage::DragStop) => tool_data
.finish_placing_handle(SnapData::new(document, input), transform, responses)
.unwrap_or(PenToolFsmState::PlacingAnchor),
(PenToolFsmState::DraggingHandle, PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }) => {
let modifiers = ModifierState {
tool_data.modifiers = ModifierState {
snap_angle: input.keyboard.key(snap_angle),
lock_angle: input.keyboard.key(lock_angle),
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);
let state = tool_data.drag_handle(snap_data, transform, input.mouse.position, responses).unwrap_or(PenToolFsmState::Ready);
// Auto-panning
let messages = [
@@ -695,13 +584,13 @@ impl Fsm for PenToolFsmState {
state
}
(PenToolFsmState::PlacingAnchor, PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }) => {
let modifiers = ModifierState {
tool_data.modifiers = ModifierState {
snap_angle: input.keyboard.key(snap_angle),
lock_angle: input.keyboard.key(lock_angle),
break_handle: input.keyboard.key(break_handle),
};
let state = tool_data
.place_anchor(SnapData::new(document, input), transform, input.mouse.position, modifiers, responses)
.place_anchor(SnapData::new(document, input), transform, input.mouse.position, responses)
.unwrap_or(PenToolFsmState::Ready);
// Auto-panning
@@ -741,11 +630,10 @@ impl Fsm for PenToolFsmState {
state
}
(PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor, PenToolMessage::Abort | PenToolMessage::Confirm) => {
// Abort or commit the transaction to the undo history
let message = tool_data.finish_transaction(self, document, responses).unwrap_or(DocumentMessage::AbortTransaction);
responses.add(message);
tool_data.layer = None;
tool_data.handle_end = None;
tool_data.latest_points.clear();
tool_data.point_index = 0;
tool_data.snap_manager.cleanup(responses);
PenToolFsmState::Ready
@@ -755,12 +643,23 @@ impl Fsm for PenToolFsmState {
self
}
(PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor, PenToolMessage::Undo) => tool_data
.place_anchor(SnapData::new(document, input), transform, input.mouse.position, ModifierState::default(), responses)
.unwrap_or(PenToolFsmState::PlacingAnchor),
(_, PenToolMessage::Redo) => tool_data
.place_anchor(SnapData::new(document, input), transform, input.mouse.position, ModifierState::default(), responses)
.unwrap_or(PenToolFsmState::PlacingAnchor),
(PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor, PenToolMessage::Undo) => {
if tool_data.point_index > 0 {
tool_data.point_index -= 1;
tool_data
.place_anchor(SnapData::new(document, input), transform, input.mouse.position, responses)
.unwrap_or(PenToolFsmState::PlacingAnchor)
} else {
responses.add(PenToolMessage::Abort);
self
}
}
(_, PenToolMessage::Redo) => {
tool_data.point_index = (tool_data.point_index + 1).min(tool_data.latest_points.len().saturating_sub(1));
tool_data
.place_anchor(SnapData::new(document, input), transform, input.mouse.position, responses)
.unwrap_or(PenToolFsmState::PlacingAnchor)
}
_ => self,
}
}
@@ -800,16 +699,3 @@ impl Fsm for PenToolFsmState {
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
}
}
/// Pushes a [ManipulatorGroup] to the current layer via a [GraphOperationMessage].
fn add_manipulator_group(layer: Option<LayerNodeIdentifier>, from_start: bool, manipulator_group: bezier_rs::ManipulatorGroup<ManipulatorGroupId>) -> Message {
let Some(layer) = layer else {
return Message::NoOp;
};
let modification = if from_start {
VectorDataModification::AddStartManipulatorGroup { subpath_index: 0, manipulator_group }
} else {
VectorDataModification::AddEndManipulatorGroup { subpath_index: 0, manipulator_group }
};
GraphOperationMessage::Vector { layer, modification }.into()
}
@@ -1,5 +1,6 @@
use super::tool_prelude::*;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_types::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
@@ -7,9 +8,8 @@ use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::resize::Resize;
use crate::messages::tool::common_functionality::snapping::SnapData;
use graph_craft::document::NodeId;
use graph_craft::document::{value::TaggedValue, NodeId, NodeInput};
use graphene_core::uuid::generate_uuid;
use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::Color;
#[derive(Default)]
@@ -244,11 +244,34 @@ impl Fsm for PolygonToolFsmState {
polygon_data.start(document, input);
responses.add(DocumentMessage::StartTransaction);
let subpath = match tool_options.polygon_type {
PolygonType::Convex => bezier_rs::Subpath::new_regular_polygon(DVec2::ZERO, tool_options.vertices as u64, 1.),
PolygonType::Star => bezier_rs::Subpath::new_star_polygon(DVec2::ZERO, tool_options.vertices as u64, 1., 0.5),
let nodes = {
let node = match tool_options.polygon_type {
PolygonType::Convex => resolve_document_node_type("Regular Polygon")
.expect("Regular Polygon node does not exist")
.to_document_node_default_inputs(
[
None,
Some(NodeInput::value(TaggedValue::U32(tool_options.vertices as u32), false)),
Some(NodeInput::value(TaggedValue::F64(0.5), false)),
],
Default::default(),
),
PolygonType::Star => resolve_document_node_type("Star").expect("Star node does not exist").to_document_node_default_inputs(
[
None,
Some(NodeInput::value(TaggedValue::U32(tool_options.vertices as u32), false)),
Some(NodeInput::value(TaggedValue::F64(0.5), false)),
Some(NodeInput::value(TaggedValue::F64(0.25), false)),
],
Default::default(),
),
};
HashMap::from([(NodeId(0), node)])
};
let layer = graph_modification_utils::new_vector_layer(vec![subpath], NodeId(generate_uuid()), document.new_layer_parent(true), responses);
let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, document.new_layer_parent(false), responses);
tool_options.fill.apply_fill(layer, responses);
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
polygon_data.layer = Some(layer);
responses.add(GraphOperationMessage::TransformSet {
@@ -258,22 +281,19 @@ impl Fsm for PolygonToolFsmState {
skip_rerender: false,
});
let fill_color = tool_options.fill.active_color();
responses.add(GraphOperationMessage::FillSet {
layer,
fill: if let Some(color) = fill_color { Fill::Solid(color) } else { Fill::None },
});
responses.add(GraphOperationMessage::StrokeSet {
layer,
stroke: Stroke::new(tool_options.stroke.active_color(), tool_options.line_weight),
});
PolygonToolFsmState::Drawing
}
(PolygonToolFsmState::Drawing, PolygonToolMessage::PointerMove { center, lock_ratio }) => {
if let Some(message) = polygon_data.calculate_transform(document, input, center, lock_ratio, false) {
responses.add(message);
if let Some([start, end]) = tool_data.data.calculate_points(document, input, center, lock_ratio) {
if let Some(layer) = tool_data.data.layer {
// TODO: make the scale impact the polygon/star node - we need to determine how to allow the polygon node to make irregular shapes
responses.add(GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_scale_angle_translation(end - start, 0., (start + end) / 2.),
transform_in: TransformIn::Viewport,
skip_rerender: false,
});
}
}
// Auto-panning
@@ -1,4 +1,5 @@
use super::tool_prelude::*;
use crate::messages::portfolio::document::node_graph::document_node_types::resolve_document_node_type;
use crate::messages::portfolio::document::{graph_operation::utility_types::TransformIn, overlays::utility_types::OverlayContext};
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
@@ -6,9 +7,8 @@ use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::resize::Resize;
use crate::messages::tool::common_functionality::snapping::SnapData;
use graph_craft::document::NodeId;
use graph_craft::document::{value::TaggedValue, NodeId, NodeInput};
use graphene_core::uuid::generate_uuid;
use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::Color;
#[derive(Default)]
@@ -205,11 +205,20 @@ impl Fsm for RectangleToolFsmState {
(RectangleToolFsmState::Ready, RectangleToolMessage::DragStart) => {
shape_data.start(document, input);
let subpath = bezier_rs::Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
responses.add(DocumentMessage::StartTransaction);
let layer = graph_modification_utils::new_vector_layer(vec![subpath], NodeId(generate_uuid()), document.new_layer_parent(true), responses);
let nodes = {
let node_type = resolve_document_node_type("Rectangle").expect("Rectangle node does not exist");
let node = node_type.to_document_node_default_inputs(
[None, Some(NodeInput::value(TaggedValue::F64(1.), false)), Some(NodeInput::value(TaggedValue::F64(1.), false))],
Default::default(),
);
HashMap::from([(NodeId(0), node)])
};
let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, document.new_layer_parent(true), responses);
tool_options.fill.apply_fill(layer, responses);
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
shape_data.layer = Some(layer);
responses.add(GraphOperationMessage::TransformSet {
@@ -219,22 +228,19 @@ impl Fsm for RectangleToolFsmState {
skip_rerender: false,
});
let fill_color = tool_options.fill.active_color();
responses.add(GraphOperationMessage::FillSet {
layer,
fill: if let Some(color) = fill_color { Fill::Solid(color) } else { Fill::None },
});
responses.add(GraphOperationMessage::StrokeSet {
layer,
stroke: Stroke::new(tool_options.stroke.active_color(), tool_options.line_weight),
});
RectangleToolFsmState::Drawing
}
(RectangleToolFsmState::Drawing, RectangleToolMessage::PointerMove { center, lock_ratio }) => {
if let Some(message) = shape_data.calculate_transform(document, input, center, lock_ratio, false) {
responses.add(message);
if let Some([start, end]) = shape_data.calculate_points(document, input, center, lock_ratio) {
if let Some(layer) = shape_data.layer {
// TODO: make the scale impact the rect node
responses.add(GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_scale_angle_translation(end - start, 0., (start + end) / 2.),
transform_in: TransformIn::Viewport,
skip_rerender: false,
});
}
}
// Auto-panning
@@ -529,7 +529,7 @@ impl Fsm for SelectToolFsmState {
.unwrap_or_default();
let mut selected: Vec<_> = document.selected_nodes.selected_visible_and_unlocked_layers(document.metadata()).collect();
let intersection_list = document.click_list(input.mouse.position, &document.network);
let intersection_list = document.click_list(input.mouse.position, &document.network).collect::<Vec<_>>();
let intersection = document.find_deepest(&intersection_list, &document.network);
// If the user is dragging the bounding box bounds, go into ResizingBounds mode.
@@ -1264,7 +1264,7 @@ fn edit_layer_deepest_manipulation(layer: LayerNodeIdentifier, document_network:
if is_layer_fed_by_node_of_name(layer, document_network, "Text") {
responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Text });
responses.add(TextToolMessage::EditSelected);
} else if is_layer_fed_by_node_of_name(layer, document_network, "Shape") {
} else if is_layer_fed_by_node_of_name(layer, document_network, "Path") {
responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Path });
}
}
@@ -1,15 +1,14 @@
use super::tool_prelude::*;
use crate::consts::DRAG_THRESHOLD;
use crate::messages::portfolio::document::graph_operation::utility_types::VectorDataModification;
use crate::messages::portfolio::document::node_graph::document_node_types::resolve_document_node_type;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::snapping::SnapManager;
use graph_craft::document::NodeId;
use graph_craft::document::{value::TaggedValue, NodeId, NodeInput};
use graphene_core::uuid::generate_uuid;
use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::Color;
#[derive(Default)]
@@ -217,17 +216,15 @@ impl Fsm for SplineToolFsmState {
tool_data.weight = tool_options.line_weight;
let layer = graph_modification_utils::new_vector_layer(vec![], NodeId(generate_uuid()), parent, responses);
let nodes = {
let node_type = resolve_document_node_type("Spline").expect("Spline node does not exist");
let node = node_type.to_document_node_default_inputs([None, Some(NodeInput::value(TaggedValue::VecDVec2(Vec::new()), false))], Default::default());
responses.add(GraphOperationMessage::FillSet {
layer,
fill: if let Some(color) = tool_options.fill.active_color() { Fill::Solid(color) } else { Fill::None },
});
responses.add(GraphOperationMessage::StrokeSet {
layer,
stroke: Stroke::new(tool_options.stroke.active_color(), tool_data.weight),
});
HashMap::from([(NodeId(0), node)])
};
let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, parent, responses);
tool_options.fill.apply_fill(layer, responses);
tool_options.stroke.apply_stroke(tool_data.weight, layer, responses);
tool_data.layer = Some(layer);
SplineToolFsmState::Drawing
@@ -247,7 +244,7 @@ impl Fsm for SplineToolFsmState {
}
}
update_spline(tool_data, true, responses);
update_spline(document, tool_data, true, responses);
SplineToolFsmState::Drawing
}
@@ -260,7 +257,7 @@ impl Fsm for SplineToolFsmState {
let pos = transform.inverse().transform_point2(snapped_position);
tool_data.next_point = pos;
update_spline(tool_data, true, responses);
update_spline(document, tool_data, true, responses);
// Auto-panning
let messages = [SplineToolMessage::PointerOutsideViewport.into(), SplineToolMessage::PointerMove.into()];
@@ -283,7 +280,7 @@ impl Fsm for SplineToolFsmState {
}
(SplineToolFsmState::Drawing, SplineToolMessage::Confirm | SplineToolMessage::Abort) => {
if tool_data.points.len() >= 2 {
update_spline(tool_data, false, responses);
update_spline(document, tool_data, false, responses);
responses.add(DocumentMessage::CommitTransaction);
} else {
responses.add(DocumentMessage::AbortTransaction);
@@ -324,20 +321,17 @@ impl Fsm for SplineToolFsmState {
}
}
fn update_spline(tool_data: &SplineToolData, show_preview: bool, responses: &mut VecDeque<Message>) {
fn update_spline(document: &DocumentMessageHandler, tool_data: &SplineToolData, show_preview: bool, responses: &mut VecDeque<Message>) {
let mut points = tool_data.points.clone();
if show_preview {
points.push(tool_data.next_point)
}
let value = TaggedValue::VecDVec2(points);
let subpath = bezier_rs::Subpath::new_cubic_spline(points);
let Some(layer) = tool_data.layer else { return };
let Some(layer) = tool_data.layer else {
let Some(node_id) = graph_modification_utils::NodeGraphLayer::new(layer, document.network()).upstream_node_id_from_name("Spline") else {
return;
};
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 });
responses.add_front(NodeGraphMessage::SetInputValue { node_id, input_index: 1, value });
}
@@ -313,25 +313,6 @@ impl TextToolData {
TextToolFsmState::Ready
}
}
fn get_bounds(&self, text: &str, font_cache: &FontCache) -> Option<[DVec2; 2]> {
let editing_text = self.editing_text.as_ref()?;
let buzz_face = font_cache.get(&editing_text.font).map(|data| load_face(data));
let subpaths = graphene_core::text::to_path(text, buzz_face, editing_text.font_size, None);
let bounds = subpaths.iter().filter_map(|subpath| subpath.bounding_box());
let combined_bounds = bounds.reduce(|a, b| [a[0].min(b[0]), a[1].max(b[1])]).unwrap_or_default();
Some(combined_bounds)
}
fn fix_text_bounds(&self, new_text: &str, _document: &DocumentMessageHandler, font_cache: &FontCache, responses: &mut VecDeque<Message>) -> Option<()> {
responses.add(GraphOperationMessage::UpdateBounds {
layer: self.layer,
old_bounds: self.get_bounds(&self.editing_text.as_ref()?.text, font_cache)?,
new_bounds: self.get_bounds(new_text, font_cache)?,
});
Some(())
}
}
fn can_edit_selected(document: &DocumentMessageHandler) -> Option<LayerNodeIdentifier> {
@@ -429,7 +410,6 @@ impl Fsm for TextToolFsmState {
TextToolFsmState::Editing
}
(TextToolFsmState::Editing, TextToolMessage::TextChange { new_text }) => {
tool_data.fix_text_bounds(&new_text, document, font_cache, responses);
responses.add(NodeGraphMessage::SetQualifiedInputValue {
node_id: graph_modification_utils::get_text_id(tool_data.layer, &document.network).unwrap(),
input_index: 1,
@@ -2,7 +2,6 @@ use crate::consts::SLOWING_DIVISOR;
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
use crate::messages::portfolio::document::utility_types::transformation::{Axis, OriginalTransforms, Selected, TransformOperation, Typing};
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::utility_types::{ToolData, ToolType};
@@ -69,16 +68,12 @@ impl<'a> MessageHandler<TransformLayerMessage, TransformData<'a>> for TransformL
}
if using_path_tool {
if let Some(subpaths) = selected_layers.first().and_then(|&layer| graph_modification_utils::get_subpaths(layer, &document.network)) {
if let Some(vector_data) = selected_layers.first().and_then(|&layer| document.metadata.compute_modified_vector(layer, &document.network)) {
*selected.original_transforms = OriginalTransforms::default();
let viewspace = document.metadata().transform_to_viewport(selected_layers[0]);
let mut point_count: usize = 0;
let get_location = |point: &ManipulatorPointId| {
graph_modification_utils::get_manipulator_from_id(subpaths, point.group)
.and_then(|manipulator_group| point.manipulator_type.get_position(manipulator_group))
.map(|position| viewspace.transform_point2(position))
};
let get_location = |point: &ManipulatorPointId| point.get_position(&vector_data).map(|position| viewspace.transform_point2(position));
let points = shape_editor.selected_points();
*selected.pivot = points.filter_map(get_location).inspect(|_| point_count += 1).sum::<DVec2>() / point_count as f64;