Migrate node graph UI interaction from frontend to backend (#1768)

* Click node using click targets based

* Display graph transform based on state stored in Rust, fix zoom and pan.

* Migrate node selection logic

* Move click targets and transform to NodeNetwork

* Keep click targets in sync with changes to node shape

* Click targets for import/export, add dragging

* Basic wire dragging

* complete wire dragging

* Add node selection box when dragging

* Fix zoom operations and dragging nodes

* Remove click targets from serialized data, fix EnterNestedNetwork

* WIP: Auto connect node when dragged on wire

* Finish auto connect node when dragged on wire

* Add context menus

* Improve layer width calculations and state

* Improve context menu state, various other improvements

* Close menu on escape

* Cleanup Graph.svelte

* Fix lock/hide tool tip shortcuts

* Clean up editor_api.rs, fix lock/hide layers

* Start transferring network and node metadata from NodeNetwork to the editor

* Transfer click targets to NodeGraphMessageHandler

* Fix infinite canvas

* Fix undo/redo, scrollbars, and fix warnings

* Unicode-3.0 license and code cleanup

* License fix

* formatting issue

* Enable DomRect

* Fix layer move crash

* Remove tests

* Ignore test

* formatting

* remove white dot

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
adamgerhant
2024-06-15 08:55:33 -07:00
committed by GitHub
parent cf01f522a8
commit 02360c7bc8
29 changed files with 2744 additions and 1257 deletions

View File

@@ -14,6 +14,7 @@ use glam::{DAffine2, DVec2};
/// Represents a clickable target for the layer
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ClickTarget {
pub subpath: bezier_rs::Subpath<PointId>,
pub stroke_width: f64,
@@ -471,7 +472,7 @@ impl GraphicElementRendered for crate::ArtboardGroup {
}
fn contains_artboard(&self) -> bool {
true
self.artboards.len() > 0
}
}
impl GraphicElementRendered for ImageFrame<Color> {

View File

@@ -5,7 +5,7 @@ use dyn_any::{DynAny, StaticType};
pub use graphene_core::uuid::generate_uuid;
use graphene_core::{ProtoNodeIdentifier, Type};
use glam::IVec2;
use glam::{DAffine2, IVec2};
use std::collections::hash_map::DefaultHasher;
use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
@@ -125,6 +125,7 @@ where
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DocumentNode {
/// A name chosen by the user for this instance of the node. Empty indicates no given name, in which case the node definition's name is displayed to the user in italics.
/// Ensure the click target in the encapsulating network is updated when this is modified by using network.update_click_target(node_id).
#[serde(default)]
pub alias: String,
// TODO: Replace this name with a reference to the [`DocumentNodeDefinition`] node definition to use the name from there instead.
@@ -136,6 +137,7 @@ pub struct DocumentNode {
/// - A constant value [`NodeInput::Value`],
/// - A [`NodeInput::Network`] which specifies that this input is from outside the graph, which is resolved in the graph flattening step in the case of nested networks.
/// In the root network, it is resolved when evaluating the borrow tree.
/// Ensure the click target in the encapsulating network is updated when the inputs cause the node shape to change (currently only when exposing/hiding an input) by using network.update_click_target(node_id).
#[serde(deserialize_with = "deserialize_inputs")]
pub inputs: Vec<NodeInput>,
/// Manual composition is a way to override the default composition flow of one node into another.
@@ -229,7 +231,7 @@ pub struct DocumentNode {
pub has_primary_output: bool,
// A nested document network or a proto-node identifier.
pub implementation: DocumentNodeImplementation,
/// User chosen state for displaying this as a left-to-right node or bottom-to-top layer.
/// User chosen state for displaying this as a left-to-right node or bottom-to-top layer. Ensure the click target in the encapsulating network is updated when the node changes to a layer by using network.update_click_target(node_id).
#[serde(default)]
pub is_layer: bool,
/// Represents the eye icon for hiding/showing the node in the graph UI. When hidden, a node gets replaced with an identity node during the graph flattening step.
@@ -238,7 +240,7 @@ pub struct DocumentNode {
/// Represents the lock icon for locking/unlocking the node in the graph UI. When locked, a node cannot be moved in the graph UI.
#[serde(default)]
pub locked: bool,
/// Metadata about the node including its position in the graph UI.
/// Metadata about the node including its position in the graph UI. Ensure the click target in the encapsulating network is updated when the node moves by using network.update_click_target(node_id).
pub metadata: DocumentNodeMetadata,
/// When two different proto nodes hash to the same value (e.g. two value nodes each containing `2_u32` or two multiply nodes that have the same node IDs as input), the duplicates are removed.
/// See [`crate::proto::ProtoNetwork::generate_stable_node_ids`] for details.
@@ -288,7 +290,7 @@ impl Default for DocumentNode {
is_layer: false,
visible: true,
locked: Default::default(),
metadata: Default::default(),
metadata: DocumentNodeMetadata::default(),
skip_deduplication: Default::default(),
world_state_hash: Default::default(),
original_location: OriginalLocation::default(),
@@ -645,7 +647,7 @@ fn default_export_metadata() -> (NodeId, IVec2) {
(NodeId(generate_uuid()), IVec2::new(8, -4))
}
#[derive(Clone, Debug, PartialEq, DynAny)]
#[derive(Clone, Debug, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
/// A network (subgraph) of nodes containing each [`DocumentNode`] and its ID, as well as list mapping each export to its connected node, or a value if disconnected
pub struct NodeNetwork {
@@ -663,6 +665,9 @@ pub struct NodeNetwork {
pub imports_metadata: (NodeId, IVec2),
#[serde(default = "default_export_metadata")]
pub exports_metadata: (NodeId, IVec2),
/// Transform from node graph space to viewport space.
#[serde(default)]
pub node_graph_to_viewport: DAffine2,
}
impl std::hash::Hash for NodeNetwork {
@@ -685,9 +690,16 @@ impl Default for NodeNetwork {
previewing: Default::default(),
imports_metadata: default_import_metadata(),
exports_metadata: default_export_metadata(),
node_graph_to_viewport: DAffine2::default(),
}
}
}
impl PartialEq for NodeNetwork {
fn eq(&self, other: &Self) -> bool {
self.exports == other.exports && self.previewing == other.previewing && self.imports_metadata == other.imports_metadata && self.exports_metadata == other.exports_metadata
}
}
/// Graph modification functions
impl NodeNetwork {
pub fn current_hash(&self) -> u64 {
@@ -775,26 +787,27 @@ impl NodeNetwork {
}
/// Appends a new node to the network after the output node and sets it as the new output
pub fn push_node(&mut self, mut node: DocumentNode) -> NodeId {
let id = NodeId(self.nodes.len().try_into().expect("Too many nodes in network"));
// Set the correct position for the new node
if node.metadata.position == IVec2::default() {
if let Some(pos) = self.get_root_node().and_then(|root_node| self.nodes.get(&root_node.id)).map(|n| n.metadata.position) {
node.metadata.position = pos + IVec2::new(8, 0);
}
}
if !self.exports.is_empty() {
let input = self.exports[0].clone();
if node.inputs.is_empty() {
node.inputs.push(input);
} else {
node.inputs[0] = input;
}
}
self.nodes.insert(id, node);
self.exports = vec![NodeInput::node(id, 0)];
id
}
// pub fn push_node_to_document_network(&mut self, mut node: DocumentNode) -> NodeId {
// let id = NodeId(self.nodes.len().try_into().expect("Too many nodes in network"));
// // Set the correct position for the new node
// if node.metadata.position == IVec2::default() {
// if let Some(pos) = self.get_root_node().and_then(|root_node| self.nodes.get(&root_node.id)).map(|n| n.metadata.position) {
// node.metadata.position = pos + IVec2::new(8, 0);
// }
// }
// if !self.exports.is_empty() {
// let input = self.exports[0].clone();
// if node.inputs.is_empty() {
// node.inputs.push(input);
// } else {
// node.inputs[0] = input;
// }
// }
// // Use node_graph.insert_node
// self.insert_node(id, node);
// self.exports = vec![NodeInput::node(id, 0)];
// id
// }
/// Get the nested network given by the path of node ids
pub fn nested_network(&self, nested_path: &[NodeId]) -> Option<&Self> {
@@ -816,7 +829,7 @@ impl NodeNetwork {
network
}
/// Get the network the selected nodes are part of, which is either self or the nested network from nested_path
/// Get the network the selected nodes are part of, which is either self or the nested network from nested_path. Used to get nodes selected in the layer panel when viewing a nested network.
pub fn nested_network_for_selected_nodes<'a>(&self, nested_path: &Vec<NodeId>, mut selected_nodes: impl Iterator<Item = &'a NodeId>) -> Option<&Self> {
if selected_nodes.any(|node_id| self.nodes.contains_key(node_id) || self.exports_metadata.0 == *node_id || self.imports_metadata.0 == *node_id) {
Some(self)
@@ -825,7 +838,7 @@ impl NodeNetwork {
}
}
/// Get the mutable network the selected nodes are part of, which is either self or the nested network from nested_path
/// Get the mutable network the selected nodes are part of, which is either self or the nested network from nested_path. Used to modify nodes selected in the layer panel when viewing a nested network.
pub fn nested_network_for_selected_nodes_mut<'a>(&mut self, nested_path: &Vec<NodeId>, mut selected_nodes: impl Iterator<Item = &'a NodeId>) -> Option<&mut Self> {
if selected_nodes.any(|node_id| self.nodes.contains_key(node_id)) {
Some(self)

View File

@@ -98,7 +98,7 @@ async fn map_gpu<'a: 'input>(image: ImageFrame<Color>, node: DocumentNode, edito
let compute_pass_descriptor = if self.cache.borrow().contains_key(&node.name) {
self.cache.borrow().get(&node.name).unwrap().clone()
} else {
let name = node.name.clone();
let name = node.name.to_string();
let Ok(compute_pass_descriptor) = create_compute_pass_descriptor(node, &image, executor, quantization).await else {
log::error!("Error creating compute pass descriptor in 'map_gpu()");
return ImageFrame::empty();