mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-29 09:08:11 +08:00
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:
co-authored by
Keavon Chambers
Adam
Dennis Kobert
parent
fd3613018a
commit
1652c713a6
@@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user