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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 229 additions and 33 deletions

View file

@ -1,7 +1,7 @@
use super::transform_utils;
use super::utility_types::ModifyInputsContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface};
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface, OutputConnector};
use crate::messages::portfolio::document::utility_types::nodes::CollapsedLayers;
use crate::messages::prelude::*;
@ -95,14 +95,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
}
}
GraphOperationMessage::SetUpstreamToChain { layer } => {
let Some(first_chain_node) = network_interface
.upstream_flow_back_from_nodes(
vec![layer.to_node()],
&[],
crate::messages::portfolio::document::utility_types::network_interface::FlowType::HorizontalFlow,
)
.nth(1)
else {
let Some(OutputConnector::Node { node_id: first_chain_node, .. }) = network_interface.upstream_output_connector(&InputConnector::node(layer.to_node(), 1), &[]) else {
return;
};

View file

@ -37,6 +37,10 @@ pub enum NodeGraphMessage {
output_connector: OutputConnector,
input_connector: InputConnector,
},
ConnectUpstreamOutputToInput {
downstream_input: InputConnector,
input_connector: InputConnector,
},
Cut,
DeleteNodes {
node_ids: Vec<NodeId>,
@ -70,6 +74,10 @@ pub enum NodeGraphMessage {
parent: LayerNodeIdentifier,
insert_index: usize,
},
MoveNodeToChainStart {
node_id: NodeId,
parent: LayerNodeIdentifier,
},
PasteNodes {
serialized_nodes: String,
},

View file

@ -210,6 +210,16 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
});
responses.add(NodeGraphMessage::SendGraph);
}
NodeGraphMessage::ConnectUpstreamOutputToInput { downstream_input, input_connector } => {
let Some(upstream_node) = network_interface.upstream_output_connector(&downstream_input, selection_network_path) else {
log::error!("Failed to find upstream node for downstream_input");
return;
};
responses.add(NodeGraphMessage::CreateWire {
output_connector: upstream_node,
input_connector,
});
}
NodeGraphMessage::Cut => {
responses.add(NodeGraphMessage::Copy);
responses.add(NodeGraphMessage::DeleteSelectedNodes { delete_children: true });
@ -323,6 +333,9 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
NodeGraphMessage::MoveLayerToStack { layer, parent, insert_index } => {
network_interface.move_layer_to_stack(layer, parent, insert_index, selection_network_path);
}
NodeGraphMessage::MoveNodeToChainStart { node_id, parent } => {
network_interface.move_node_to_chain_start(&node_id, parent, selection_network_path);
}
NodeGraphMessage::PasteNodes { serialized_nodes } => {
let data = match serde_json::from_str::<Vec<(NodeId, NodeTemplate)>>(&serialized_nodes) {
Ok(d) => d,

View file

@ -88,6 +88,10 @@ impl DocumentMetadata {
self.upstream_transforms.get(&node_id).copied().map(|(_, transform)| transform).unwrap_or(DAffine2::IDENTITY)
}
pub fn downstream_transform_to_document(&self, layer: LayerNodeIdentifier) -> DAffine2 {
self.document_to_viewport.inverse() * self.downstream_transform_to_viewport(layer)
}
pub fn downstream_transform_to_viewport(&self, layer: LayerNodeIdentifier) -> DAffine2 {
if layer == LayerNodeIdentifier::ROOT_PARENT {
return self.transform_to_viewport(layer);

View file

@ -4894,6 +4894,32 @@ impl NodeNetworkInterface {
self.create_wire(&OutputConnector::node(*node_id, 0), &InputConnector::node(parent.to_node(), 1), network_path);
self.set_chain_position(node_id, network_path);
} else {
// TODO: Implement a more robust horizontal shift system when inserting a node into a chain.
// This should be done by breaking the chain and shifting the sole dependents for each node upstream of the insertion.
// Before inserting the node, shift the layer right 7 units so that all sole dependents are also shifted
// let input_connector = InputConnector::node(parent.to_node(), 0);
// let old_upstream = self.upstream_output_connector(&input_connector, network_path);
// This also needs to disconnect from the downstream layer
// self.disconnect_input(&input_connector, network_path);
// let Some(selected_nodes) = self.selected_nodes_mut(network_path) else {
// log::error!("Could not get selected nodes in move_layer_to_stack");
// return;
// };
// let old_selected_nodes = selected_nodes.replace_with(vec![parent.to_node()]);
// for _ in 0..7 {
// self.shift_selected_nodes(Direction::Left, false, network_path);
// }
// // Grip drag it back to the right
// for _ in 0..7 {
// self.shift_selected_nodes(Direction::Right, true, network_path);
// }
// let _ = self.selected_nodes_mut(network_path).unwrap().replace_with(old_selected_nodes);
// if let Some(old_upstream) = old_upstream {
// self.create_wire(&old_upstream, &input_connector, network_path);
// }
// Insert the node in the gap and set the upstream to a chain
self.insert_node_between(node_id, &InputConnector::node(parent.to_node(), 1), 0, network_path);
self.force_set_upstream_to_chain(node_id, network_path);
}

View file

@ -6,14 +6,14 @@ use graphene_std::vector::PointId;
use glam::DVec2;
/// Determines if a path should be extended. Goal in viewport space. Returns the path and if it is extending from the start, if applicable.
pub fn should_extend(document: &DocumentMessageHandler, goal: DVec2, tolerance: f64) -> Option<(LayerNodeIdentifier, PointId, DVec2)> {
pub fn should_extend(document: &DocumentMessageHandler, goal: DVec2, tolerance: f64, layers: impl Iterator<Item = LayerNodeIdentifier>) -> Option<(LayerNodeIdentifier, PointId, DVec2)> {
let mut best = None;
let mut best_distance_squared = tolerance * tolerance;
for layer in document.network_interface.selected_nodes(&[]).unwrap().selected_layers(document.metadata()) {
for layer in layers {
let viewspace = document.metadata().transform_to_viewport(layer);
let vector_data = document.network_interface.compute_modified_vector(layer)?;
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else {
continue;
};
for id in vector_data.single_connected_points() {
let Some(point) = vector_data.point_domain.position_from_id(id) else { continue };

View file

@ -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));

View file

@ -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;