Implement merging pairs of {paths, splines} with the Pen and Spline tools (#2292)

* refactor

* impl find_spline function

* impl merge_layers() to merge two spline layer to one spline layer

* impl merging spline to another spline which are not in the same layer

* impl merging of spline with path

* impl merge spline start endpoint and last endpoint

* fix naming

* fix handle transformation

* refactor

* fix merging with path with only one segment

* refactor

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Priyanshu
2025-02-21 17:43:49 -08:00
committed by GitHub
co-authored by Keavon Chambers
parent fb13d58767
commit 674db97dc4
2 changed files with 211 additions and 617 deletions
@@ -10,34 +10,62 @@ use graphene_core::raster::image::ImageFrame;
use graphene_core::raster::BlendMode; use graphene_core::raster::BlendMode;
use graphene_core::text::{Font, TypesettingConfig}; use graphene_core::text::{Font, TypesettingConfig};
use graphene_core::vector::style::Gradient; use graphene_core::vector::style::Gradient;
use graphene_core::vector::PointId;
use graphene_core::Color; use graphene_core::Color;
use graphene_std::vector::{ManipulatorPointId, PointId, SegmentId, VectorModificationType};
use glam::DVec2; use glam::DVec2;
use std::collections::VecDeque; use std::collections::VecDeque;
pub fn merge_layers(document: &DocumentMessageHandler, current_layer: LayerNodeIdentifier, other_layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) { /// Returns the ID of the first Spline node in the horizontal flow which is not followed by a `Path` node, or `None` if none exists.
pub fn find_spline(document: &DocumentMessageHandler, layer: LayerNodeIdentifier) -> Option<NodeId> {
document
.network_interface
.upstream_flow_back_from_nodes([layer.to_node()].to_vec(), &[], FlowType::HorizontalFlow)
.map(|node_id| (document.network_interface.reference(&node_id, &[]).unwrap(), node_id))
.take_while(|(reference, _)| reference.as_ref().is_some_and(|node_ref| node_ref != "Path"))
.find(|(reference, _)| reference.as_ref().is_some_and(|node_ref| node_ref == "Spline"))
.map(|node| node.1)
}
/// Merge `second_layer` to the `first_layer`.
pub fn merge_layers(document: &DocumentMessageHandler, first_layer: LayerNodeIdentifier, second_layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
if first_layer == second_layer {
return;
}
// Calculate the downstream transforms in order to bring the other vector data into the same layer space // Calculate the downstream transforms in order to bring the other vector data into the same layer space
let current_transform = document.metadata().downstream_transform_to_document(current_layer); let first_layer_transform = document.metadata().downstream_transform_to_document(first_layer);
let other_transform = document.metadata().downstream_transform_to_document(other_layer); let second_layer_transform = document.metadata().downstream_transform_to_document(second_layer);
// Represents the change in position that would occur if the other layer was moved below the current layer // Represents the change in position that would occur if the other layer was moved below the current layer
let transform_delta = current_transform * other_transform.inverse(); let transform_delta = first_layer_transform * second_layer_transform.inverse();
let offset = transform_delta.inverse(); let offset = transform_delta.inverse();
responses.add(GraphOperationMessage::TransformChange { responses.add(GraphOperationMessage::TransformChange {
layer: other_layer, layer: second_layer,
transform: offset, transform: offset,
transform_in: TransformIn::Local, transform_in: TransformIn::Local,
skip_rerender: false, skip_rerender: false,
}); });
// Move the other layer below the current layer for positioning purposes let mut current_and_other_layer_is_spline = false;
let current_layer_parent = current_layer.parent(document.metadata()).unwrap();
let current_layer_index = current_layer_parent.children(document.metadata()).position(|child| child == current_layer).unwrap(); match (find_spline(document, first_layer), find_spline(document, second_layer)) {
(Some(current_layer_spline), Some(other_layer_spline)) => {
responses.add(NodeGraphMessage::DeleteNodes {
node_ids: [current_layer_spline, other_layer_spline].to_vec(),
delete_children: false,
});
current_and_other_layer_is_spline = true;
}
_ => {}
}
// Move the `second_layer` below the `first_layer` for positioning purposes
let first_layer_parent = first_layer.parent(document.metadata()).unwrap();
let first_layer_index = first_layer_parent.children(document.metadata()).position(|child| child == first_layer).unwrap();
responses.add(NodeGraphMessage::MoveLayerToStack { responses.add(NodeGraphMessage::MoveLayerToStack {
layer: other_layer, layer: second_layer,
parent: current_layer_parent, parent: first_layer_parent,
insert_index: current_layer_index + 1, insert_index: first_layer_index + 1,
}); });
// Merge the inputs of the two layers // Merge the inputs of the two layers
@@ -55,14 +83,14 @@ pub fn merge_layers(document: &DocumentMessageHandler, current_layer: LayerNodeI
}); });
responses.add(NodeGraphMessage::MoveNodeToChainStart { responses.add(NodeGraphMessage::MoveNodeToChainStart {
node_id: merge_node_id, node_id: merge_node_id,
parent: current_layer, parent: first_layer,
}); });
responses.add(NodeGraphMessage::ConnectUpstreamOutputToInput { responses.add(NodeGraphMessage::ConnectUpstreamOutputToInput {
downstream_input: InputConnector::node(other_layer.to_node(), 1), downstream_input: InputConnector::node(second_layer.to_node(), 1),
input_connector: InputConnector::node(merge_node_id, 1), input_connector: InputConnector::node(merge_node_id, 1),
}); });
responses.add(NodeGraphMessage::DeleteNodes { responses.add(NodeGraphMessage::DeleteNodes {
node_ids: vec![other_layer.to_node()], node_ids: vec![second_layer.to_node()],
delete_children: false, delete_children: false,
}); });
@@ -77,7 +105,7 @@ pub fn merge_layers(document: &DocumentMessageHandler, current_layer: LayerNodeI
}); });
responses.add(NodeGraphMessage::MoveNodeToChainStart { responses.add(NodeGraphMessage::MoveNodeToChainStart {
node_id: flatten_node_id, node_id: flatten_node_id,
parent: current_layer, parent: first_layer,
}); });
// Add a path node after the flatten node // Add a path node after the flatten node
@@ -91,9 +119,25 @@ pub fn merge_layers(document: &DocumentMessageHandler, current_layer: LayerNodeI
}); });
responses.add(NodeGraphMessage::MoveNodeToChainStart { responses.add(NodeGraphMessage::MoveNodeToChainStart {
node_id: path_node_id, node_id: path_node_id,
parent: current_layer, parent: first_layer,
}); });
// Add a Spline node after the Path node if both the layers we are merging is spline.
if current_and_other_layer_is_spline {
let spline_node_id = NodeId::new();
let spline_node = document_node_definitions::resolve_document_node_type("Spline")
.expect("Failed to create Spline node")
.default_node_template();
responses.add(NodeGraphMessage::InsertNode {
node_id: spline_node_id,
node_template: spline_node,
});
responses.add(NodeGraphMessage::MoveNodeToChainStart {
node_id: spline_node_id,
parent: first_layer,
});
}
// Add a transform node to ensure correct tooling modifications // Add a transform node to ensure correct tooling modifications
let transform_node_id = NodeId::new(); let transform_node_id = NodeId::new();
let transform_node = document_node_definitions::resolve_document_node_type("Transform") let transform_node = document_node_definitions::resolve_document_node_type("Transform")
@@ -105,7 +149,7 @@ pub fn merge_layers(document: &DocumentMessageHandler, current_layer: LayerNodeI
}); });
responses.add(NodeGraphMessage::MoveNodeToChainStart { responses.add(NodeGraphMessage::MoveNodeToChainStart {
node_id: transform_node_id, node_id: transform_node_id,
parent: current_layer, parent: first_layer,
}); });
responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::RunDocumentGraph);
@@ -113,6 +157,49 @@ pub fn merge_layers(document: &DocumentMessageHandler, current_layer: LayerNodeI
responses.add(PenToolMessage::RecalculateLatestPointsPosition); responses.add(PenToolMessage::RecalculateLatestPointsPosition);
} }
/// Merge the `first_endpoint` with `second_endpoint`.
pub fn merge_points(document: &DocumentMessageHandler, layer: LayerNodeIdentifier, first_endpoint: PointId, second_endpont: PointId, responses: &mut VecDeque<Message>) {
let transform = document.metadata().transform_to_document(layer);
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { return };
let segment = vector_data.segment_bezier_iter().find(|(_, _, start, end)| *end == second_endpont || *start == second_endpont);
let Some((segment, _, mut segment_start_point, mut segment_end_point)) = segment else {
log::error!("Could not get the segment for second_endpoint.");
return;
};
let mut handles = [None; 2];
if let Some(handle_position) = ManipulatorPointId::PrimaryHandle(segment).get_position(&vector_data) {
let anchor_position = ManipulatorPointId::Anchor(segment_start_point).get_position(&vector_data).unwrap();
let handle_position = transform.transform_point2(handle_position);
let anchor_position = transform.transform_point2(anchor_position);
let anchor_to_handle = handle_position - anchor_position;
handles[0] = Some(anchor_to_handle);
}
if let Some(handle_position) = ManipulatorPointId::EndHandle(segment).get_position(&vector_data) {
let anchor_position = ManipulatorPointId::Anchor(segment_end_point).get_position(&vector_data).unwrap();
let handle_position = transform.transform_point2(handle_position);
let anchor_position = transform.transform_point2(anchor_position);
let anchor_to_handle = handle_position - anchor_position;
handles[1] = Some(anchor_to_handle);
}
if segment_start_point == second_endpont {
core::mem::swap(&mut segment_start_point, &mut segment_end_point);
handles.reverse();
}
let modification_type = VectorModificationType::RemovePoint { id: second_endpont };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
let modification_type = VectorModificationType::RemoveSegment { id: segment };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
let points = [segment_start_point, first_endpoint];
let id = SegmentId::generate();
let modification_type = VectorModificationType::InsertSegment { id, points, handles };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
}
/// Create a new vector layer. /// Create a new vector layer.
pub fn new_vector_layer(subpaths: Vec<Subpath<PointId>>, 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 = 0; let insert_index = 0;
@@ -1,14 +1,12 @@
use super::tool_prelude::*; use super::tool_prelude::*;
use crate::consts::{DEFAULT_STROKE_WIDTH, DRAG_THRESHOLD, PATH_JOIN_THRESHOLD, SNAP_POINT_TOLERANCE}; use crate::consts::{DEFAULT_STROKE_WIDTH, DRAG_THRESHOLD, PATH_JOIN_THRESHOLD, SNAP_POINT_TOLERANCE};
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::node_graph::document_node_definitions::{self, resolve_document_node_type};
use crate::messages::portfolio::document::overlays::utility_functions::path_endpoint_overlays; 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::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector};
use crate::messages::tool::common_functionality::auto_panning::AutoPanning; 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::color_selector::{ToolColorOptions, ToolColorType};
use crate::messages::tool::common_functionality::graph_modification_utils::{self}; use crate::messages::tool::common_functionality::graph_modification_utils::{self, find_spline, merge_layers, merge_points};
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapData, SnapManager, SnapTypeConfiguration, SnappedPoint}; use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapData, SnapManager, SnapTypeConfiguration, SnappedPoint};
use crate::messages::tool::common_functionality::utility_functions::{closest_point, should_extend}; use crate::messages::tool::common_functionality::utility_functions::{closest_point, should_extend};
@@ -52,6 +50,7 @@ pub enum SplineToolMessage {
Confirm, Confirm,
DragStart { append_to_selected: Key }, DragStart { append_to_selected: Key },
DragStop, DragStop,
MergeEndpoints,
PointerMove, PointerMove,
PointerOutsideViewport, PointerOutsideViewport,
Undo, Undo,
@@ -63,6 +62,7 @@ enum SplineToolFsmState {
#[default] #[default]
Ready, Ready,
Drawing, Drawing,
MergingEndpoints,
} }
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
@@ -168,6 +168,9 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for SplineT
Confirm, Confirm,
Abort, Abort,
), ),
SplineToolFsmState::MergingEndpoints => actions!(SplineToolMessageDiscriminant;
MergeEndpoints,
),
} }
} }
} }
@@ -196,26 +199,31 @@ struct SplineToolData {
preview_segment: Option<SegmentId>, preview_segment: Option<SegmentId>,
extend: bool, extend: bool,
weight: f64, weight: f64,
layer: Option<LayerNodeIdentifier>, /// The layer we are editing.
starting_layer: Option<LayerNodeIdentifier>, current_layer: Option<LayerNodeIdentifier>,
/// The layer and endpoint to merge with the spline's starting endpoint after drawing is complete.
start_merge_layer: Option<(LayerNodeIdentifier, PointId)>,
/// The endpoint pairs to merge after we have finished drawing the spline and merged the layers.
merge_endpoints: Vec<(PointId, PointId)>,
snap_manager: SnapManager, snap_manager: SnapManager,
auto_panning: AutoPanning, auto_panning: AutoPanning,
} }
impl SplineToolData { impl SplineToolData {
fn cleanup(&mut self) { fn cleanup(&mut self) {
self.layer = None; self.current_layer = None;
self.start_merge_layer = None;
self.merge_endpoints = Vec::new();
self.preview_point = None; self.preview_point = None;
self.preview_segment = None; self.preview_segment = None;
self.extend = false; self.extend = false;
self.points = Vec::new(); self.points = Vec::new();
self.starting_layer = None;
} }
/// Get the snapped point while ignoring current layer /// Get the snapped point while ignoring current layer
fn snapped_point(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler) -> SnappedPoint { fn snapped_point(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler) -> SnappedPoint {
let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position)); let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position));
let ignore = if let Some(layer) = self.layer { vec![layer] } else { vec![] }; let ignore = if let Some(layer) = self.current_layer { vec![layer] } else { vec![] };
let snap_data = SnapData::ignore(document, input, &ignore); let snap_data = SnapData::ignore(document, input, &ignore);
self.snap_manager.free_snap(&snap_data, &point, SnapTypeConfiguration::default()) self.snap_manager.free_snap(&snap_data, &point, SnapTypeConfiguration::default())
} }
@@ -243,9 +251,22 @@ impl Fsm for SplineToolFsmState {
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
self self
} }
(SplineToolFsmState::MergingEndpoints, SplineToolMessage::MergeEndpoints) => {
let Some(layer) = tool_data.current_layer else { return SplineToolFsmState::Ready };
if let Some((first, second)) = tool_data.merge_endpoints.pop() {
merge_points(document, layer, first, second, responses);
responses.add(SplineToolMessage::MergeEndpoints);
return SplineToolFsmState::MergingEndpoints;
}
responses.add(DocumentMessage::EndTransaction);
SplineToolFsmState::Ready
}
(SplineToolFsmState::Ready, SplineToolMessage::DragStart { append_to_selected }) => { (SplineToolFsmState::Ready, SplineToolMessage::DragStart { append_to_selected }) => {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
tool_data.snap_manager.cleanup(responses);
tool_data.cleanup(); tool_data.cleanup();
tool_data.weight = tool_options.line_weight; tool_data.weight = tool_options.line_weight;
@@ -253,38 +274,38 @@ impl Fsm for SplineToolFsmState {
let snapped = tool_data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default()); let snapped = tool_data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default());
let viewport = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document); let viewport = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document);
// Check if we're starting from an endpoint of any layer, even if not extending let layers = LayerNodeIdentifier::ROOT_PARENT
let closest_endpoint = closest_point( .descendants(document.metadata())
document, .filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]));
viewport,
PATH_JOIN_THRESHOLD,
LayerNodeIdentifier::ROOT_PARENT.descendants(document.metadata()),
|_| false, // Don't exclude any points
preferences,
);
if let Some((start_layer, _, _)) = closest_endpoint {
tool_data.starting_layer = Some(start_layer);
}
// Extend an endpoint of the selected path // Extend an endpoint of the selected path
let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap(); if let Some((layer, point, position)) = should_extend(document, viewport, SNAP_POINT_TOLERANCE, layers, preferences) {
if let Some((layer, point, position)) = should_extend(document, viewport, SNAP_POINT_TOLERANCE, selected_nodes.selected_layers(document.metadata()), preferences) { if find_spline(document, layer).is_some() {
tool_data.layer = Some(layer); // If the point is the part of Spline then we extend it.
tool_data.points.push((point, position)); tool_data.current_layer = Some(layer);
tool_data.next_point = position; tool_data.points.push((point, position));
tool_data.extend = true; tool_data.next_point = position;
tool_data.extend = true;
extend_spline(tool_data, true, responses); extend_spline(tool_data, true, responses);
return SplineToolFsmState::Drawing; return SplineToolFsmState::Drawing;
} else {
// Otherwise we keep the layer identifier and endpoint identifier to merge it with the spline later when we finish drawing the spline.
tool_data.start_merge_layer = Some((layer, point));
}
} }
// Create new path in the same layer when shift is down let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap();
if input.keyboard.key(append_to_selected) { let mut selected_layers_except_artboards = selected_nodes.selected_layers_except_artboards(&document.network_interface);
let mut selected_layers_except_artboards = selected_nodes.selected_layers_except_artboards(&document.network_interface); let selected_layer = selected_layers_except_artboards.next().filter(|_| selected_layers_except_artboards.next().is_none());
let existing_layer = selected_layers_except_artboards.next().filter(|_| selected_layers_except_artboards.next().is_none());
if let Some(layer) = existing_layer { let append_to_selected_layer = input.keyboard.key(append_to_selected);
tool_data.layer = Some(layer);
// Create new path in the selected layer when shift is down
match (selected_layer, append_to_selected_layer) {
(Some(layer), true) => {
tool_data.current_layer = Some(layer);
let transform = document.metadata().transform_to_viewport(layer); let transform = document.metadata().transform_to_viewport(layer);
let position = transform.inverse().transform_point2(input.mouse.position); let position = transform.inverse().transform_point2(input.mouse.position);
@@ -292,6 +313,7 @@ impl Fsm for SplineToolFsmState {
return SplineToolFsmState::Drawing; return SplineToolFsmState::Drawing;
} }
_ => {}
} }
responses.add(DocumentMessage::DeselectAllLayers); responses.add(DocumentMessage::DeselectAllLayers);
@@ -307,7 +329,7 @@ impl Fsm for SplineToolFsmState {
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses); let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
tool_options.fill.apply_fill(layer, responses); tool_options.fill.apply_fill(layer, responses);
tool_options.stroke.apply_stroke(tool_data.weight, layer, responses); tool_options.stroke.apply_stroke(tool_data.weight, layer, responses);
tool_data.layer = Some(layer); tool_data.current_layer = Some(layer);
responses.add(Message::StartBuffer); responses.add(Message::StartBuffer);
@@ -319,13 +341,9 @@ impl Fsm for SplineToolFsmState {
tool_data.extend = false; tool_data.extend = false;
return SplineToolFsmState::Drawing; return SplineToolFsmState::Drawing;
} }
if tool_data.layer.is_none() { if tool_data.current_layer.is_none() {
return SplineToolFsmState::Ready; return SplineToolFsmState::Ready;
}; };
if join_path(document, input.mouse.position, tool_data, preferences, responses) {
responses.add(DocumentMessage::EndTransaction);
return SplineToolFsmState::Ready;
}
tool_data.next_point = tool_data.snapped_point(document, input).snapped_point_document; tool_data.next_point = tool_data.snapped_point(document, input).snapped_point_document;
if tool_data.points.last().map_or(true, |last_pos| last_pos.1.distance(tool_data.next_point) > DRAG_THRESHOLD) { if tool_data.points.last().map_or(true, |last_pos| last_pos.1.distance(tool_data.next_point) > DRAG_THRESHOLD) {
extend_spline(tool_data, false, responses); extend_spline(tool_data, false, responses);
@@ -334,7 +352,7 @@ impl Fsm for SplineToolFsmState {
SplineToolFsmState::Drawing SplineToolFsmState::Drawing
} }
(SplineToolFsmState::Drawing, SplineToolMessage::PointerMove) => { (SplineToolFsmState::Drawing, SplineToolMessage::PointerMove) => {
let Some(layer) = tool_data.layer else { return SplineToolFsmState::Ready }; let Some(layer) = tool_data.current_layer else { return SplineToolFsmState::Ready };
let ignore = |cp: PointId| tool_data.preview_point.is_some_and(|pp| pp == cp) || tool_data.points.last().is_some_and(|(ep, _)| *ep == cp); let ignore = |cp: PointId| tool_data.preview_point.is_some_and(|pp| pp == cp) || tool_data.points.last().is_some_and(|(ep, _)| *ep == cp);
let join_point = closest_point(document, input.mouse.position, PATH_JOIN_THRESHOLD, vec![layer].into_iter(), ignore, preferences); let join_point = closest_point(document, input.mouse.position, PATH_JOIN_THRESHOLD, vec![layer].into_iter(), ignore, preferences);
@@ -374,17 +392,16 @@ impl Fsm for SplineToolFsmState {
state state
} }
(SplineToolFsmState::Drawing, SplineToolMessage::Confirm | SplineToolMessage::Abort) => { (SplineToolFsmState::Drawing, SplineToolMessage::Confirm) => {
if tool_data.points.len() >= 2 { if tool_data.points.len() >= 2 {
delete_preview(tool_data, responses); delete_preview(tool_data, responses);
responses.add(DocumentMessage::EndTransaction); merge_paths_layer(document, tool_data, preferences, responses);
} else {
responses.add(DocumentMessage::AbortTransaction);
} }
responses.add(SplineToolMessage::MergeEndpoints);
tool_data.snap_manager.cleanup(responses); SplineToolFsmState::MergingEndpoints
tool_data.cleanup(); }
(SplineToolFsmState::Drawing, SplineToolMessage::Abort) => {
responses.add(DocumentMessage::AbortTransaction);
SplineToolFsmState::Ready SplineToolFsmState::Ready
} }
(_, SplineToolMessage::WorkingColorChanged) => { (_, SplineToolMessage::WorkingColorChanged) => {
@@ -409,6 +426,7 @@ impl Fsm for SplineToolFsmState {
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Extend Spline")]), HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Extend Spline")]),
HintGroup(vec![HintInfo::keys([Key::Enter], "End Spline")]), HintGroup(vec![HintInfo::keys([Key::Enter], "End Spline")]),
]), ]),
SplineToolFsmState::MergingEndpoints => HintData(vec![]),
}; };
responses.add(FrontendMessage::UpdateInputHints { hint_data }); responses.add(FrontendMessage::UpdateInputHints { hint_data });
@@ -419,144 +437,56 @@ impl Fsm for SplineToolFsmState {
} }
} }
/// Return `true` only if new segment is inserted to connect two end points in the selected layer otherwise `false`. fn merge_paths_layer(document: &DocumentMessageHandler, tool_data: &mut SplineToolData, preferences: &PreferencesMessageHandler, responses: &mut VecDeque<Message>) {
fn join_path(document: &DocumentMessageHandler, mouse_pos: DVec2, tool_data: &mut SplineToolData, preferences: &PreferencesMessageHandler, responses: &mut VecDeque<Message>) -> bool { if tool_data.points.len() < 2 {
let Some(&(endpoint, _)) = tool_data.points.last() else { return false }; return;
let Some(&(start_point, _)) = tool_data.points.first() else { return false }; };
let Some(starting_layer) = tool_data.starting_layer else { return false }; let (last_endpoint, last_endpoint_position) = tool_data.points.last().unwrap();
let Some(current_layer) = tool_data.layer else { return false }; let (start_endpoint, _) = tool_data.points.first().unwrap();
let preview_point = tool_data.preview_point; let preview_point = tool_data.preview_point;
let Some(current_layer) = tool_data.current_layer else { return };
// Get the closest point to mouse position which is not preview_point or end_point. let layers = LayerNodeIdentifier::ROOT_PARENT
let closest_point = closest_point( .descendants(document.metadata())
document, .filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]));
mouse_pos,
PATH_JOIN_THRESHOLD,
LayerNodeIdentifier::ROOT_PARENT.descendants(document.metadata()),
|cp| preview_point.is_some_and(|pp| pp == cp) || cp == endpoint,
preferences,
);
let Some((other_layer, join_point, _)) = closest_point else { return false };
// Last end point inserted was the preview point and segment therefore we delete it before joining the end_point & join_point. let exclude = |p: PointId| preview_point.is_some_and(|pp| pp == p) || *last_endpoint == p;
delete_preview(tool_data, responses); let position = document.metadata().transform_to_viewport(current_layer).transform_point2(*last_endpoint_position);
// If the points are in different layers, merge them first let start_merge_layer = tool_data.start_merge_layer;
if current_layer == other_layer { let end_merge_layer = closest_point(document, position, PATH_JOIN_THRESHOLD, layers, exclude, preferences);
// If points are in the same layer, just connect them
let points = [endpoint, join_point];
let id = SegmentId::generate();
let modification_type = VectorModificationType::InsertSegment { id, points, handles: [None, None] };
responses.add(GraphOperationMessage::Vector {
layer: current_layer,
modification_type,
});
return true; let mut endpoints_to_merge = [None, None];
match (start_merge_layer, end_merge_layer) {
(Some(first), Some(last)) => {
merge_layers(document, first.0, last.0, responses);
merge_layers(document, current_layer, first.0, responses);
endpoints_to_merge = [first.1, last.1].map(|p| Some(p));
}
(Some(first), None) => {
endpoints_to_merge[0] = Some(first.1);
merge_layers(document, current_layer, first.0, responses);
}
(None, Some(last)) => {
endpoints_to_merge[1] = Some(last.1);
merge_layers(document, current_layer, last.0, responses);
}
(None, None) => return,
} }
match (is_layer_spline(document, starting_layer), is_layer_spline(document, other_layer)) { if let Some(merge_endpoint) = endpoints_to_merge[0] {
(true, true) => { tool_data.merge_endpoints.push((*start_endpoint, merge_endpoint));
merge_two_spline_layer(document, current_layer, other_layer, responses); }
let points = [endpoint, join_point]; if let Some(merge_endpoint) = endpoints_to_merge[1] {
let id = SegmentId::generate(); tool_data.merge_endpoints.push((*last_endpoint, merge_endpoint));
let modification_type = VectorModificationType::InsertSegment { id, points, handles: [None, None] };
responses.add(GraphOperationMessage::Vector {
layer: current_layer,
modification_type,
});
}
(false, false) => {
let Some(current_vector_data) = document.network_interface.compute_modified_vector(current_layer) else {
log::error!("Could not get vector data for current layer");
return false;
};
let Some(starting_vector_data) = document.network_interface.compute_modified_vector(starting_layer) else {
log::error!("Could not get vector data for other layer");
return false;
};
let Some(starting_layer_endpoint) = starting_vector_data.end_point().last() else {
log::error!("Could not get endpoint");
return false;
};
let handles = (0..current_vector_data.segment_domain.handles().len())
.find_map(|index| {
let (start_id, end_id, bezier) = current_vector_data.segment_points_from_index(index);
if start_id == endpoint {
Some([bezier.handles.start(), bezier.handles.end()])
} else if end_id == endpoint {
// Reverse the handles if connecting to end point
Some([bezier.handles.end(), bezier.handles.start()])
} else {
None
}
})
.unwrap();
// Merge the layers first
merge_non_spline_layers(document, starting_layer, current_layer, other_layer, responses);
let points = [endpoint, join_point];
let points2 = [start_point, starting_layer_endpoint];
let id = SegmentId::generate();
let modification_type = VectorModificationType::InsertSegment { id, points, handles };
responses.add(GraphOperationMessage::Vector {
layer: starting_layer,
modification_type,
});
let id = SegmentId::generate();
let modification_type = VectorModificationType::InsertSegment { id, points: points2, handles };
responses.add(GraphOperationMessage::Vector {
layer: starting_layer,
modification_type,
});
}
_ => {
let current_vector_data = match document.network_interface.compute_modified_vector(current_layer) {
Some(data) => data,
None => {
log::error!("Could not get vector data for current layer");
return false;
}
};
let handles = (0..current_vector_data.segment_domain.handles().len()).find_map(|index| {
let (start_id, end_id, bezier) = current_vector_data.segment_points_from_index(index);
if start_id == endpoint {
Some([bezier.handles.start(), bezier.handles.end()])
} else if end_id == endpoint {
Some([bezier.handles.end(), bezier.handles.start()])
} else {
None
}
});
let Some(handles) = handles else {
log::error!("Could not find handles for endpoint");
return false;
};
merge_path_spline_layer(document, current_layer, other_layer, responses);
let points = [endpoint, join_point];
let id = SegmentId::generate();
responses.add(GraphOperationMessage::Vector {
layer: other_layer,
modification_type: VectorModificationType::InsertSegment { id, points, handles },
});
}
} }
true
} }
fn extend_spline(tool_data: &mut SplineToolData, show_preview: bool, responses: &mut VecDeque<Message>) { fn extend_spline(tool_data: &mut SplineToolData, show_preview: bool, responses: &mut VecDeque<Message>) {
delete_preview(tool_data, responses); delete_preview(tool_data, responses);
let Some(layer) = tool_data.layer else { return }; let Some(layer) = tool_data.current_layer else { return };
let next_point_pos = tool_data.next_point; let next_point_pos = tool_data.next_point;
let next_point_id = PointId::generate(); let next_point_id = PointId::generate();
@@ -585,7 +515,7 @@ fn extend_spline(tool_data: &mut SplineToolData, show_preview: bool, responses:
} }
fn delete_preview(tool_data: &mut SplineToolData, responses: &mut VecDeque<Message>) { fn delete_preview(tool_data: &mut SplineToolData, responses: &mut VecDeque<Message>) {
let Some(layer) = tool_data.layer else { return }; let Some(layer) = tool_data.current_layer else { return };
if let Some(id) = tool_data.preview_point { if let Some(id) = tool_data.preview_point {
let modification_type = VectorModificationType::RemovePoint { id }; let modification_type = VectorModificationType::RemovePoint { id };
@@ -599,426 +529,3 @@ fn delete_preview(tool_data: &mut SplineToolData, responses: &mut VecDeque<Messa
tool_data.preview_point = None; tool_data.preview_point = None;
tool_data.preview_segment = None; tool_data.preview_segment = None;
} }
fn merge_two_spline_layer(document: &DocumentMessageHandler, current_layer: LayerNodeIdentifier, other_layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
// Calculate the downstream transforms in order to bring the other vector data into the same layer space
let current_transform = document.metadata().downstream_transform_to_document(current_layer);
let other_transform = document.metadata().downstream_transform_to_document(other_layer);
// Represents the change in position that would occur if the other layer was moved below the current layer
let transform_delta = current_transform * other_transform.inverse();
let offset = transform_delta.inverse();
responses.add(GraphOperationMessage::TransformChange {
layer: other_layer,
transform: offset,
transform_in: TransformIn::Local,
skip_rerender: false,
});
// First find their IDs
let current_layer_nodes = document
.network_interface
.upstream_flow_back_from_nodes(
vec![current_layer.to_node()],
&[],
crate::messages::portfolio::document::utility_types::network_interface::FlowType::HorizontalFlow,
)
.collect::<Vec<_>>();
let other_layer_nodes = document
.network_interface
.upstream_flow_back_from_nodes(vec![other_layer.to_node()], &[], FlowType::HorizontalFlow)
.collect::<Vec<_>>();
// Add merge node and insert between path and spline
let merge_node_id = NodeId::new();
let merge_node = document_node_definitions::resolve_document_node_type("Merge")
.expect("Failed to create merge node")
.default_node_template();
responses.add(NodeGraphMessage::InsertNode {
node_id: merge_node_id,
node_template: merge_node,
});
responses.add(NodeGraphMessage::SetToNodeOrLayer {
node_id: merge_node_id,
is_layer: false,
});
responses.add(NodeGraphMessage::MoveNodeToChainStart {
node_id: merge_node_id,
parent: current_layer,
});
responses.add(NodeGraphMessage::ConnectUpstreamOutputToInput {
downstream_input: InputConnector::node(other_layer.to_node(), 1),
input_connector: InputConnector::node(merge_node_id, 1),
});
// Add flatten vector elements node after merge
let flatten_node_id = NodeId::new();
let flatten_node = document_node_definitions::resolve_document_node_type("Flatten Vector Elements")
.expect("Failed to create flatten node")
.default_node_template();
responses.add(NodeGraphMessage::InsertNode {
node_id: flatten_node_id,
node_template: flatten_node,
});
responses.add(NodeGraphMessage::MoveNodeToChainStart {
node_id: flatten_node_id,
parent: current_layer,
});
let path_node_id = NodeId::new();
let path_node = document_node_definitions::resolve_document_node_type("Path")
.expect("Failed to create path node")
.default_node_template();
responses.add(NodeGraphMessage::InsertNode {
node_id: path_node_id,
node_template: path_node,
});
responses.add(NodeGraphMessage::MoveNodeToChainStart {
node_id: path_node_id,
parent: current_layer,
});
let spline_node_id = NodeId::new();
let spline_node = document_node_definitions::resolve_document_node_type("Splines from Points")
.expect("Failed to create spline node")
.default_node_template();
responses.add(NodeGraphMessage::InsertNode {
node_id: spline_node_id,
node_template: spline_node,
});
responses.add(NodeGraphMessage::MoveNodeToChainStart {
node_id: spline_node_id,
parent: current_layer,
});
let stroke_node_id = NodeId::new();
let stroke_node = document_node_definitions::resolve_document_node_type("Stroke")
.expect("Failed to create stroke node")
.default_node_template();
responses.add(NodeGraphMessage::InsertNode {
node_id: stroke_node_id,
node_template: stroke_node,
});
responses.add(NodeGraphMessage::MoveNodeToChainStart {
node_id: stroke_node_id,
parent: current_layer,
});
responses.add(NodeGraphMessage::DeleteNodes {
node_ids: current_layer_nodes[1..3].to_vec(),
delete_children: false,
});
responses.add(NodeGraphMessage::DeleteNodes {
node_ids: other_layer_nodes[..3].to_vec(),
delete_children: false,
});
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(Message::StartBuffer);
}
fn merge_non_spline_layers(
document: &DocumentMessageHandler,
starting_layer: LayerNodeIdentifier,
current_layer: LayerNodeIdentifier,
other_layer: LayerNodeIdentifier,
responses: &mut VecDeque<Message>,
) {
// Calculate the downstream transforms in order to bring the other vector data into the same layer space
let current_transform = document.metadata().downstream_transform_to_document(current_layer);
let other_transform = document.metadata().downstream_transform_to_document(other_layer);
// Represents the change in position that would occur if the other layer was moved below the current layer
let transform_delta = current_transform * other_transform.inverse();
let offset = transform_delta.inverse();
responses.add(GraphOperationMessage::TransformChange {
layer: other_layer,
transform: offset,
transform_in: TransformIn::Local,
skip_rerender: false,
});
// Delete spline and stroke nodes from both layers
// First find their IDs
let starting_layer_nodes = document
.network_interface
.upstream_flow_back_from_nodes(
vec![starting_layer.to_node()],
&[],
crate::messages::portfolio::document::utility_types::network_interface::FlowType::HorizontalFlow,
)
.collect::<Vec<_>>();
let current_layer_nodes = document
.network_interface
.upstream_flow_back_from_nodes(
vec![current_layer.to_node()],
&[],
crate::messages::portfolio::document::utility_types::network_interface::FlowType::HorizontalFlow,
)
.collect::<Vec<_>>();
let other_layer_nodes = document
.network_interface
.upstream_flow_back_from_nodes(vec![other_layer.to_node()], &[], FlowType::HorizontalFlow)
.collect::<Vec<_>>();
// Add merge node and insert between path and spline
let merge_node_id = NodeId::new();
let merge_node = document_node_definitions::resolve_document_node_type("Merge")
.expect("Failed to create merge node")
.default_node_template();
responses.add(NodeGraphMessage::InsertNode {
node_id: merge_node_id,
node_template: merge_node,
});
responses.add(NodeGraphMessage::SetToNodeOrLayer {
node_id: merge_node_id,
is_layer: false,
});
responses.add(NodeGraphMessage::MoveNodeToChainStart {
node_id: merge_node_id,
parent: starting_layer,
});
responses.add(NodeGraphMessage::ConnectUpstreamOutputToInput {
downstream_input: InputConnector::node(other_layer.to_node(), 1),
input_connector: InputConnector::node(merge_node_id, 1),
});
let merge_node_id2 = NodeId::new();
let merge_node2 = document_node_definitions::resolve_document_node_type("Merge")
.expect("Failed to create merge node")
.default_node_template();
responses.add(NodeGraphMessage::InsertNode {
node_id: merge_node_id2,
node_template: merge_node2,
});
responses.add(NodeGraphMessage::SetToNodeOrLayer {
node_id: merge_node_id2,
is_layer: false,
});
responses.add(NodeGraphMessage::MoveNodeToChainStart {
node_id: merge_node_id2,
parent: starting_layer,
});
responses.add(NodeGraphMessage::ConnectUpstreamOutputToInput {
downstream_input: InputConnector::node(current_layer.to_node(), 1),
input_connector: InputConnector::node(merge_node_id2, 1),
});
// Add flatten vector elements node after merge
let flatten_node_id2 = NodeId::new();
let flatten_node2 = document_node_definitions::resolve_document_node_type("Flatten Vector Elements")
.expect("Failed to create flatten node")
.default_node_template();
responses.add(NodeGraphMessage::InsertNode {
node_id: flatten_node_id2,
node_template: flatten_node2,
});
responses.add(NodeGraphMessage::MoveNodeToChainStart {
node_id: flatten_node_id2,
parent: starting_layer,
});
let path_node_id = NodeId::new();
let path_node = document_node_definitions::resolve_document_node_type("Path")
.expect("Failed to create path node")
.default_node_template();
responses.add(NodeGraphMessage::InsertNode {
node_id: path_node_id,
node_template: path_node,
});
responses.add(NodeGraphMessage::MoveNodeToChainStart {
node_id: path_node_id,
parent: starting_layer,
});
let stroke_node_id = NodeId::new();
let stroke_node = document_node_definitions::resolve_document_node_type("Stroke")
.expect("Failed to create stroke node")
.default_node_template();
responses.add(NodeGraphMessage::InsertNode {
node_id: stroke_node_id,
node_template: stroke_node,
});
responses.add(NodeGraphMessage::MoveNodeToChainStart {
node_id: stroke_node_id,
parent: starting_layer,
});
responses.add(NodeGraphMessage::DeleteNodes {
node_ids: starting_layer_nodes[1..(if is_layer_line(document, starting_layer) { 2 } else { 3 })].to_vec(),
delete_children: false,
});
responses.add(NodeGraphMessage::DeleteNodes {
node_ids: other_layer_nodes[..(if is_layer_line(document, other_layer) { 2 } else { 3 })].to_vec(),
delete_children: false,
});
responses.add(NodeGraphMessage::DeleteNodes {
node_ids: current_layer_nodes[..2].to_vec(),
delete_children: false,
});
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(Message::StartBuffer);
}
fn merge_path_spline_layer(document: &DocumentMessageHandler, current_layer: LayerNodeIdentifier, other_layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
// Calculate the downstream transforms in order to bring the other vector data into the same layer space
let current_transform = document.metadata().downstream_transform_to_document(current_layer);
let other_transform = document.metadata().downstream_transform_to_document(other_layer);
// Represents the change in position that would occur if the other layer was moved below the current layer
let transform_delta = current_transform * other_transform.inverse();
let offset = transform_delta.inverse();
responses.add(GraphOperationMessage::TransformChange {
layer: other_layer,
transform: offset,
transform_in: TransformIn::Local,
skip_rerender: false,
});
// First find their IDs
let current_layer_nodes = document
.network_interface
.upstream_flow_back_from_nodes(
vec![current_layer.to_node()],
&[],
crate::messages::portfolio::document::utility_types::network_interface::FlowType::HorizontalFlow,
)
.collect::<Vec<_>>();
let other_layer_nodes = document
.network_interface
.upstream_flow_back_from_nodes(vec![other_layer.to_node()], &[], FlowType::HorizontalFlow)
.collect::<Vec<_>>();
// Add merge node and insert between path and spline
let merge_node_id = NodeId::new();
let merge_node = document_node_definitions::resolve_document_node_type("Merge")
.expect("Failed to create merge node")
.default_node_template();
responses.add(NodeGraphMessage::InsertNode {
node_id: merge_node_id,
node_template: merge_node,
});
responses.add(NodeGraphMessage::SetToNodeOrLayer {
node_id: merge_node_id,
is_layer: false,
});
responses.add(NodeGraphMessage::MoveNodeToChainStart {
node_id: merge_node_id,
parent: other_layer,
});
responses.add(NodeGraphMessage::ConnectUpstreamOutputToInput {
downstream_input: InputConnector::node(current_layer.to_node(), 1),
input_connector: InputConnector::node(merge_node_id, 1),
});
// Add flatten vector elements node after merge
let flatten_node_id = NodeId::new();
let flatten_node = document_node_definitions::resolve_document_node_type("Flatten Vector Elements")
.expect("Failed to create flatten node")
.default_node_template();
responses.add(NodeGraphMessage::InsertNode {
node_id: flatten_node_id,
node_template: flatten_node,
});
responses.add(NodeGraphMessage::MoveNodeToChainStart {
node_id: flatten_node_id,
parent: other_layer,
});
let path_node_id = NodeId::new();
let path_node = document_node_definitions::resolve_document_node_type("Path")
.expect("Failed to create path node")
.default_node_template();
responses.add(NodeGraphMessage::InsertNode {
node_id: path_node_id,
node_template: path_node,
});
responses.add(NodeGraphMessage::MoveNodeToChainStart {
node_id: path_node_id,
parent: other_layer,
});
let stroke_node_id = NodeId::new();
let stroke_node = document_node_definitions::resolve_document_node_type("Stroke")
.expect("Failed to create stroke node")
.default_node_template();
responses.add(NodeGraphMessage::InsertNode {
node_id: stroke_node_id,
node_template: stroke_node,
});
responses.add(NodeGraphMessage::MoveNodeToChainStart {
node_id: stroke_node_id,
parent: other_layer,
});
let node_range = if is_layer_line(document, other_layer) {
1..2 // transform node is not deleted
} else {
1..3
};
responses.add(NodeGraphMessage::DeleteNodes {
node_ids: other_layer_nodes[node_range].to_vec(),
delete_children: false,
});
responses.add(NodeGraphMessage::DeleteNodes {
node_ids: current_layer_nodes[..2].to_vec(),
delete_children: false,
});
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(Message::StartBuffer);
}
fn is_layer_spline(document: &DocumentMessageHandler, layer: LayerNodeIdentifier) -> bool {
let nodes = document
.network_interface
.upstream_flow_back_from_nodes(vec![layer.to_node()], &[], FlowType::HorizontalFlow)
.collect::<Vec<NodeId>>();
// Check node types in the chain
let mut has_spline = false;
for node in nodes {
if let Some(reference) = document.network_interface.reference(&node, &[]) {
match reference.as_deref() {
Some("Splines from Points") => has_spline = true,
_ => continue,
}
}
}
has_spline
}
fn is_layer_line(document: &DocumentMessageHandler, layer: LayerNodeIdentifier) -> bool {
let nodes = document
.network_interface
.upstream_flow_back_from_nodes(vec![layer.to_node()], &[], FlowType::HorizontalFlow)
.collect::<Vec<NodeId>>();
// Check node types in the chain
let mut has_line = false;
for node in nodes {
if let Some(reference) = document.network_interface.reference(&node, &[]) {
match reference.as_deref() {
Some("Line") => has_line = true,
_ => continue,
}
}
}
has_line
}