Allow the Pen tool to connect layers by their endpoints, merging into a single layer (#2076)

* Merge vector data layers

* Fix early return

* Fix layer space multiplication error

* Recalculate positions when changing layer space

* Add transform node

* Remove pen tool layer state
This commit is contained in:
adamgerhant
2024-11-01 12:19:46 -07:00
committed by GitHub
parent 018e9839f8
commit c7b08246c2
8 changed files with 229 additions and 33 deletions
@@ -211,7 +211,8 @@ impl Fsm for FreehandToolFsmState {
tool_data.weight = tool_options.line_weight;
// Extend an endpoint of the selected path
if let Some((layer, point, position)) = should_extend(document, input.mouse.position, crate::consts::SNAP_POINT_TOLERANCE) {
let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap();
if let Some((layer, point, position)) = should_extend(document, input.mouse.position, crate::consts::SNAP_POINT_TOLERANCE, selected_nodes.selected_layers(document.metadata())) {
tool_data.layer = Some(layer);
tool_data.end_point = Some((position, point));
@@ -1,9 +1,10 @@
use super::tool_prelude::*;
use crate::consts::{DEFAULT_STROKE_WIDTH, HIDE_HANDLE_DISTANCE, LINE_ROTATE_SNAP_ANGLE};
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_overlays;
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::network_interface::InputConnector;
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;
@@ -57,6 +58,7 @@ pub enum PenToolMessage {
Redo,
Undo,
UpdateOptions(PenOptionsUpdate),
RecalculateLatestPointsPosition,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
@@ -202,7 +204,6 @@ struct LastPoint {
#[derive(Clone, Debug, Default)]
struct PenToolData {
layer: Option<LayerNodeIdentifier>,
snap_manager: SnapManager,
latest_points: Vec<LastPoint>,
point_index: usize,
@@ -215,6 +216,8 @@ struct PenToolData {
angle: f64,
auto_panning: AutoPanning,
modifiers: ModifierState,
buffering_merged_vector: bool,
}
impl PenToolData {
fn latest_point(&self) -> Option<&LastPoint> {
@@ -231,6 +234,24 @@ impl PenToolData {
self.latest_points.push(point);
}
// When the vector data transform changes, the positions of the points must be recalculated.
fn recalculate_latest_points_position(&mut self, document: &DocumentMessageHandler) {
let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap();
let mut selected_layers = selected_nodes.selected_layers(document.metadata());
if let (Some(layer), None) = (selected_layers.next(), selected_layers.next()) {
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else {
return;
};
for point in &mut self.latest_points {
let Some(pos) = vector_data.point_domain.position_from_id(point.id) else {
continue;
};
point.pos = pos;
point.handle_start = point.pos;
}
}
}
/// 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, snap_data: SnapData, transform: DAffine2) {
self.g1_continuous = true;
@@ -266,7 +287,9 @@ impl PenToolData {
// Get close path
let mut end = None;
let layer = self.layer?;
let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap();
let mut selected_layers = selected_nodes.selected_layers(document.metadata());
let layer = selected_layers.next().filter(|_| selected_layers.next().is_none())?;
let vector_data = document.network_interface.compute_modified_vector(layer)?;
let start = self.latest_point()?.id;
let transform = document.metadata().document_to_viewport * transform;
@@ -426,13 +449,13 @@ impl Fsm for PenToolFsmState {
..
} = tool_action_data;
let mut transform = tool_data.layer.map(|layer| document.metadata().transform_to_document(layer)).unwrap_or_default();
let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap();
let mut selected_layers = selected_nodes.selected_layers(document.metadata());
let layer = selected_layers.next().filter(|_| selected_layers.next().is_none());
let mut transform = layer.map(|layer| document.metadata().transform_to_document(layer)).unwrap_or_default();
if !transform.inverse().is_finite() {
let parent_transform = tool_data
.layer
.and_then(|layer| layer.parent(document.metadata()))
.map(|layer| document.metadata().transform_to_document(layer));
let parent_transform = layer.and_then(|layer| layer.parent(document.metadata())).map(|layer| document.metadata().transform_to_document(layer));
transform = parent_transform.unwrap_or(DAffine2::IDENTITY);
}
@@ -511,19 +534,23 @@ impl Fsm for PenToolFsmState {
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 viewport = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document);
let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap();
let mut selected_layers = selected_nodes.selected_layers(document.metadata());
// Perform extension of an existing path
if let Some((layer, point, position)) = should_extend(document, viewport, crate::consts::SNAP_POINT_TOLERANCE) {
let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap();
if let Some((layer, point, position)) = should_extend(document, viewport, crate::consts::SNAP_POINT_TOLERANCE, selected_nodes.selected_layers(document.metadata())) {
log::debug!("Should extend: {:?}", layer);
tool_data.add_point(LastPoint {
id: point,
pos: position,
in_segment: None,
handle_start: position,
});
tool_data.layer = Some(layer);
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] });
tool_data.next_point = position;
tool_data.next_handle_start = position;
} else if let Some(layer) = tool_data.layer {
} else if let (Some(layer), None) = (selected_layers.next(), selected_layers.next()) {
log::debug!("Adding to layer: {:?}", layer);
// Add the first point to a new layer
// Generate first point
let id = PointId::generate();
@@ -539,6 +566,7 @@ impl Fsm for PenToolFsmState {
tool_data.next_point = pos;
tool_data.next_handle_start = pos;
} else {
log::debug!("Creating new layer");
// New path layer
let node_type = resolve_document_node_type("Path").expect("Path node does not exist");
let nodes = vec![(NodeId(0), node_type.default_node_template())];
@@ -547,7 +575,7 @@ impl Fsm for PenToolFsmState {
let layer = graph_modification_utils::new_custom(NodeId::new(), 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);
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] });
responses.add(Message::StartBuffer);
responses.add(PenToolMessage::DragStart);
return PenToolFsmState::Ready;
@@ -556,12 +584,135 @@ impl Fsm for PenToolFsmState {
// Enter the dragging handle state while the mouse is held down, allowing the user to move the mouse and position the handle
PenToolFsmState::DraggingHandle
}
(state, PenToolMessage::RecalculateLatestPointsPosition) => {
tool_data.recalculate_latest_points_position(document);
state
}
(PenToolFsmState::PlacingAnchor, PenToolMessage::DragStart) => {
if tool_data.handle_end.is_some() {
responses.add(DocumentMessage::StartTransaction);
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 viewport = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document);
// Early return if the buffer was started and this message is being run again after the buffer (so that place_anchor updates the state with the newly merged vector)
if tool_data.buffering_merged_vector {
tool_data.buffering_merged_vector = false;
tool_data.bend_from_previous_point(SnapData::new(document, input), transform);
tool_data.place_anchor(SnapData::new(document, input), transform, input.mouse.position, responses);
tool_data.buffering_merged_vector = false;
PenToolFsmState::DraggingHandle
} else {
if tool_data.handle_end.is_some() {
responses.add(DocumentMessage::StartTransaction);
}
// Merge two layers if the point is connected to the end point of another path
// This might not be the correct solution to artboards being included as the other layer, which occurs due to the compute_modified_vector call in should_extend using the click targets for a layer instead of vector data.
let layers = LayerNodeIdentifier::ROOT_PARENT
.descendants(document.metadata())
.filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]));
if let Some((other_layer, _, _)) = should_extend(document, viewport, crate::consts::SNAP_POINT_TOLERANCE, layers) {
let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap();
let mut selected_layers = selected_nodes.selected_layers(document.metadata());
if let Some(current_layer) = selected_layers.next().filter(|current_layer| selected_layers.next().is_none() && *current_layer != other_layer) {
// 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: crate::messages::portfolio::document::graph_operation::utility_types::TransformIn::Local,
skip_rerender: false,
});
// Move the other layer below the current layer for positioning purposes
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();
responses.add(NodeGraphMessage::MoveLayerToStack {
layer: other_layer,
parent: current_layer_parent,
insert_index: current_layer_index + 1,
});
// Merge the inputs of the two layers
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),
});
responses.add(NodeGraphMessage::DeleteNodes {
node_ids: vec![other_layer.to_node()],
delete_children: false,
});
// Add a flatten vector elements node after the 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,
});
// Add a path node after the flatten node
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,
});
// Add a transform node to ensure correct tooling modifications
let transform_node_id = NodeId::new();
let transform_node = document_node_definitions::resolve_document_node_type("Transform")
.expect("Failed to create transform node")
.default_node_template();
responses.add(NodeGraphMessage::InsertNode {
node_id: transform_node_id,
node_template: transform_node,
});
responses.add(NodeGraphMessage::MoveNodeToChainStart {
node_id: transform_node_id,
parent: current_layer,
});
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(Message::StartBuffer);
responses.add(PenToolMessage::RecalculateLatestPointsPosition);
}
}
// Even if no buffer was started, the message still has to be run again in order to call bend_from_previous_point
tool_data.buffering_merged_vector = true;
responses.add(PenToolMessage::DragStart);
PenToolFsmState::PlacingAnchor
}
tool_data.bend_from_previous_point(SnapData::new(document, input), transform);
PenToolFsmState::DraggingHandle
}
(PenToolFsmState::DraggingHandle, PenToolMessage::DragStop) => tool_data
.finish_placing_handle(SnapData::new(document, input), transform, responses)
@@ -633,7 +784,7 @@ impl Fsm for PenToolFsmState {
}
(PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor, PenToolMessage::Abort | PenToolMessage::Confirm) => {
responses.add(DocumentMessage::EndTransaction);
tool_data.layer = None;
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: Vec::new() });
tool_data.handle_end = None;
tool_data.latest_points.clear();
tool_data.point_index = 0;