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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 2744 additions and 1257 deletions

860
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,7 @@ accepted = [
"MIT", "MIT",
"MPL-2.0", "MPL-2.0",
"OpenSSL", "OpenSSL",
"Unicode-3.0",
"Unicode-DFS-2016", "Unicode-DFS-2016",
"Zlib", "Zlib",
] ]

View File

@ -83,6 +83,7 @@ allow = [
"MIT", "MIT",
"MPL-2.0", "MPL-2.0",
"OpenSSL", "OpenSSL",
"Unicode-3.0",
"Unicode-DFS-2016", "Unicode-DFS-2016",
"Zlib", "Zlib",
] ]

View File

@ -53,6 +53,7 @@ wasm-bindgen-futures = { workspace = true, optional = true }
once_cell = "1.13.0" once_cell = "1.13.0"
web-sys = { workspace = true, features = [ web-sys = { workspace = true, features = [
"Document", "Document",
"DomRect",
"Element", "Element",
"HtmlCanvasElement", "HtmlCanvasElement",
"CanvasRenderingContext2d", "CanvasRenderingContext2d",

View File

@ -294,6 +294,8 @@ mod test {
editor editor
} }
// TODO: Fix text
#[ignore]
#[test] #[test]
/// - create rect, shape and ellipse /// - create rect, shape and ellipse
/// - copy /// - copy
@ -323,6 +325,8 @@ mod test {
} }
} }
// TODO: Fix text
#[ignore]
#[test] #[test]
#[cfg_attr(miri, ignore)] #[cfg_attr(miri, ignore)]
/// - create rect, shape and ellipse /// - create rect, shape and ellipse
@ -358,6 +362,8 @@ mod test {
} }
} }
// TODO: Fix text
#[ignore]
#[test] #[test]
#[cfg_attr(miri, ignore)] #[cfg_attr(miri, ignore)]
/// - create rect, shape and ellipse /// - create rect, shape and ellipse
@ -406,6 +412,8 @@ mod test {
assert_eq!(layers_after_copy[5], shape_id); assert_eq!(layers_after_copy[5], shape_id);
} }
// TODO: Fix text
#[ignore]
#[test] #[test]
/// This test will fail when you make changes to the underlying serialization format for a document. /// This test will fail when you make changes to the underlying serialization format for a document.
fn check_if_demo_art_opens() { fn check_if_demo_art_opens() {

View File

@ -1,6 +1,6 @@
use super::utility_types::{FrontendDocumentDetails, MouseCursorIcon}; use super::utility_types::{FrontendDocumentDetails, MouseCursorIcon};
use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::node_graph::utility_types::{FrontendNode, FrontendNodeType, FrontendNodeWire}; use crate::messages::portfolio::document::node_graph::utility_types::{BoxSelection, ContextMenuInformation, FrontendNode, FrontendNodeType, FrontendNodeWire, Transform, WirePath};
use crate::messages::portfolio::document::utility_types::nodes::{JsRawBuffer, LayerPanelEntry, RawBuffer}; use crate::messages::portfolio::document::utility_types::nodes::{JsRawBuffer, LayerPanelEntry, RawBuffer};
use crate::messages::prelude::*; use crate::messages::prelude::*;
use crate::messages::tool::utility_types::HintData; use crate::messages::tool::utility_types::HintData;
@ -109,6 +109,18 @@ pub enum FrontendMessage {
#[serde(rename = "documentId")] #[serde(rename = "documentId")]
document_id: DocumentId, document_id: DocumentId,
}, },
UpdateBox {
#[serde(rename = "box")]
box_selection: Option<BoxSelection>,
},
UpdateContextMenuInformation {
#[serde(rename = "contextMenuInformation")]
context_menu_information: Option<ContextMenuInformation>,
},
UpdateLayerWidths {
#[serde(rename = "layerWidths")]
layer_widths: HashMap<NodeId, u32>,
},
UpdateDialogButtons { UpdateDialogButtons {
#[serde(rename = "layoutTarget")] #[serde(rename = "layoutTarget")]
layout_target: LayoutTarget, layout_target: LayoutTarget,
@ -198,6 +210,9 @@ pub enum FrontendMessage {
UpdateNodeGraphSelection { UpdateNodeGraphSelection {
selected: Vec<NodeId>, selected: Vec<NodeId>,
}, },
UpdateNodeGraphTransform {
transform: Transform,
},
UpdateNodeThumbnail { UpdateNodeThumbnail {
id: NodeId, id: NodeId,
value: String, value: String,
@ -234,6 +249,10 @@ pub enum FrontendMessage {
layout_target: LayoutTarget, layout_target: LayoutTarget,
diff: Vec<WidgetDiff>, diff: Vec<WidgetDiff>,
}, },
UpdateWirePathInProgress {
#[serde(rename = "wirePath")]
wire_path: Option<WirePath>,
},
UpdateWorkingColorsLayout { UpdateWorkingColorsLayout {
#[serde(rename = "layoutTarget")] #[serde(rename = "layoutTarget")]
layout_target: LayoutTarget, layout_target: LayoutTarget,

View File

@ -52,6 +52,16 @@ pub fn input_mappings() -> Mapping {
// Hack to prevent LMB + CTRL (OPTION) + Z combo (this effectively blocks you from making a double undo with AbortTransaction) // Hack to prevent LMB + CTRL (OPTION) + Z combo (this effectively blocks you from making a double undo with AbortTransaction)
entry!(KeyDown(KeyZ); modifiers=[Accel, Lmb], action_dispatch=DocumentMessage::Noop), entry!(KeyDown(KeyZ); modifiers=[Accel, Lmb], action_dispatch=DocumentMessage::Noop),
// NodeGraphMessage // NodeGraphMessage
entry!(KeyDown(Lmb); action_dispatch=NodeGraphMessage::PointerDown {shift_click: false, control_click: false, alt_click: false, right_click: false}),
entry!(KeyDown(Lmb); modifiers=[Shift], action_dispatch=NodeGraphMessage::PointerDown {shift_click: true, control_click: false, alt_click: false, right_click: false}),
entry!(KeyDown(Lmb); modifiers=[Accel], action_dispatch=NodeGraphMessage::PointerDown {shift_click: false, control_click: true, alt_click: false, right_click: false}),
entry!(KeyDown(Lmb); modifiers=[Shift, Accel], action_dispatch=NodeGraphMessage::PointerDown {shift_click: true, control_click: true, alt_click: false, right_click: false}),
entry!(KeyDown(Lmb); modifiers=[Alt], action_dispatch=NodeGraphMessage::PointerDown {shift_click: false, control_click: false, alt_click: true, right_click: false}),
entry!(KeyDown(Rmb); action_dispatch=NodeGraphMessage::PointerDown {shift_click: false, control_click: false, alt_click: false, right_click: true}),
entry!(DoubleClick(MouseButton::Left); action_dispatch=NodeGraphMessage::EnterNestedNetwork),
entry!(PointerMove; refresh_keys=[Shift], action_dispatch=NodeGraphMessage::PointerMove {shift: Shift}),
entry!(KeyUp(Lmb); action_dispatch=NodeGraphMessage::PointerUp),
entry!(KeyUp(Escape); action_dispatch=NodeGraphMessage::CloseCreateNodeMenu),
entry!(KeyDown(Delete); modifiers=[Accel], action_dispatch=NodeGraphMessage::DeleteSelectedNodes { reconnect: false }), entry!(KeyDown(Delete); modifiers=[Accel], action_dispatch=NodeGraphMessage::DeleteSelectedNodes { reconnect: false }),
entry!(KeyDown(Backspace); modifiers=[Accel], action_dispatch=NodeGraphMessage::DeleteSelectedNodes { reconnect: false }), entry!(KeyDown(Backspace); modifiers=[Accel], action_dispatch=NodeGraphMessage::DeleteSelectedNodes { reconnect: false }),
entry!(KeyDown(Delete); action_dispatch=NodeGraphMessage::DeleteSelectedNodes { reconnect: true }), entry!(KeyDown(Delete); action_dispatch=NodeGraphMessage::DeleteSelectedNodes { reconnect: true }),
@ -59,8 +69,8 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=NodeGraphMessage::Cut), entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=NodeGraphMessage::Cut),
entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=NodeGraphMessage::Copy), entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=NodeGraphMessage::Copy),
entry!(KeyDown(KeyD); modifiers=[Accel], action_dispatch=NodeGraphMessage::DuplicateSelectedNodes), entry!(KeyDown(KeyD); modifiers=[Accel], action_dispatch=NodeGraphMessage::DuplicateSelectedNodes),
entry!(KeyDown(KeyH); modifiers=[Accel], action_dispatch=NodeGraphMessage::ToggleSelectedVisibility), entry!(KeyDown(KeyH); modifiers=[Accel], action_dispatch=GraphOperationMessage::ToggleSelectedVisibility),
entry!(KeyDown(KeyL); modifiers=[Accel], action_dispatch=NodeGraphMessage::ToggleSelectedLocked), entry!(KeyDown(KeyL); modifiers=[Accel], action_dispatch=GraphOperationMessage::ToggleSelectedLocked),
entry!(KeyDown(KeyL); modifiers=[Alt], action_dispatch=NodeGraphMessage::ToggleSelectedAsLayersOrNodes), entry!(KeyDown(KeyL); modifiers=[Alt], action_dispatch=NodeGraphMessage::ToggleSelectedAsLayersOrNodes),
entry!(KeyDown(KeyC); modifiers=[Shift], action_dispatch=NodeGraphMessage::PrintSelectedNodeCoordinates), entry!(KeyDown(KeyC); modifiers=[Shift], action_dispatch=NodeGraphMessage::PrintSelectedNodeCoordinates),
// //

View File

@ -1,3 +1,4 @@
use super::node_graph::utility_types::Transform;
use super::utility_types::clipboards::Clipboard; use super::utility_types::clipboards::Clipboard;
use super::utility_types::error::EditorError; use super::utility_types::error::EditorError;
use super::utility_types::misc::{BoundingBoxSnapTarget, GeometrySnapTarget, OptionBoundsSnapping, OptionPointSnapping, SnappingOptions, SnappingState}; use super::utility_types::misc::{BoundingBoxSnapTarget, GeometrySnapTarget, OptionBoundsSnapping, OptionPointSnapping, SnappingOptions, SnappingState};
@ -74,6 +75,8 @@ pub struct DocumentMessageHandler {
commit_hash: String, commit_hash: String,
/// The current pan, tilt, and zoom state of the viewport's view of the document canvas. /// The current pan, tilt, and zoom state of the viewport's view of the document canvas.
pub navigation: PTZ, pub navigation: PTZ,
/// The current pan, and zoom state of the viewport's view of the node graph.
node_graph_transform: PTZ,
/// The current mode that the document is in, which starts out as Design Mode. This choice affects the editing behavior of the tools. /// The current mode that the document is in, which starts out as Design Mode. This choice affects the editing behavior of the tools.
document_mode: DocumentMode, document_mode: DocumentMode,
/// The current view mode that the user has set for rendering the document within the viewport. /// The current view mode that the user has set for rendering the document within the viewport.
@ -137,6 +140,7 @@ impl Default for DocumentMessageHandler {
name: DEFAULT_DOCUMENT_NAME.to_string(), name: DEFAULT_DOCUMENT_NAME.to_string(),
commit_hash: GRAPHITE_GIT_COMMIT_HASH.to_string(), commit_hash: GRAPHITE_GIT_COMMIT_HASH.to_string(),
navigation: PTZ::default(), navigation: PTZ::default(),
node_graph_transform: PTZ::default(),
document_mode: DocumentMode::DesignMode, document_mode: DocumentMode::DesignMode,
view_mode: ViewMode::default(), view_mode: ViewMode::default(),
overlays_visible: true, overlays_visible: true,
@ -169,13 +173,18 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
match message { match message {
// Sub-messages // Sub-messages
DocumentMessage::Navigation(message) => { DocumentMessage::Navigation(message) => {
let document_bounds = self.metadata().document_bounds_viewport_space();
let data = NavigationMessageData { let data = NavigationMessageData {
metadata: &self.metadata, metadata: &self.metadata,
document_bounds,
ipp, ipp,
selection_bounds: self.selected_visible_layers_bounding_box_viewport(), selection_bounds: if self.graph_view_overlay_open {
ptz: &mut self.navigation, self.selected_nodes_bounding_box_viewport()
} else {
self.selected_visible_layers_bounding_box_viewport()
},
ptz: if self.graph_view_overlay_open { &mut self.node_graph_transform } else { &mut self.navigation },
graph_view_overlay_open: self.graph_view_overlay_open,
document_network: &self.network,
node_graph_handler: &self.node_graph_handler,
}; };
self.navigation_handler.process_message(message, responses, data); self.navigation_handler.process_message(message, responses, data);
@ -207,7 +216,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
document_id, document_id,
document_name: self.name.as_str(), document_name: self.name.as_str(),
collapsed: &mut self.collapsed, collapsed: &mut self.collapsed,
input: ipp, ipp,
graph_view_overlay_open: self.graph_view_overlay_open, graph_view_overlay_open: self.graph_view_overlay_open,
}, },
); );
@ -369,10 +378,19 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
DocumentMessage::GraphViewOverlay { open } => { DocumentMessage::GraphViewOverlay { open } => {
self.graph_view_overlay_open = open; self.graph_view_overlay_open = open;
// TODO: Find a better way to update click targets when undoing/redoing
if self.graph_view_overlay_open {
self.node_graph_handler.update_all_click_targets(&mut self.network, self.node_graph_handler.network.clone())
}
responses.add(FrontendMessage::TriggerGraphViewOverlay { open });
responses.add(FrontendMessage::TriggerRefreshBoundsOfViewports);
// Update the tilt menu bar buttons to be disabled when the graph is open
responses.add(MenuBarMessage::SendLayout);
if open { if open {
responses.add(NodeGraphMessage::SendGraph); responses.add(NodeGraphMessage::SendGraph);
responses.add(NavigationMessage::CanvasTiltSet { angle_radians: 0. });
} }
responses.add(FrontendMessage::TriggerGraphViewOverlay { open });
} }
DocumentMessage::GraphViewOverlayToggle => { DocumentMessage::GraphViewOverlayToggle => {
responses.add(DocumentMessage::GraphViewOverlay { open: !self.graph_view_overlay_open }); responses.add(DocumentMessage::GraphViewOverlay { open: !self.graph_view_overlay_open });
@ -558,9 +576,10 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
// TODO: The `.collect()` is necessary to avoid borrowing issues with `self`. See if this can be avoided to improve performance. // TODO: The `.collect()` is necessary to avoid borrowing issues with `self`. See if this can be avoided to improve performance.
let ordered_last_elements = self.metadata.all_layers().filter(|layer| get_last_elements.contains(&layer)).rev().collect::<Vec<_>>(); let ordered_last_elements = self.metadata.all_layers().filter(|layer| get_last_elements.contains(&layer)).rev().collect::<Vec<_>>();
for layer_to_move in ordered_last_elements { for layer_to_move in ordered_last_elements {
if layer_to_move if insert_index > 0
.upstream_siblings(&self.metadata) && layer_to_move
.any(|layer| layer_above_insertion.is_some_and(|layer_above_insertion| layer_above_insertion == layer)) .upstream_siblings(&self.metadata)
.any(|layer| layer_above_insertion.is_some_and(|layer_above_insertion| layer_above_insertion == layer))
{ {
insert_index -= 1; insert_index -= 1;
} }
@ -714,9 +733,17 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
DocumentMessage::RenderRulers => { DocumentMessage::RenderRulers => {
let document_transform_scale = self.navigation_handler.snapped_zoom(self.navigation.zoom); let document_transform_scale = self.navigation_handler.snapped_zoom(self.navigation.zoom);
let ruler_origin = self.metadata().document_to_viewport.transform_point2(DVec2::ZERO); let ruler_origin = if !self.graph_view_overlay_open {
self.metadata().document_to_viewport.transform_point2(DVec2::ZERO)
} else {
let Some(network) = self.network.nested_network(&self.node_graph_handler.network) else {
log::error!("Nested network not found in UpdateDocumentTransform");
return;
};
network.node_graph_to_viewport.transform_point2(DVec2::ZERO)
};
let log = document_transform_scale.log2(); let log = document_transform_scale.log2();
let ruler_interval = if log < 0. { 100. * 2_f64.powf(-log.ceil()) } else { 100. / 2_f64.powf(log.ceil()) }; let ruler_interval: f64 = if log < 0. { 100. * 2_f64.powf(-log.ceil()) } else { 100. / 2_f64.powf(log.ceil()) };
let ruler_spacing = ruler_interval * document_transform_scale; let ruler_spacing = ruler_interval * document_transform_scale;
responses.add(FrontendMessage::UpdateDocumentRulers { responses.add(FrontendMessage::UpdateDocumentRulers {
@ -733,7 +760,11 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
let viewport_size = ipp.viewport_bounds.size(); let viewport_size = ipp.viewport_bounds.size();
let viewport_mid = ipp.viewport_bounds.center(); let viewport_mid = ipp.viewport_bounds.center();
let [bounds1, bounds2] = self.metadata().document_bounds_viewport_space().unwrap_or([viewport_mid; 2]); let [bounds1, bounds2] = if !self.graph_view_overlay_open {
self.metadata().document_bounds_viewport_space().unwrap_or([viewport_mid; 2])
} else {
self.node_graph_handler.graph_bounds_viewport_space(&self.network).unwrap_or([viewport_mid; 2])
};
let bounds1 = bounds1.min(viewport_mid) - viewport_size * scale; let bounds1 = bounds1.min(viewport_mid) - viewport_size * scale;
let bounds2 = bounds2.max(viewport_mid) + viewport_size * scale; let bounds2 = bounds2.max(viewport_mid) + viewport_size * scale;
let bounds_length = (bounds2 - bounds1) * (1. + SCROLLBAR_SPACING); let bounds_length = (bounds2 - bounds1) * (1. + SCROLLBAR_SPACING);
@ -957,7 +988,9 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
responses.add(DocumentMessage::UndoFinished); responses.add(DocumentMessage::UndoFinished);
responses.add(ToolMessage::Undo); responses.add(ToolMessage::Undo);
} }
DocumentMessage::UndoFinished => self.undo_in_progress = false, DocumentMessage::UndoFinished => {
self.undo_in_progress = false;
}
DocumentMessage::UngroupSelectedLayers => { DocumentMessage::UngroupSelectedLayers => {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
@ -1050,11 +1083,29 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
responses.add(NodeGraphMessage::SendGraph); responses.add(NodeGraphMessage::SendGraph);
} }
DocumentMessage::UpdateDocumentTransform { transform } => { DocumentMessage::UpdateDocumentTransform { transform } => {
self.metadata.document_to_viewport = transform;
responses.add(DocumentMessage::RenderRulers); responses.add(DocumentMessage::RenderRulers);
responses.add(DocumentMessage::RenderScrollbars); responses.add(DocumentMessage::RenderScrollbars);
responses.add(NodeGraphMessage::RunDocumentGraph);
if !self.graph_view_overlay_open {
self.metadata.document_to_viewport = transform;
responses.add(NodeGraphMessage::RunDocumentGraph);
} else {
let Some(network) = self.network.nested_network_mut(&self.node_graph_handler.network) else {
log::error!("Nested network not found in UpdateDocumentTransform");
return;
};
network.node_graph_to_viewport = transform;
responses.add(FrontendMessage::UpdateNodeGraphTransform {
transform: Transform {
scale: transform.matrix2.x_axis.x,
x: transform.translation.x,
y: transform.translation.y,
},
})
}
responses.add(PortfolioMessage::UpdateDocumentWidgets); responses.add(PortfolioMessage::UpdateDocumentWidgets);
} }
DocumentMessage::ZoomCanvasTo100Percent => { DocumentMessage::ZoomCanvasTo100Percent => {
@ -1064,7 +1115,15 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
responses.add_front(NavigationMessage::CanvasZoomSet { zoom_factor: 2. }); responses.add_front(NavigationMessage::CanvasZoomSet { zoom_factor: 2. });
} }
DocumentMessage::ZoomCanvasToFitAll => { DocumentMessage::ZoomCanvasToFitAll => {
if let Some(bounds) = self.metadata().document_bounds_document_space(true) { let bounds = if self.graph_view_overlay_open {
self.node_graph_handler
.network_metadata
.get(&self.node_graph_handler.network)
.and_then(|network_metadata| network_metadata.bounding_box_subpath.as_ref().and_then(|subpath| subpath.bounding_box()))
} else {
self.metadata().document_bounds_document_space(true)
};
if let Some(bounds) = bounds {
responses.add(NavigationMessage::CanvasTiltSet { angle_radians: 0. }); responses.add(NavigationMessage::CanvasTiltSet { angle_radians: 0. });
responses.add(NavigationMessage::FitViewportToBounds { bounds, prevent_zoom_past_100: true }); responses.add(NavigationMessage::FitViewportToBounds { bounds, prevent_zoom_past_100: true });
} }
@ -1116,9 +1175,9 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
common.extend(self.node_graph_handler.actions_additional_if_node_graph_is_open()); common.extend(self.node_graph_handler.actions_additional_if_node_graph_is_open());
} }
// More additional actions // More additional actions
common.extend(self.node_graph_handler.actions());
common.extend(self.navigation_handler.actions()); common.extend(self.navigation_handler.actions());
common.extend(self.node_graph_handler.actions());
common.extend(actions!(GraphOperationMessageDiscriminant; ToggleSelectedLocked, ToggleSelectedVisibility));
common common
} }
} }
@ -1201,6 +1260,31 @@ impl DocumentMessageHandler {
.reduce(graphene_core::renderer::Quad::combine_bounds) .reduce(graphene_core::renderer::Quad::combine_bounds)
} }
/// Get the combined bounding box of the click targets of the selected nodes in the node graph in viewport space
pub fn selected_nodes_bounding_box_viewport(&self) -> Option<[DVec2; 2]> {
let Some(network) = self.network.nested_network(&self.node_graph_handler.network) else {
log::error!("Could not get nested network in selected_nodes_bounding_box_viewport");
return None;
};
self.selected_nodes
.selected_nodes(network)
.filter_map(|node| {
let mut node_path = self.node_graph_handler.network.clone();
node_path.push(*node);
let Some(node_metadata) = self.node_graph_handler.node_metadata.get(&node_path) else {
log::debug!("Could not get click target for node {node}");
return None;
};
let Some(network_metadata) = self.node_graph_handler.network_metadata.get(&self.node_graph_handler.network) else {
log::debug!("Could not get network_metadata in selected_nodes_bounding_box_viewport");
return None;
};
node_metadata.node_click_target.subpath.bounding_box_with_transform(network_metadata.node_graph_to_viewport)
})
.reduce(graphene_core::renderer::Quad::combine_bounds)
}
pub fn selected_visible_and_unlock_layers_bounding_box_viewport(&self) -> Option<[DVec2; 2]> { pub fn selected_visible_and_unlock_layers_bounding_box_viewport(&self) -> Option<[DVec2; 2]> {
self.selected_nodes self.selected_nodes
.selected_visible_and_unlocked_layers(self.metadata()) .selected_visible_and_unlocked_layers(self.metadata())
@ -1416,6 +1500,10 @@ impl DocumentMessageHandler {
if self.document_redo_history.len() > crate::consts::MAX_UNDO_HISTORY_LEN { if self.document_redo_history.len() > crate::consts::MAX_UNDO_HISTORY_LEN {
self.document_redo_history.pop_front(); self.document_redo_history.pop_front();
} }
// TODO: Find a better way to update click targets when undoing/redoing
if self.graph_view_overlay_open {
self.node_graph_handler.update_all_click_targets(&mut self.network, self.node_graph_handler.network.clone())
}
} }
pub fn undo(&mut self, responses: &mut VecDeque<Message>) -> Option<NodeNetwork> { pub fn undo(&mut self, responses: &mut VecDeque<Message>) -> Option<NodeNetwork> {
// Push the UpdateOpenDocumentsList message to the bus in order to update the save status of the open documents // Push the UpdateOpenDocumentsList message to the bus in order to update the save status of the open documents
@ -1447,6 +1535,10 @@ impl DocumentMessageHandler {
if self.document_undo_history.len() > crate::consts::MAX_UNDO_HISTORY_LEN { if self.document_undo_history.len() > crate::consts::MAX_UNDO_HISTORY_LEN {
self.document_undo_history.pop_front(); self.document_undo_history.pop_front();
} }
// TODO: Find a better way to update click targets when undoing/redoing
if self.graph_view_overlay_open {
self.node_graph_handler.update_all_click_targets(&mut self.network, self.node_graph_handler.network.clone())
}
} }
pub fn current_hash(&self) -> Option<u64> { pub fn current_hash(&self) -> Option<u64> {

View File

@ -67,7 +67,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
document_node = document_node.map_ids(default_inputs, &new_ids); document_node = document_node.map_ids(default_inputs, &new_ids);
// Insert node into network // Insert node into network
document_network.nodes.insert(node_id, document_node); node_graph.insert_node(node_id, document_node, document_network, &Vec::new());
} }
let Some(new_layer_id) = new_ids.get(&NodeId(0)) else { let Some(new_layer_id) = new_ids.get(&NodeId(0)) else {
@ -129,14 +129,15 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
], ],
Default::default(), Default::default(),
); );
document_network.nodes.insert(node_id, new_boolean_operation_node);
node_graph.insert_node(node_id, new_boolean_operation_node, document_network, &Vec::new());
} }
GraphOperationMessage::DeleteLayer { layer, reconnect } => { GraphOperationMessage::DeleteLayer { layer, reconnect } => {
if layer == LayerNodeIdentifier::ROOT_PARENT { if layer == LayerNodeIdentifier::ROOT_PARENT {
log::error!("Cannot delete ROOT_PARENT"); log::error!("Cannot delete ROOT_PARENT");
return; return;
} }
ModifyInputsContext::delete_nodes(document_network, selected_nodes, vec![layer.to_node()], reconnect, responses, Vec::new(), &node_graph.resolved_types); ModifyInputsContext::delete_nodes(node_graph, document_network, selected_nodes, vec![layer.to_node()], reconnect, responses, Vec::new());
load_network_structure(document_network, document_metadata, collapsed); load_network_structure(document_network, document_metadata, collapsed);
responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::RunDocumentGraph);
@ -144,7 +145,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
// TODO: Eventually remove this (probably starting late 2024) // TODO: Eventually remove this (probably starting late 2024)
GraphOperationMessage::DeleteLegacyOutputNode => { GraphOperationMessage::DeleteLegacyOutputNode => {
if document_network.nodes.iter().any(|(node_id, node)| node.name == "Output" && *node_id == NodeId(0)) { if document_network.nodes.iter().any(|(node_id, node)| node.name == "Output" && *node_id == NodeId(0)) {
ModifyInputsContext::delete_nodes(document_network, selected_nodes, vec![NodeId(0)], true, responses, Vec::new(), &node_graph.resolved_types); ModifyInputsContext::delete_nodes(node_graph, document_network, selected_nodes, vec![NodeId(0)], true, responses, Vec::new());
} }
} }
// Make sure to also update NodeGraphMessage::DisconnectInput when changing this // Make sure to also update NodeGraphMessage::DisconnectInput when changing this
@ -182,7 +183,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
responses.add(NodeGraphMessage::SendGraph); responses.add(NodeGraphMessage::SendGraph);
} }
GraphOperationMessage::DisconnectNodeFromStack { node_id, reconnect_to_sibling } => { GraphOperationMessage::DisconnectNodeFromStack { node_id, reconnect_to_sibling } => {
ModifyInputsContext::remove_references_from_network(document_network, node_id, reconnect_to_sibling, &Vec::new(), &node_graph.resolved_types); ModifyInputsContext::remove_references_from_network(node_graph, document_network, node_id, reconnect_to_sibling, &Vec::new());
responses.add(GraphOperationMessage::DisconnectInput { node_id, input_index: 0 }); responses.add(GraphOperationMessage::DisconnectInput { node_id, input_index: 0 });
} }
GraphOperationMessage::FillSet { layer, fill } => { GraphOperationMessage::FillSet { layer, fill } => {
@ -559,8 +560,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
}); });
} }
GraphOperationMessage::NewArtboard { id, artboard } => { GraphOperationMessage::NewArtboard { id, artboard } => {
let mut modify_inputs = ModifyInputsContext::new(document_network, document_metadata, node_graph, responses); if let Some(artboard_id) = ModifyInputsContext::create_artboard(node_graph, document_network, id, artboard) {
if let Some(artboard_id) = modify_inputs.create_artboard(id, artboard) {
responses.add_front(NodeGraphMessage::SelectedNodesSet { nodes: vec![artboard_id] }); responses.add_front(NodeGraphMessage::SelectedNodesSet { nodes: vec![artboard_id] });
} }
load_network_structure(document_network, document_metadata, collapsed); load_network_structure(document_network, document_metadata, collapsed);
@ -572,8 +572,8 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
insert_index, insert_index,
} => { } => {
let mut modify_inputs = ModifyInputsContext::new(document_network, document_metadata, node_graph, responses); let mut modify_inputs = ModifyInputsContext::new(document_network, document_metadata, node_graph, responses);
if let Some(layer) = modify_inputs.create_layer_with_insert_index(id, insert_index, parent) { if let Some(layer) = modify_inputs.create_layer(id, parent, insert_index) {
modify_inputs.insert_image_data(image_frame, layer); ModifyInputsContext::insert_image_data(node_graph, document_network, image_frame, layer, responses);
} }
} }
GraphOperationMessage::NewCustomLayer { GraphOperationMessage::NewCustomLayer {
@ -585,12 +585,13 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
} => { } => {
let mut modify_inputs = ModifyInputsContext::new(document_network, document_metadata, node_graph, responses); let mut modify_inputs = ModifyInputsContext::new(document_network, document_metadata, node_graph, responses);
if let Some(layer) = modify_inputs.create_layer_with_insert_index(id, insert_index, parent) { if let Some(layer) = modify_inputs.create_layer(id, parent, insert_index) {
let new_ids: HashMap<_, _> = nodes.iter().map(|(&id, _)| (id, NodeId(generate_uuid()))).collect(); let new_ids: HashMap<_, _> = nodes.iter().map(|(&id, _)| (id, NodeId(generate_uuid()))).collect();
if let Some(node) = modify_inputs.document_network.nodes.get_mut(&id) { if let Some(node) = modify_inputs.document_network.nodes.get_mut(&id) {
node.alias = alias.clone(); node.alias = alias.clone();
} }
modify_inputs.node_graph.update_click_target(id, &modify_inputs.document_network, Vec::new());
let shift = nodes let shift = nodes
.get(&NodeId(0)) .get(&NodeId(0))
@ -613,7 +614,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
document_node = document_node.map_ids(default_inputs, &new_ids); document_node = document_node.map_ids(default_inputs, &new_ids);
// Insert node into network // Insert node into network
document_network.nodes.insert(node_id, document_node); node_graph.insert_node(node_id, document_node, document_network, &Vec::new());
} }
if let Some(layer_node) = document_network.nodes.get_mut(&layer) { if let Some(layer_node) = document_network.nodes.get_mut(&layer) {
@ -631,7 +632,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
} }
GraphOperationMessage::NewVectorLayer { id, subpaths, parent, insert_index } => { GraphOperationMessage::NewVectorLayer { id, subpaths, parent, insert_index } => {
let mut modify_inputs = ModifyInputsContext::new(document_network, document_metadata, node_graph, responses); let mut modify_inputs = ModifyInputsContext::new(document_network, document_metadata, node_graph, responses);
if let Some(layer) = modify_inputs.create_layer_with_insert_index(id, insert_index, parent) { if let Some(layer) = modify_inputs.create_layer(id, parent, insert_index) {
modify_inputs.insert_vector_data(subpaths, layer); modify_inputs.insert_vector_data(subpaths, layer);
} }
load_network_structure(document_network, document_metadata, collapsed); load_network_structure(document_network, document_metadata, collapsed);
@ -645,7 +646,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
insert_index, insert_index,
} => { } => {
let mut modify_inputs = ModifyInputsContext::new(document_network, document_metadata, node_graph, responses); let mut modify_inputs = ModifyInputsContext::new(document_network, document_metadata, node_graph, responses);
if let Some(layer) = modify_inputs.create_layer_with_insert_index(id, insert_index, parent) { if let Some(layer) = modify_inputs.create_layer(id, parent, insert_index) {
modify_inputs.insert_text(text, font, size, layer); modify_inputs.insert_text(text, font, size, layer);
} }
load_network_structure(document_network, document_metadata, collapsed); load_network_structure(document_network, document_metadata, collapsed);
@ -690,23 +691,30 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
return; return;
}; };
node.metadata.position = position; node.metadata.position = position;
node_graph.update_click_target(node_id, document_network, Vec::new());
responses.add(DocumentMessage::RenderRulers);
responses.add(DocumentMessage::RenderScrollbars);
} }
GraphOperationMessage::SetName { layer, name } => { GraphOperationMessage::SetName { layer, name } => {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
responses.add(GraphOperationMessage::SetNameImpl { layer, name }); responses.add(GraphOperationMessage::SetNameImpl { layer, name });
} }
GraphOperationMessage::SetNameImpl { layer, name } => { GraphOperationMessage::SetNameImpl { layer, name } => {
let Some(node) = document_network.nodes.get_mut(&layer.to_node()) else { return }; if let Some(node) = document_network.nodes.get_mut(&layer.to_node()) {
node.alias = name; node.alias = name;
responses.add(NodeGraphMessage::SendGraph); node_graph.update_click_target(layer.to_node(), document_network, Vec::new());
responses.add(DocumentMessage::RenderRulers);
responses.add(DocumentMessage::RenderScrollbars);
responses.add(NodeGraphMessage::SendGraph);
}
} }
GraphOperationMessage::SetNodeInput { node_id, input_index, input } => { GraphOperationMessage::SetNodeInput { node_id, input_index, input } => {
if ModifyInputsContext::set_input(document_network, node_id, input_index, input, true) { if ModifyInputsContext::set_input(node_graph, document_network, &Vec::new(), node_id, input_index, input, true) {
load_network_structure(document_network, document_metadata, collapsed); load_network_structure(document_network, document_metadata, collapsed);
} }
} }
GraphOperationMessage::ShiftUpstream { node_id, shift, shift_self } => { GraphOperationMessage::ShiftUpstream { node_id, shift, shift_self } => {
ModifyInputsContext::shift_upstream(document_network, node_id, shift, shift_self); ModifyInputsContext::shift_upstream(node_graph, document_network, &Vec::new(), node_id, shift, shift_self);
} }
GraphOperationMessage::ToggleSelectedVisibility => { GraphOperationMessage::ToggleSelectedVisibility => {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
@ -746,11 +754,11 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
GraphOperationMessage::ToggleSelectedLocked => { GraphOperationMessage::ToggleSelectedLocked => {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
// If any of the selected nodes are hidden, show them all. Otherwise, hide them all. // If any of the selected nodes are locked, show them all. Otherwise, hide them all.
let visible = !selected_nodes.selected_layers(&document_metadata).all(|layer| document_metadata.node_is_locked(layer.to_node())); let locked = !selected_nodes.selected_layers(&document_metadata).all(|layer| document_metadata.node_is_locked(layer.to_node()));
for layer in selected_nodes.selected_layers(&document_metadata) { for layer in selected_nodes.selected_layers(&document_metadata) {
responses.add(GraphOperationMessage::SetVisibility { node_id: layer.to_node(), visible }); responses.add(GraphOperationMessage::SetLocked { node_id: layer.to_node(), locked });
} }
} }
GraphOperationMessage::ToggleLocked { node_id } => { GraphOperationMessage::ToggleLocked { node_id } => {
@ -796,7 +804,7 @@ fn usvg_transform(c: usvg::Transform) -> DAffine2 {
} }
fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, transform: DAffine2, id: NodeId, parent: LayerNodeIdentifier, insert_index: isize) { fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, transform: DAffine2, id: NodeId, parent: LayerNodeIdentifier, insert_index: isize) {
let Some(layer) = modify_inputs.create_layer_with_insert_index(id, insert_index, parent) else { let Some(layer) = modify_inputs.create_layer(id, parent, insert_index) else {
return; return;
}; };
modify_inputs.layer_node = Some(layer); modify_inputs.layer_node = Some(layer);

View File

@ -45,7 +45,7 @@ pub enum VectorDataModification {
UpdateSubpaths { subpaths: Vec<Subpath<ManipulatorGroupId>> }, UpdateSubpaths { subpaths: Vec<Subpath<ManipulatorGroupId>> },
} }
// TODO: Generalize for any network, rewrite as static functions since there only a few fields are used for each function, so when calling only the necessary data will be provided // TODO: This is helpful to prevent passing the same arguments to multiple functions, but is currently inefficient due to the collect_outwards_wires. Move it into a function and use only when needed.
/// NodeGraphMessage or GraphOperationMessage cannot be added in ModifyInputsContext, since the functions are called by both messages handlers /// NodeGraphMessage or GraphOperationMessage cannot be added in ModifyInputsContext, since the functions are called by both messages handlers
pub struct ModifyInputsContext<'a> { pub struct ModifyInputsContext<'a> {
pub document_metadata: &'a mut DocumentMetadata, pub document_metadata: &'a mut DocumentMetadata,
@ -94,7 +94,8 @@ impl<'a> ModifyInputsContext<'a> {
} }
pub fn insert_between( pub fn insert_between(
&mut self, node_graph: &mut NodeGraphMessageHandler,
document_network: &mut NodeNetwork,
id: NodeId, id: NodeId,
mut new_node: DocumentNode, mut new_node: DocumentNode,
new_node_input: NodeInput, new_node_input: NodeInput,
@ -104,34 +105,42 @@ impl<'a> ModifyInputsContext<'a> {
post_node_input_index: usize, post_node_input_index: usize,
shift_upstream: IVec2, shift_upstream: IVec2,
) -> Option<NodeId> { ) -> Option<NodeId> {
assert!(!self.document_network.nodes.contains_key(&id), "Creating already existing node"); assert!(!document_network.nodes.contains_key(&id), "Creating already existing node");
let pre_node = self.document_network.nodes.get_mut(&new_node_input.as_node().expect("Input should reference a node"))?; let pre_node = document_network.nodes.get_mut(&new_node_input.as_node().expect("Input should reference a node"))?;
new_node.metadata.position = pre_node.metadata.position; new_node.metadata.position = pre_node.metadata.position;
let post_node = self.document_network.nodes.get_mut(&post_node_id)?; let post_node = document_network.nodes.get_mut(&post_node_id)?;
new_node.inputs[new_node_input_index] = new_node_input; new_node.inputs[new_node_input_index] = new_node_input;
post_node.inputs[post_node_input_index] = post_node_input; post_node.inputs[post_node_input_index] = post_node_input;
self.document_network.nodes.insert(id, new_node); node_graph.insert_node(id, new_node, document_network, &Vec::new());
ModifyInputsContext::shift_upstream(self.document_network, id, shift_upstream, false); ModifyInputsContext::shift_upstream(node_graph, document_network, &Vec::new(), id, shift_upstream, false);
Some(id) Some(id)
} }
pub fn insert_node_before(&mut self, new_id: NodeId, node_id: NodeId, input_index: usize, mut document_node: DocumentNode, offset: IVec2) -> Option<NodeId> { pub fn insert_node_before(
assert!(!self.document_network.nodes.contains_key(&new_id), "Creating already existing node"); node_graph: &mut NodeGraphMessageHandler,
document_network: &mut NodeNetwork,
new_id: NodeId,
node_id: NodeId,
input_index: usize,
mut document_node: DocumentNode,
offset: IVec2,
) -> Option<NodeId> {
assert!(!document_network.nodes.contains_key(&new_id), "Creating already existing node");
let post_node = self.document_network.nodes.get_mut(&node_id)?; let post_node = document_network.nodes.get_mut(&node_id)?;
post_node.inputs[input_index] = NodeInput::node(new_id, 0); post_node.inputs[input_index] = NodeInput::node(new_id, 0);
document_node.metadata.position = post_node.metadata.position + offset; document_node.metadata.position = post_node.metadata.position + offset;
self.document_network.nodes.insert(new_id, document_node); node_graph.insert_node(new_id, document_node, document_network, &Vec::new());
Some(new_id) Some(new_id)
} }
/// Inserts a node as an export. If there is already a root node connected to the export, that node will be connected to the new node at node_input_index /// Inserts a node as an export. If there is already a root node connected to the export, that node will be connected to the new node at node_input_index
pub fn insert_node_as_primary_export(document_network: &mut NodeNetwork, id: NodeId, mut new_node: DocumentNode) -> Option<NodeId> { pub fn insert_node_as_primary_export(node_graph: &mut NodeGraphMessageHandler, document_network: &mut NodeNetwork, id: NodeId, mut new_node: DocumentNode) -> Option<NodeId> {
assert!(!document_network.nodes.contains_key(&id), "Creating already existing node"); assert!(!document_network.nodes.contains_key(&id), "Creating already existing node");
if let Some(root_node) = document_network.get_root_node() { if let Some(root_node) = document_network.get_root_node() {
@ -140,7 +149,7 @@ impl<'a> ModifyInputsContext<'a> {
// Insert whatever non artboard node previously fed into export as a child of the new node // Insert whatever non artboard node previously fed into export as a child of the new node
let node_input_index = if new_node.is_artboard() && !previous_root_node.is_artboard() { 1 } else { 0 }; let node_input_index = if new_node.is_artboard() && !previous_root_node.is_artboard() { 1 } else { 0 };
new_node.inputs[node_input_index] = NodeInput::node(root_node.id, root_node.output_index); new_node.inputs[node_input_index] = NodeInput::node(root_node.id, root_node.output_index);
ModifyInputsContext::shift_upstream(document_network, root_node.id, IVec2::new(8, 0), true); ModifyInputsContext::shift_upstream(node_graph, document_network, &Vec::new(), root_node.id, IVec2::new(8, 0), true);
} }
let Some(export) = document_network.exports.get_mut(0) else { let Some(export) = document_network.exports.get_mut(0) else {
@ -149,9 +158,9 @@ impl<'a> ModifyInputsContext<'a> {
}; };
*export = NodeInput::node(id, 0); *export = NodeInput::node(id, 0);
document_network.nodes.insert(id, new_node); node_graph.insert_node(id, new_node, document_network, &Vec::new());
ModifyInputsContext::shift_upstream(document_network, id, IVec2::new(-8, 3), false); ModifyInputsContext::shift_upstream(node_graph, document_network, &Vec::new(), id, IVec2::new(-8, 3), false);
Some(id) Some(id)
} }
@ -243,7 +252,9 @@ impl<'a> ModifyInputsContext<'a> {
(Some(post_node_id), pre_node_id, post_node_input_index) (Some(post_node_id), pre_node_id, post_node_input_index)
} }
pub fn create_layer(&mut self, new_id: NodeId, parent: LayerNodeIdentifier, skip_layer_nodes: usize) -> Option<NodeId> { pub fn create_layer(&mut self, new_id: NodeId, parent: LayerNodeIdentifier, insert_index: isize) -> Option<NodeId> {
let skip_layer_nodes = if insert_index < 0 { (-1 - insert_index) as usize } else { insert_index as usize };
assert!(!self.document_network.nodes.contains_key(&new_id), "Creating already existing layer"); assert!(!self.document_network.nodes.contains_key(&new_id), "Creating already existing layer");
// TODO: Smarter placement of layers into artboards https://github.com/GraphiteEditor/Graphite/issues/1507 // TODO: Smarter placement of layers into artboards https://github.com/GraphiteEditor/Graphite/issues/1507
@ -259,11 +270,13 @@ impl<'a> ModifyInputsContext<'a> {
} }
let new_layer_node = resolve_document_node_type("Merge").expect("Merge node").default_document_node(); let new_layer_node = resolve_document_node_type("Merge").expect("Merge node").default_document_node();
let (post_node_id, pre_node_id, post_node_input_index) = Self::get_post_node_with_index(self.document_network, parent, skip_layer_nodes); let (post_node_id, pre_node_id, post_node_input_index) = ModifyInputsContext::get_post_node_with_index(self.document_network, parent, skip_layer_nodes);
if let Some(post_node_id) = post_node_id { if let Some(post_node_id) = post_node_id {
if let Some(pre_node_id) = pre_node_id { if let Some(pre_node_id) = pre_node_id {
self.insert_between( ModifyInputsContext::insert_between(
self.node_graph,
self.document_network,
new_id, new_id,
new_layer_node, new_layer_node,
NodeInput::node(pre_node_id, 0), NodeInput::node(pre_node_id, 0),
@ -275,23 +288,18 @@ impl<'a> ModifyInputsContext<'a> {
); );
} else { } else {
let offset = if post_node_input_index == 1 { IVec2::new(-8, 3) } else { IVec2::new(0, 3) }; let offset = if post_node_input_index == 1 { IVec2::new(-8, 3) } else { IVec2::new(0, 3) };
self.insert_node_before(new_id, post_node_id, post_node_input_index, new_layer_node, offset); ModifyInputsContext::insert_node_before(self.node_graph, self.document_network, new_id, post_node_id, post_node_input_index, new_layer_node, offset);
}; };
} else { } else {
// If post_node does not exist, then network is empty // If post_node does not exist, then network is empty
ModifyInputsContext::insert_node_as_primary_export(self.document_network, new_id, new_layer_node); ModifyInputsContext::insert_node_as_primary_export(self.node_graph, self.document_network, new_id, new_layer_node);
} }
Some(new_id) Some(new_id)
} }
pub fn create_layer_with_insert_index(&mut self, new_id: NodeId, insert_index: isize, parent: LayerNodeIdentifier) -> Option<NodeId> {
let skip_layer_nodes = if insert_index < 0 { (-1 - insert_index) as usize } else { insert_index as usize };
self.create_layer(new_id, parent, skip_layer_nodes)
}
/// Creates an artboard that outputs to the output node. /// Creates an artboard that outputs to the output node.
pub fn create_artboard(&mut self, new_id: NodeId, artboard: Artboard) -> Option<NodeId> { pub fn create_artboard(node_graph: &mut NodeGraphMessageHandler, document_network: &mut NodeNetwork, new_id: NodeId, artboard: Artboard) -> Option<NodeId> {
let artboard_node = resolve_document_node_type("Artboard").expect("Node").to_document_node_default_inputs( let artboard_node = resolve_document_node_type("Artboard").expect("Node").to_document_node_default_inputs(
[ [
Some(NodeInput::value(TaggedValue::ArtboardGroup(graphene_std::ArtboardGroup::EMPTY), true)), Some(NodeInput::value(TaggedValue::ArtboardGroup(graphene_std::ArtboardGroup::EMPTY), true)),
@ -304,11 +312,11 @@ impl<'a> ModifyInputsContext<'a> {
Default::default(), Default::default(),
); );
ModifyInputsContext::insert_node_as_primary_export(self.document_network, new_id, artboard_node) ModifyInputsContext::insert_node_as_primary_export(node_graph, document_network, new_id, artboard_node)
} }
pub fn insert_vector_data(&mut self, subpaths: Vec<Subpath<ManipulatorGroupId>>, layer: NodeId) { pub fn insert_vector_data(&mut self, subpaths: Vec<Subpath<ManipulatorGroupId>>, layer: NodeId) {
let shape = { let shape = {
let node_type = resolve_document_node_type("Shape").expect("Shape node does not exist"); let node_type: &crate::messages::portfolio::document::node_graph::document_node_types::DocumentNodeDefinition = resolve_document_node_type("Shape").expect("Shape node does not exist");
node_type.to_document_node_default_inputs([Some(NodeInput::value(TaggedValue::Subpaths(subpaths), false))], Default::default()) node_type.to_document_node_default_inputs([Some(NodeInput::value(TaggedValue::Subpaths(subpaths), false))], Default::default())
}; };
let transform = resolve_document_node_type("Transform").expect("Transform node does not exist").default_document_node(); let transform = resolve_document_node_type("Transform").expect("Transform node does not exist").default_document_node();
@ -316,13 +324,13 @@ impl<'a> ModifyInputsContext<'a> {
let stroke = resolve_document_node_type("Stroke").expect("Stroke node does not exist").default_document_node(); let stroke = resolve_document_node_type("Stroke").expect("Stroke node does not exist").default_document_node();
let stroke_id = NodeId(generate_uuid()); let stroke_id = NodeId(generate_uuid());
self.insert_node_before(stroke_id, layer, 1, stroke, IVec2::new(-8, 0)); ModifyInputsContext::insert_node_before(self.node_graph, self.document_network, stroke_id, layer, 1, stroke, IVec2::new(-8, 0));
let fill_id = NodeId(generate_uuid()); let fill_id = NodeId(generate_uuid());
self.insert_node_before(fill_id, stroke_id, 0, fill, IVec2::new(-8, 0)); ModifyInputsContext::insert_node_before(self.node_graph, self.document_network, fill_id, stroke_id, 0, fill, IVec2::new(-8, 0));
let transform_id = NodeId(generate_uuid()); let transform_id = NodeId(generate_uuid());
self.insert_node_before(transform_id, fill_id, 0, transform, IVec2::new(-8, 0)); ModifyInputsContext::insert_node_before(self.node_graph, self.document_network, transform_id, fill_id, 0, transform, IVec2::new(-8, 0));
let shape_id = NodeId(generate_uuid()); let shape_id = NodeId(generate_uuid());
self.insert_node_before(shape_id, transform_id, 0, shape, IVec2::new(-8, 0)); ModifyInputsContext::insert_node_before(self.node_graph, self.document_network, shape_id, transform_id, 0, shape, IVec2::new(-8, 0));
self.responses.add(NodeGraphMessage::RunDocumentGraph); self.responses.add(NodeGraphMessage::RunDocumentGraph);
} }
@ -341,17 +349,17 @@ impl<'a> ModifyInputsContext<'a> {
let stroke = resolve_document_node_type("Stroke").expect("Stroke node does not exist").default_document_node(); let stroke = resolve_document_node_type("Stroke").expect("Stroke node does not exist").default_document_node();
let stroke_id = NodeId(generate_uuid()); let stroke_id = NodeId(generate_uuid());
self.insert_node_before(stroke_id, layer, 1, stroke, IVec2::new(-8, 0)); ModifyInputsContext::insert_node_before(self.node_graph, self.document_network, stroke_id, layer, 1, stroke, IVec2::new(-8, 0));
let fill_id = NodeId(generate_uuid()); let fill_id = NodeId(generate_uuid());
self.insert_node_before(fill_id, stroke_id, 0, fill, IVec2::new(-8, 0)); ModifyInputsContext::insert_node_before(self.node_graph, self.document_network, fill_id, stroke_id, 0, fill, IVec2::new(-8, 0));
let transform_id = NodeId(generate_uuid()); let transform_id = NodeId(generate_uuid());
self.insert_node_before(transform_id, fill_id, 0, transform, IVec2::new(-8, 0)); ModifyInputsContext::insert_node_before(self.node_graph, self.document_network, transform_id, fill_id, 0, transform, IVec2::new(-8, 0));
let text_id = NodeId(generate_uuid()); let text_id = NodeId(generate_uuid());
self.insert_node_before(text_id, transform_id, 0, text, IVec2::new(-8, 0)); ModifyInputsContext::insert_node_before(self.node_graph, self.document_network, text_id, transform_id, 0, text, IVec2::new(-8, 0));
self.responses.add(NodeGraphMessage::RunDocumentGraph); self.responses.add(NodeGraphMessage::RunDocumentGraph);
} }
pub fn insert_image_data(&mut self, image_frame: ImageFrame<Color>, layer: NodeId) { pub fn insert_image_data(node_graph: &mut NodeGraphMessageHandler, document_network: &mut NodeNetwork, image_frame: ImageFrame<Color>, layer: NodeId, responses: &mut VecDeque<Message>) {
let image = { let image = {
let node_type = resolve_document_node_type("Image").expect("Image node does not exist"); let node_type = resolve_document_node_type("Image").expect("Image node does not exist");
node_type.to_document_node_default_inputs([Some(NodeInput::value(TaggedValue::ImageFrame(image_frame), false))], Default::default()) node_type.to_document_node_default_inputs([Some(NodeInput::value(TaggedValue::ImageFrame(image_frame), false))], Default::default())
@ -359,15 +367,20 @@ impl<'a> ModifyInputsContext<'a> {
let transform = resolve_document_node_type("Transform").expect("Transform node does not exist").default_document_node(); let transform = resolve_document_node_type("Transform").expect("Transform node does not exist").default_document_node();
let transform_id = NodeId(generate_uuid()); let transform_id = NodeId(generate_uuid());
self.insert_node_before(transform_id, layer, 1, transform, IVec2::new(-8, 0)); ModifyInputsContext::insert_node_before(node_graph, document_network, transform_id, layer, 1, transform, IVec2::new(-8, 0));
let image_id = NodeId(generate_uuid()); let image_id = NodeId(generate_uuid());
self.insert_node_before(image_id, transform_id, 0, image, IVec2::new(-8, 0)); ModifyInputsContext::insert_node_before(node_graph, document_network, image_id, transform_id, 0, image, IVec2::new(-8, 0));
self.responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::RunDocumentGraph);
} }
pub fn shift_upstream(network: &mut NodeNetwork, node_id: NodeId, shift: IVec2, shift_self: bool) { pub fn shift_upstream(node_graph: &mut NodeGraphMessageHandler, document_network: &mut NodeNetwork, network_path: &Vec<NodeId>, node_id: NodeId, shift: IVec2, shift_self: bool) {
let Some(network) = document_network.nested_network(network_path) else {
log::error!("Could not get nested network for shift_upstream");
return;
};
let mut shift_nodes = HashSet::new(); let mut shift_nodes = HashSet::new();
if shift_self { if shift_self {
shift_nodes.insert(node_id); shift_nodes.insert(node_id);
@ -385,8 +398,9 @@ impl<'a> ModifyInputsContext<'a> {
} }
for node_id in shift_nodes { for node_id in shift_nodes {
if let Some(node) = network.nodes.get_mut(&node_id) { if let Some(node) = document_network.nodes.get_mut(&node_id) {
node.metadata.position += shift; node.metadata.position += shift;
node_graph.update_click_target(node_id, document_network, network_path.clone());
} }
} }
} }
@ -424,7 +438,7 @@ impl<'a> ModifyInputsContext<'a> {
}; };
let mut new_document_node = node_type.to_document_node_default_inputs([new_input], metadata); let mut new_document_node = node_type.to_document_node_default_inputs([new_input], metadata);
update_input(&mut new_document_node.inputs, node_id, self.document_metadata); update_input(&mut new_document_node.inputs, node_id, self.document_metadata);
self.document_network.nodes.insert(node_id, new_document_node); self.node_graph.insert_node(node_id, new_document_node, self.document_network, &Vec::new());
let upstream_nodes = self let upstream_nodes = self
.document_network .document_network
@ -434,6 +448,7 @@ impl<'a> ModifyInputsContext<'a> {
for node_id in upstream_nodes { for node_id in upstream_nodes {
let Some(node) = self.document_network.nodes.get_mut(&node_id) else { continue }; let Some(node) = self.document_network.nodes.get_mut(&node_id) else { continue };
node.metadata.position.x -= 8; node.metadata.position.x -= 8;
self.node_graph.update_click_target(node_id, self.document_network, Vec::new());
} }
} }
@ -503,14 +518,32 @@ impl<'a> ModifyInputsContext<'a> {
} }
/// Returns true if the network structure is updated /// Returns true if the network structure is updated
pub fn set_input(network: &mut NodeNetwork, node_id: NodeId, input_index: usize, input: NodeInput, is_document_network: bool) -> bool { pub fn set_input(
node_graph: &mut NodeGraphMessageHandler,
document_network: &mut NodeNetwork,
network_path: &Vec<NodeId>,
node_id: NodeId,
input_index: usize,
input: NodeInput,
is_document_network: bool,
) -> bool {
let Some(network) = document_network.nested_network_mut(network_path) else {
log::error!("Could not get nested network for set_input");
return false;
};
if let Some(node) = network.nodes.get_mut(&node_id) { if let Some(node) = network.nodes.get_mut(&node_id) {
let Some(node_input) = node.inputs.get_mut(input_index) else { let Some(node_input) = node.inputs.get_mut(input_index) else {
log::error!("Tried to set input {input_index} to {input:?}, but the index was invalid. Node {node_id}:\n{node:#?}"); log::error!("Tried to set input {input_index} to {input:?}, but the index was invalid. Node {node_id}:\n{node:#?}");
return false; return false;
}; };
let structure_changed = node_input.as_node().is_some() || input.as_node().is_some(); let structure_changed = node_input.as_node().is_some() || input.as_node().is_some();
let previously_exposed = node_input.is_exposed();
*node_input = input; *node_input = input;
let currently_exposed = node_input.is_exposed();
if previously_exposed != currently_exposed {
node_graph.update_click_target(node_id, document_network, network_path.clone());
}
// Only load network structure for changes to document_network // Only load network structure for changes to document_network
structure_changed && is_document_network structure_changed && is_document_network
@ -519,7 +552,11 @@ impl<'a> ModifyInputsContext<'a> {
log::error!("Tried to set export {input_index} to {input:?}, but the index was invalid. Network:\n{network:#?}"); log::error!("Tried to set export {input_index} to {input:?}, but the index was invalid. Network:\n{network:#?}");
return false; return false;
}; };
let previously_exposed = export.is_exposed();
*export = input; *export = input;
let currently_exposed = export.is_exposed();
if let NodeInput::Node { node_id, output_index, .. } = *export { if let NodeInput::Node { node_id, output_index, .. } = *export {
network.update_root_node(node_id, output_index); network.update_root_node(node_id, output_index);
} else if let NodeInput::Value { .. } = *export { } else if let NodeInput::Value { .. } = *export {
@ -530,6 +567,10 @@ impl<'a> ModifyInputsContext<'a> {
log::error!("Network export input not supported"); log::error!("Network export input not supported");
} }
if previously_exposed != currently_exposed {
node_graph.update_click_target(node_id, document_network, network_path.clone());
}
// Only load network structure for changes to document_network // Only load network structure for changes to document_network
is_document_network is_document_network
} else { } else {
@ -717,13 +758,13 @@ impl<'a> ModifyInputsContext<'a> {
/// Deletes all nodes in `node_ids` and any sole dependents in the horizontal chain if the node to delete is a layer node. /// Deletes all nodes in `node_ids` and any sole dependents in the horizontal chain if the node to delete is a layer node.
pub fn delete_nodes( pub fn delete_nodes(
node_graph: &mut NodeGraphMessageHandler,
document_network: &mut NodeNetwork, document_network: &mut NodeNetwork,
selected_nodes: &mut SelectedNodes, selected_nodes: &mut SelectedNodes,
node_ids: Vec<NodeId>, node_ids: Vec<NodeId>,
reconnect: bool, reconnect: bool,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
network_path: Vec<NodeId>, network_path: Vec<NodeId>,
resolved_types: &ResolvedDocumentNodeTypes,
) { ) {
let Some(network) = document_network.nested_network_for_selected_nodes(&network_path, selected_nodes.selected_nodes_ref().iter()) else { let Some(network) = document_network.nested_network_for_selected_nodes(&network_path, selected_nodes.selected_nodes_ref().iter()) else {
return; return;
@ -798,21 +839,21 @@ impl<'a> ModifyInputsContext<'a> {
selected_nodes.add_selected_nodes(delete_nodes.iter().cloned().collect(), document_network, &network_path); selected_nodes.add_selected_nodes(delete_nodes.iter().cloned().collect(), document_network, &network_path);
for delete_node_id in delete_nodes { for delete_node_id in delete_nodes {
ModifyInputsContext::remove_node(document_network, selected_nodes, delete_node_id, reconnect, responses, &network_path, resolved_types); ModifyInputsContext::remove_node(node_graph, document_network, selected_nodes, delete_node_id, reconnect, responses, &network_path);
} }
} }
/// Tries to remove a node from the network, returning `true` on success. /// Tries to remove a node from the network, returning `true` on success.
fn remove_node( fn remove_node(
node_graph: &mut NodeGraphMessageHandler,
document_network: &mut NodeNetwork, document_network: &mut NodeNetwork,
selected_nodes: &mut SelectedNodes, selected_nodes: &mut SelectedNodes,
node_id: NodeId, node_id: NodeId,
reconnect: bool, reconnect: bool,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
network_path: &Vec<NodeId>, network_path: &Vec<NodeId>,
resolved_types: &ResolvedDocumentNodeTypes,
) -> bool { ) -> bool {
if !ModifyInputsContext::remove_references_from_network(document_network, node_id, reconnect, &network_path, resolved_types) { if !ModifyInputsContext::remove_references_from_network(node_graph, document_network, node_id, reconnect, &network_path) {
log::error!("could not remove_references_from_network"); log::error!("could not remove_references_from_network");
return false; return false;
} }
@ -820,19 +861,14 @@ impl<'a> ModifyInputsContext<'a> {
network.nodes.remove(&node_id); network.nodes.remove(&node_id);
selected_nodes.retain_selected_nodes(|&id| id != node_id || id == network.exports_metadata.0 || id == network.imports_metadata.0); selected_nodes.retain_selected_nodes(|&id| id != node_id || id == network.exports_metadata.0 || id == network.imports_metadata.0);
node_graph.update_click_target(node_id, document_network, network_path.clone());
responses.add(BroadcastEvent::SelectionChanged); responses.add(BroadcastEvent::SelectionChanged);
true true
} }
pub fn remove_references_from_network( pub fn remove_references_from_network(node_graph: &mut NodeGraphMessageHandler, document_network: &mut NodeNetwork, deleting_node_id: NodeId, reconnect: bool, network_path: &Vec<NodeId>) -> bool {
document_network: &mut NodeNetwork,
deleting_node_id: NodeId,
reconnect: bool,
network_path: &Vec<NodeId>,
resolved_types: &ResolvedDocumentNodeTypes,
) -> bool {
let Some(network) = document_network.nested_network(network_path) else { return false }; let Some(network) = document_network.nested_network(network_path) else { return false };
let mut reconnect_to_input: Option<NodeInput> = None; let mut reconnect_to_input: Option<NodeInput> = None;
@ -888,18 +924,18 @@ impl<'a> ModifyInputsContext<'a> {
can_reconnect = false; can_reconnect = false;
} else { } else {
// Disconnect input // Disconnect input
let tagged_value = TaggedValue::from_type(&ModifyInputsContext::get_input_type(document_network, network_path, node_id, resolved_types, input_index)); let tagged_value = TaggedValue::from_type(&ModifyInputsContext::get_input_type(document_network, network_path, node_id, &node_graph.resolved_types, input_index));
let value_input = NodeInput::value(tagged_value, true); let value_input = NodeInput::value(tagged_value, true);
nodes_to_set_input.push((node_id, input_index, Some(value_input))); nodes_to_set_input.push((node_id, input_index, Some(value_input)));
} }
} }
let Some(network) = document_network.nested_network_mut(network_path) else { return false }; //let Some(network) = document_network.nested_network(network_path) else { return false };
if let Previewing::Yes { root_node_to_restore } = network.previewing { if let Some(Previewing::Yes { root_node_to_restore }) = document_network.nested_network(network_path).map(|network| &network.previewing) {
if let Some(root_node_to_restore) = root_node_to_restore { if let Some(root_node_to_restore) = root_node_to_restore {
if root_node_to_restore.id == deleting_node_id { if root_node_to_restore.id == deleting_node_id {
network.start_previewing_without_restore(); document_network.nested_network_mut(network_path).unwrap().start_previewing_without_restore();
} }
} }
} }
@ -908,40 +944,62 @@ impl<'a> ModifyInputsContext<'a> {
for (node_id, input_index, value_input) in nodes_to_set_input { for (node_id, input_index, value_input) in nodes_to_set_input {
if let Some(value_input) = value_input { if let Some(value_input) = value_input {
// Disconnect input to root node only if not previewing // Disconnect input to root node only if not previewing
if node_id != network.exports_metadata.0 || matches!(&network.previewing, Previewing::No) { if document_network
ModifyInputsContext::set_input(network, node_id, input_index, value_input, is_document_network); .nested_network(network_path)
} else if let Previewing::Yes { root_node_to_restore } = network.previewing { .is_some_and(|network| node_id != network.exports_metadata.0 || matches!(&network.previewing, Previewing::No))
{
ModifyInputsContext::set_input(node_graph, document_network, network_path, node_id, input_index, value_input, is_document_network);
} else if let Some(Previewing::Yes { root_node_to_restore }) = document_network.nested_network(network_path).map(|network| &network.previewing) {
if let Some(root_node) = root_node_to_restore { if let Some(root_node) = root_node_to_restore {
if node_id == root_node.id { if node_id == root_node.id {
network.start_previewing_without_restore(); document_network.nested_network_mut(network_path).unwrap().start_previewing_without_restore();
} else { } else {
ModifyInputsContext::set_input(network, node_id, input_index, NodeInput::node(root_node.id, root_node.output_index), is_document_network); ModifyInputsContext::set_input(
node_graph,
document_network,
network_path,
node_id,
input_index,
NodeInput::node(root_node.id, root_node.output_index),
is_document_network,
);
} }
} else { } else {
ModifyInputsContext::set_input(network, node_id, input_index, value_input, is_document_network); ModifyInputsContext::set_input(node_graph, document_network, network_path, node_id, input_index, value_input, is_document_network);
} }
} }
} }
// Reconnect to node upstream of the deleted node // Reconnect to node upstream of the deleted node
else if node_id != network.exports_metadata.0 || matches!(network.previewing, Previewing::No) { else if document_network
.nested_network(network_path)
.is_some_and(|network| node_id != network.exports_metadata.0 || matches!(network.previewing, Previewing::No))
{
if let Some(reconnect_to_input) = reconnect_to_input.clone() { if let Some(reconnect_to_input) = reconnect_to_input.clone() {
ModifyInputsContext::set_input(network, node_id, input_index, reconnect_to_input, is_document_network); ModifyInputsContext::set_input(node_graph, document_network, network_path, node_id, input_index, reconnect_to_input, is_document_network);
} }
} }
// Reconnect previous root node to the export, or disconnect export // Reconnect previous root node to the export, or disconnect export
else if let Previewing::Yes { root_node_to_restore } = network.previewing { else if let Some(Previewing::Yes { root_node_to_restore }) = document_network.nested_network(network_path).map(|network| &network.previewing) {
if let Some(root_node) = root_node_to_restore { if let Some(root_node) = root_node_to_restore {
ModifyInputsContext::set_input(network, node_id, input_index, NodeInput::node(root_node.id, root_node.output_index), is_document_network); ModifyInputsContext::set_input(
node_graph,
document_network,
network_path,
node_id,
input_index,
NodeInput::node(root_node.id, root_node.output_index),
is_document_network,
);
} else if let Some(reconnect_to_input) = reconnect_to_input.clone() { } else if let Some(reconnect_to_input) = reconnect_to_input.clone() {
ModifyInputsContext::set_input(network, node_id, input_index, reconnect_to_input, is_document_network); ModifyInputsContext::set_input(node_graph, document_network, network_path, node_id, input_index, reconnect_to_input, is_document_network);
network.start_previewing_without_restore(); document_network.nested_network_mut(network_path).unwrap().start_previewing_without_restore();
} }
} }
} }
true true
} }
/// Get the [`Type`] for any `node_i`d and `input_index`. The `network_path` is the path to the encapsulating node (including the encapsulating node). The `node_id` is the selected node. /// Get the [`Type`] for any `node_id` and `input_index`. The `network_path` is the path to the encapsulating node (including the encapsulating node). The `node_id` is the selected node.
pub fn get_input_type(document_network: &NodeNetwork, network_path: &Vec<NodeId>, node_id: NodeId, resolved_types: &ResolvedDocumentNodeTypes, input_index: usize) -> Type { pub fn get_input_type(document_network: &NodeNetwork, network_path: &Vec<NodeId>, node_id: NodeId, resolved_types: &ResolvedDocumentNodeTypes, input_index: usize) -> Type {
let Some(network) = document_network.nested_network(&network_path) else { let Some(network) = document_network.nested_network(&network_path) else {
log::error!("Could not get network in get_tagged_value"); log::error!("Could not get network in get_tagged_value");

View File

@ -12,13 +12,16 @@ use crate::messages::prelude::*;
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo}; use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graph_craft::document::NodeNetwork;
pub struct NavigationMessageData<'a> { pub struct NavigationMessageData<'a> {
pub metadata: &'a DocumentMetadata, pub metadata: &'a DocumentMetadata,
pub document_bounds: Option<[DVec2; 2]>,
pub ipp: &'a InputPreprocessorMessageHandler, pub ipp: &'a InputPreprocessorMessageHandler,
pub selection_bounds: Option<[DVec2; 2]>, pub selection_bounds: Option<[DVec2; 2]>,
pub ptz: &'a mut PTZ, pub ptz: &'a mut PTZ,
pub graph_view_overlay_open: bool,
pub document_network: &'a NodeNetwork,
pub node_graph_handler: &'a NodeGraphMessageHandler,
} }
#[derive(Debug, Clone, PartialEq, Default)] #[derive(Debug, Clone, PartialEq, Default)]
@ -32,10 +35,12 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
fn process_message(&mut self, message: NavigationMessage, responses: &mut VecDeque<Message>, data: NavigationMessageData) { fn process_message(&mut self, message: NavigationMessage, responses: &mut VecDeque<Message>, data: NavigationMessageData) {
let NavigationMessageData { let NavigationMessageData {
metadata, metadata,
document_bounds,
ipp, ipp,
selection_bounds, selection_bounds,
ptz, ptz,
graph_view_overlay_open,
document_network,
node_graph_handler,
} = data; } = data;
let old_zoom = ptz.zoom; let old_zoom = ptz.zoom;
@ -51,29 +56,34 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
self.navigation_operation = NavigationOperation::Pan { pan_original_for_abort: ptz.pan }; self.navigation_operation = NavigationOperation::Pan { pan_original_for_abort: ptz.pan };
} }
NavigationMessage::BeginCanvasTilt { was_dispatched_from_menu } => { NavigationMessage::BeginCanvasTilt { was_dispatched_from_menu } => {
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); // If the node graph is open, prevent tilt and instead start panning
responses.add(FrontendMessage::UpdateInputHints { if graph_view_overlay_open {
hint_data: HintData(vec![ responses.add(NavigationMessage::BeginCanvasPan);
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), } else {
HintGroup(vec![HintInfo { responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
key_groups: vec![KeysGroup(vec![Key::Control]).into()], responses.add(FrontendMessage::UpdateInputHints {
key_groups_mac: None, hint_data: HintData(vec![
mouse: None, HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
label: String::from("Snap 15°"), HintGroup(vec![HintInfo {
plus: false, key_groups: vec![KeysGroup(vec![Key::Control]).into()],
slash: false, key_groups_mac: None,
}]), mouse: None,
]), label: String::from("Snap 15°"),
}); plus: false,
slash: false,
}]),
]),
});
self.navigation_operation = NavigationOperation::Tilt { self.navigation_operation = NavigationOperation::Tilt {
tilt_original_for_abort: ptz.tilt, tilt_original_for_abort: ptz.tilt,
tilt_raw_not_snapped: ptz.tilt, tilt_raw_not_snapped: ptz.tilt,
snap: false, snap: false,
}; };
self.mouse_position = ipp.mouse.position; self.mouse_position = ipp.mouse.position;
self.finish_operation_with_click = was_dispatched_from_menu; self.finish_operation_with_click = was_dispatched_from_menu;
}
} }
NavigationMessage::BeginCanvasZoom => { NavigationMessage::BeginCanvasZoom => {
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::ZoomIn }); responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::ZoomIn });
@ -99,15 +109,28 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
self.mouse_position = ipp.mouse.position; self.mouse_position = ipp.mouse.position;
} }
NavigationMessage::CanvasPan { delta } => { NavigationMessage::CanvasPan { delta } => {
let transformed_delta = metadata.document_to_viewport.inverse().transform_vector2(delta); let transformed_delta = if !graph_view_overlay_open {
metadata.document_to_viewport.inverse().transform_vector2(delta)
} else {
let Some(network) = document_network.nested_network(&node_graph_handler.network) else {
return;
};
network.node_graph_to_viewport.inverse().transform_vector2(delta)
};
ptz.pan += transformed_delta; ptz.pan += transformed_delta;
responses.add(BroadcastEvent::CanvasTransformed); responses.add(BroadcastEvent::CanvasTransformed);
self.create_document_transform(ipp.viewport_bounds.center(), ptz, responses); self.create_document_transform(ipp.viewport_bounds.center(), ptz, responses);
} }
NavigationMessage::CanvasPanByViewportFraction { delta } => { NavigationMessage::CanvasPanByViewportFraction { delta } => {
let transformed_delta = metadata.document_to_viewport.inverse().transform_vector2(delta * ipp.viewport_bounds.size()); let transformed_delta = if !graph_view_overlay_open {
metadata.document_to_viewport.inverse().transform_vector2(delta * ipp.viewport_bounds.size())
} else {
let Some(network) = document_network.nested_network(&node_graph_handler.network) else {
return;
};
network.node_graph_to_viewport.inverse().transform_vector2(delta * ipp.viewport_bounds.size())
};
ptz.pan += transformed_delta; ptz.pan += transformed_delta;
self.create_document_transform(ipp.viewport_bounds.center(), ptz, responses); self.create_document_transform(ipp.viewport_bounds.center(), ptz, responses);
} }
@ -148,12 +171,24 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
if ipp.mouse.scroll_delta.y > 0. { if ipp.mouse.scroll_delta.y > 0. {
zoom_factor = 1. / zoom_factor zoom_factor = 1. / zoom_factor
} }
let document_bounds = if !graph_view_overlay_open {
// TODO: Cache this in node graph coordinates and apply the transform to the rectangle to get viewport coordinates
metadata.document_bounds_viewport_space()
} else {
node_graph_handler.graph_bounds_viewport_space(document_network)
};
zoom_factor *= Self::clamp_zoom(ptz.zoom * zoom_factor, document_bounds, old_zoom, ipp); zoom_factor *= Self::clamp_zoom(ptz.zoom * zoom_factor, document_bounds, old_zoom, ipp);
responses.add(self.center_zoom(ipp.viewport_bounds.size(), zoom_factor, ipp.mouse.position)); responses.add(self.center_zoom(ipp.viewport_bounds.size(), zoom_factor, ipp.mouse.position));
responses.add(NavigationMessage::CanvasZoomSet { zoom_factor: ptz.zoom * zoom_factor }); responses.add(NavigationMessage::CanvasZoomSet { zoom_factor: ptz.zoom * zoom_factor });
} }
NavigationMessage::CanvasZoomSet { zoom_factor } => { NavigationMessage::CanvasZoomSet { zoom_factor } => {
let document_bounds = if !graph_view_overlay_open {
// TODO: Cache this in node graph coordinates and apply the transform to the rectangle to get viewport coordinates
metadata.document_bounds_viewport_space()
} else {
node_graph_handler.graph_bounds_viewport_space(document_network)
};
ptz.zoom = zoom_factor.clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX); ptz.zoom = zoom_factor.clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
ptz.zoom *= Self::clamp_zoom(ptz.zoom, document_bounds, old_zoom, ipp); ptz.zoom *= Self::clamp_zoom(ptz.zoom, document_bounds, old_zoom, ipp);
responses.add(PortfolioMessage::UpdateDocumentWidgets); responses.add(PortfolioMessage::UpdateDocumentWidgets);
@ -201,14 +236,35 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
bounds: [pos1, pos2], bounds: [pos1, pos2],
prevent_zoom_past_100, prevent_zoom_past_100,
} => { } => {
let v1 = metadata.document_to_viewport.inverse().transform_point2(DVec2::ZERO); let v1 = if !graph_view_overlay_open {
let v2 = metadata.document_to_viewport.inverse().transform_point2(ipp.viewport_bounds.size()); metadata.document_to_viewport.inverse().transform_point2(DVec2::ZERO)
} else {
let Some(network) = document_network.nested_network(&node_graph_handler.network) else {
return;
};
network.node_graph_to_viewport.inverse().transform_point2(DVec2::ZERO)
};
let v2 = if !graph_view_overlay_open {
metadata.document_to_viewport.inverse().transform_point2(ipp.viewport_bounds.size())
} else {
let Some(network) = document_network.nested_network(&node_graph_handler.network) else {
return;
};
network.node_graph_to_viewport.inverse().transform_point2(ipp.viewport_bounds.size())
};
let center = ((v1 + v2) - (pos1 + pos2)) / 2.; let center = ((v1 + v2) - (pos1 + pos2)) / 2.;
let size = 1. / ((pos2 - pos1) / (v2 - v1)); let size = 1. / ((pos2 - pos1) / (v2 - v1));
let new_scale = size.min_element(); let new_scale = size.min_element();
let viewport_change = metadata.document_to_viewport.transform_vector2(center); let viewport_change = if !graph_view_overlay_open {
metadata.document_to_viewport.transform_vector2(center)
} else {
let Some(network) = document_network.nested_network(&node_graph_handler.network) else {
return;
};
network.node_graph_to_viewport.transform_vector2(center)
};
// Only change the pan if the change will be visible in the viewport // Only change the pan if the change will be visible in the viewport
if viewport_change.x.abs() > 0.5 || viewport_change.y.abs() > 0.5 { if viewport_change.x.abs() > 0.5 || viewport_change.y.abs() > 0.5 {
@ -228,7 +284,14 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
} }
NavigationMessage::FitViewportToSelection => { NavigationMessage::FitViewportToSelection => {
if let Some(bounds) = selection_bounds { if let Some(bounds) = selection_bounds {
let transform = metadata.document_to_viewport.inverse(); let transform = if !graph_view_overlay_open {
metadata.document_to_viewport.inverse()
} else {
let Some(network) = document_network.nested_network(&node_graph_handler.network) else {
return;
};
network.node_graph_to_viewport.inverse()
};
responses.add(NavigationMessage::FitViewportToBounds { responses.add(NavigationMessage::FitViewportToBounds {
bounds: [transform.transform_point2(bounds[0]), transform.transform_point2(bounds[1])], bounds: [transform.transform_point2(bounds[0]), transform.transform_point2(bounds[1])],
prevent_zoom_past_100: false, prevent_zoom_past_100: false,
@ -277,6 +340,13 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
let amount = vertical_delta * VIEWPORT_ZOOM_MOUSE_RATE; let amount = vertical_delta * VIEWPORT_ZOOM_MOUSE_RATE;
let updated_zoom = zoom_raw_not_snapped * (1. + amount); let updated_zoom = zoom_raw_not_snapped * (1. + amount);
let document_bounds = if !graph_view_overlay_open {
// TODO: Cache this in node graph coordinates and apply the transform to the rectangle to get viewport coordinates
metadata.document_bounds_viewport_space()
} else {
node_graph_handler.graph_bounds_viewport_space(document_network)
};
updated_zoom * Self::clamp_zoom(updated_zoom, document_bounds, old_zoom, ipp) updated_zoom * Self::clamp_zoom(updated_zoom, document_bounds, old_zoom, ipp)
}; };
ptz.zoom = self.snapped_zoom(zoom_raw_not_snapped); ptz.zoom = self.snapped_zoom(zoom_raw_not_snapped);
@ -376,7 +446,6 @@ impl NavigationMessageHandler {
let delta_size = viewport_bounds - new_viewport_bounds; let delta_size = viewport_bounds - new_viewport_bounds;
let mouse_fraction = mouse / viewport_bounds; let mouse_fraction = mouse / viewport_bounds;
let delta = delta_size * (DVec2::splat(0.5) - mouse_fraction); let delta = delta_size * (DVec2::splat(0.5) - mouse_fraction);
NavigationMessage::CanvasPan { delta }.into() NavigationMessage::CanvasPan { delta }.into()
} }

View File

@ -2901,41 +2901,43 @@ pub fn wrap_network_in_scope(mut network: NodeNetwork, hash: u64) -> NodeNetwork
} }
} }
pub fn new_image_network(output_offset: i32, output_node_id: NodeId) -> NodeNetwork { // Previously used by the Imaginate node, but usage was commented out since it did nothing.
let mut network = NodeNetwork { ..Default::default() }; // pub fn new_image_network(output_offset: i32, output_node_id: NodeId) -> NodeNetwork {
network.push_node( // let mut network = NodeNetwork { ..Default::default() };
resolve_document_node_type("Input Frame") // network.push_node_to_document_network(
.expect("Input Frame node does not exist") // resolve_document_node_type("Input Frame")
.to_document_node_default_inputs([], DocumentNodeMetadata::position((8, 4))), // .expect("Input Frame node does not exist")
); // .to_document_node_default_inputs([], DocumentNodeMetadata::position((8, 4))),
network.push_node( // );
resolve_document_node_type("Output") // network.push_node_to_document_network(
.expect("Output node does not exist") // resolve_document_node_type("Output")
.to_document_node([NodeInput::node(output_node_id, 0)], DocumentNodeMetadata::position((output_offset + 8, 4))), // .expect("Output node does not exist")
); // .to_document_node([NodeInput::node(output_node_id, 0)], DocumentNodeMetadata::position((output_offset + 8, 4))),
network // );
} // network
// }
pub fn new_text_network(text: String, font: Font, size: f64) -> NodeNetwork { // Unused
let text_generator = resolve_document_node_type("Text").expect("Text node does not exist"); // pub fn new_text_network(text: String, font: Font, size: f64) -> NodeNetwork {
let transform = resolve_document_node_type("Transform").expect("Transform node does not exist"); // let text_generator = resolve_document_node_type("Text").expect("Text node does not exist");
let fill = resolve_document_node_type("Fill").expect("Fill node does not exist"); // let transform = resolve_document_node_type("Transform").expect("Transform node does not exist");
let stroke = resolve_document_node_type("Stroke").expect("Stroke node does not exist"); // let fill = resolve_document_node_type("Fill").expect("Fill node does not exist");
let output = resolve_document_node_type("Output").expect("Output node does not exist"); // let stroke = resolve_document_node_type("Stroke").expect("Stroke node does not exist");
// let output = resolve_document_node_type("Output").expect("Output node does not exist");
let mut network = NodeNetwork { ..Default::default() }; // let mut network = NodeNetwork { ..Default::default() };
network.push_node(text_generator.to_document_node( // network.push_node_to_document_network(text_generator.to_document_node(
[ // [
NodeInput::network(concrete!(WasmEditorApi), 0), // NodeInput::network(concrete!(WasmEditorApi), 0),
NodeInput::value(TaggedValue::String(text), false), // NodeInput::value(TaggedValue::String(text), false),
NodeInput::value(TaggedValue::Font(font), false), // NodeInput::value(TaggedValue::Font(font), false),
NodeInput::value(TaggedValue::F64(size), false), // NodeInput::value(TaggedValue::F64(size), false),
], // ],
DocumentNodeMetadata::position((0, 4)), // DocumentNodeMetadata::position((0, 4)),
)); // ));
network.push_node(transform.to_document_node_default_inputs([None], Default::default())); // network.push_node_to_document_network(transform.to_document_node_default_inputs([None], Default::default()));
network.push_node(fill.to_document_node_default_inputs([None], Default::default())); // network.push_node_to_document_network(fill.to_document_node_default_inputs([None], Default::default()));
network.push_node(stroke.to_document_node_default_inputs([None], Default::default())); // network.push_node_to_document_network(stroke.to_document_node_default_inputs([None], Default::default()));
network.push_node(output.to_document_node_default_inputs([None], Default::default())); // network.push_node_to_document_network(output.to_document_node_default_inputs([None], Default::default()));
network // network
} // }

View File

@ -1,3 +1,4 @@
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
@ -18,6 +19,7 @@ pub enum NodeGraphMessage {
input_node_connector_index: usize, input_node_connector_index: usize,
}, },
Copy, Copy,
CloseCreateNodeMenu,
CreateNode { CreateNode {
node_id: Option<NodeId>, node_id: Option<NodeId>,
node_type: String, node_type: String,
@ -36,9 +38,7 @@ pub enum NodeGraphMessage {
node_id: NodeId, node_id: NodeId,
input_index: usize, input_index: usize,
}, },
EnterNestedNetwork { EnterNestedNetwork,
node: NodeId,
},
DuplicateSelectedNodes, DuplicateSelectedNodes,
EnforceLayerHasNoMultiParams { EnforceLayerHasNoMultiParams {
node_id: NodeId, node_id: NodeId,
@ -71,6 +71,16 @@ pub enum NodeGraphMessage {
PasteNodes { PasteNodes {
serialized_nodes: String, serialized_nodes: String,
}, },
PointerDown {
shift_click: bool,
control_click: bool,
alt_click: bool,
right_click: bool,
},
PointerMove {
shift: Key,
},
PointerUp,
PrintSelectedNodeCoordinates, PrintSelectedNodeCoordinates,
RunDocumentGraph, RunDocumentGraph,
SelectedNodesAdd { SelectedNodesAdd {
@ -132,7 +142,6 @@ pub enum NodeGraphMessage {
node_id: NodeId, node_id: NodeId,
}, },
ToggleSelectedAsLayersOrNodes, ToggleSelectedAsLayersOrNodes,
ToggleSelectedLocked,
ToggleSelectedVisibility, ToggleSelectedVisibility,
ToggleVisibility { ToggleVisibility {
node_id: NodeId, node_id: NodeId,

View File

@ -110,3 +110,59 @@ impl FrontendNodeType {
} }
} }
} }
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct DragStart {
pub start_x: f64,
pub start_y: f64,
pub round_x: i32,
pub round_y: i32,
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct Transform {
pub scale: f64,
pub x: f64,
pub y: f64,
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct WirePath {
#[serde(rename = "pathString")]
pub path_string: String,
#[serde(rename = "dataType")]
pub data_type: FrontendGraphDataType,
pub thick: bool,
pub dashed: bool,
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct BoxSelection {
#[serde(rename = "startX")]
pub start_x: u32,
#[serde(rename = "startY")]
pub start_y: u32,
#[serde(rename = "endX")]
pub end_x: u32,
#[serde(rename = "endY")]
pub end_y: u32,
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum ContextMenuData {
ToggleLayer {
#[serde(rename = "nodeId")]
node_id: NodeId,
#[serde(rename = "currentlyIsNode")]
currently_is_node: bool,
},
CreateNode,
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct ContextMenuInformation {
// Stores whether the context menu is open and its position in graph coordinates
#[serde(rename = "contextMenuCoordinates")]
pub context_menu_coordinates: (i32, i32),
#[serde(rename = "contextMenuData")]
pub context_menu_data: ContextMenuData,
}

View File

@ -2,5 +2,6 @@ pub mod clipboards;
pub mod document_metadata; pub mod document_metadata;
pub mod error; pub mod error;
pub mod misc; pub mod misc;
pub mod node_metadata;
pub mod nodes; pub mod nodes;
pub mod transformation; pub mod transformation;

View File

@ -0,0 +1,26 @@
use bezier_rs::Subpath;
use glam::DAffine2;
use graphene_core::renderer::ClickTarget;
use graphene_core::uuid::ManipulatorGroupId;
#[derive(Debug, Clone)]
pub struct NodeMetadata {
/// Cache for all node click targets in node graph space. Ensure update_click_target is called when modifying a node property that changes its size. Currently this is alias, inputs, is_layer, and metadata
pub node_click_target: ClickTarget,
/// Cache for all node inputs. Should be automatically updated when update_click_target is called
pub input_click_targets: Vec<ClickTarget>,
/// Cache for all node outputs. Should be automatically updated when update_click_target is called
pub output_click_targets: Vec<ClickTarget>,
/// Cache for all visibility buttons. Should be automatically updated when update_click_target is called
pub visibility_click_target: Option<ClickTarget>,
/// Stores the width in grid cell units for layer nodes from the left edge of the thumbnail (+12px padding since thumbnail ends between grid spaces) to the end of the node
pub layer_width: Option<u32>,
}
#[derive(Debug, Clone)]
pub struct NetworkMetadata {
/// Cache for the bounding box around all nodes in node graph space.
pub bounding_box_subpath: Option<Subpath<ManipulatorGroupId>>,
/// Transform from node graph space to viewport space.
pub node_graph_to_viewport: DAffine2,
}

View File

@ -6,20 +6,26 @@ use crate::messages::prelude::*;
pub struct MenuBarMessageData { pub struct MenuBarMessageData {
pub has_active_document: bool, pub has_active_document: bool,
pub rulers_visible: bool, pub rulers_visible: bool,
pub node_graph_open: bool,
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct MenuBarMessageHandler { pub struct MenuBarMessageHandler {
has_active_document: bool, has_active_document: bool,
rulers_visible: bool, rulers_visible: bool,
node_graph_open: bool,
} }
impl MessageHandler<MenuBarMessage, MenuBarMessageData> for MenuBarMessageHandler { impl MessageHandler<MenuBarMessage, MenuBarMessageData> for MenuBarMessageHandler {
fn process_message(&mut self, message: MenuBarMessage, responses: &mut VecDeque<Message>, data: MenuBarMessageData) { fn process_message(&mut self, message: MenuBarMessage, responses: &mut VecDeque<Message>, data: MenuBarMessageData) {
let MenuBarMessageData { has_active_document, rulers_visible } = data; let MenuBarMessageData {
has_active_document,
rulers_visible,
node_graph_open,
} = data;
self.has_active_document = has_active_document; self.has_active_document = has_active_document;
self.rulers_visible = rulers_visible; self.rulers_visible = rulers_visible;
self.node_graph_open = node_graph_open;
match message { match message {
MenuBarMessage::SendLayout => self.send_layout(responses, LayoutTarget::MenuBar), MenuBarMessage::SendLayout => self.send_layout(responses, LayoutTarget::MenuBar),
@ -34,6 +40,7 @@ impl MessageHandler<MenuBarMessage, MenuBarMessageData> for MenuBarMessageHandle
impl LayoutHolder for MenuBarMessageHandler { impl LayoutHolder for MenuBarMessageHandler {
fn layout(&self) -> Layout { fn layout(&self) -> Layout {
let no_active_document = !self.has_active_document; let no_active_document = !self.has_active_document;
let node_graph_open = self.node_graph_open;
let menu_bar_entries = vec![ let menu_bar_entries = vec![
MenuBarEntry { MenuBarEntry {
@ -271,14 +278,14 @@ impl LayoutHolder for MenuBarMessageHandler {
label: "Tilt".into(), label: "Tilt".into(),
shortcut: action_keys!(NavigationMessageDiscriminant::BeginCanvasTilt), shortcut: action_keys!(NavigationMessageDiscriminant::BeginCanvasTilt),
action: MenuBarEntry::create_action(|_| NavigationMessage::BeginCanvasTilt { was_dispatched_from_menu: true }.into()), action: MenuBarEntry::create_action(|_| NavigationMessage::BeginCanvasTilt { was_dispatched_from_menu: true }.into()),
disabled: no_active_document, disabled: no_active_document || node_graph_open,
..MenuBarEntry::default() ..MenuBarEntry::default()
}, },
MenuBarEntry { MenuBarEntry {
label: "Reset Tilt".into(), label: "Reset Tilt".into(),
shortcut: action_keys!(NavigationMessageDiscriminant::CanvasTiltSet), shortcut: action_keys!(NavigationMessageDiscriminant::CanvasTiltSet),
action: MenuBarEntry::create_action(|_| NavigationMessage::CanvasTiltSet { angle_radians: 0.into() }.into()), action: MenuBarEntry::create_action(|_| NavigationMessage::CanvasTiltSet { angle_radians: 0.into() }.into()),
disabled: no_active_document, disabled: no_active_document || node_graph_open,
..MenuBarEntry::default() ..MenuBarEntry::default()
}, },
], ],

View File

@ -40,14 +40,22 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
PortfolioMessage::MenuBar(message) => { PortfolioMessage::MenuBar(message) => {
let mut has_active_document = false; let mut has_active_document = false;
let mut rulers_visible = false; let mut rulers_visible = false;
let mut node_graph_open = false;
if let Some(document) = self.active_document_id.and_then(|document_id| self.documents.get_mut(&document_id)) { if let Some(document) = self.active_document_id.and_then(|document_id| self.documents.get_mut(&document_id)) {
has_active_document = true; has_active_document = true;
rulers_visible = document.rulers_visible; rulers_visible = document.rulers_visible;
node_graph_open = document.is_graph_overlay_open();
} }
self.menu_bar_message_handler.process_message(
self.menu_bar_message_handler message,
.process_message(message, responses, MenuBarMessageData { has_active_document, rulers_visible }); responses,
MenuBarMessageData {
has_active_document,
rulers_visible,
node_graph_open,
},
);
} }
PortfolioMessage::Document(message) => { PortfolioMessage::Document(message) => {
if let Some(document_id) = self.active_document_id { if let Some(document_id) = self.active_document_id {
@ -216,7 +224,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
visible: active_document.selected_nodes.layer_visible(layer, active_document.metadata()), visible: active_document.selected_nodes.layer_visible(layer, active_document.metadata()),
locked: active_document.selected_nodes.layer_locked(layer, active_document.metadata()), locked: active_document.selected_nodes.layer_locked(layer, active_document.metadata()),
collapsed: false, collapsed: false,
alias: previous_alias, alias: previous_alias.to_string(),
}); });
} }
}; };
@ -613,10 +621,12 @@ impl PortfolioMessageHandler {
// TODO: Fix how this doesn't preserve tab order upon loading new document from *File > Open* // TODO: Fix how this doesn't preserve tab order upon loading new document from *File > Open*
fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque<Message>) { fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque<Message>) {
let mut new_document = new_document;
self.document_ids.push(document_id); self.document_ids.push(document_id);
new_document.update_layers_panel_options_bar_widgets(responses); new_document.update_layers_panel_options_bar_widgets(responses);
new_document.node_graph_handler.update_all_click_targets(&mut new_document.network, Vec::new());
self.documents.insert(document_id, new_document); self.documents.insert(document_id, new_document);
if self.active_document().is_some() { if self.active_document().is_some() {
@ -624,6 +634,8 @@ impl PortfolioMessageHandler {
responses.add(ToolMessage::DeactivateTools); responses.add(ToolMessage::DeactivateTools);
} }
//TODO: Remove this and find a way to fix the issue where creating a new document when the node graph is open causes the transform in the new document to be incorrect
responses.add(DocumentMessage::GraphViewOverlay { open: false });
responses.add(PortfolioMessage::UpdateOpenDocumentsList); responses.add(PortfolioMessage::UpdateOpenDocumentsList);
responses.add(PortfolioMessage::SelectDocument { document_id }); responses.add(PortfolioMessage::SelectDocument { document_id });
responses.add(PortfolioMessage::LoadDocumentResources { document_id }); responses.add(PortfolioMessage::LoadDocumentResources { document_id });

View File

@ -1,10 +1,8 @@
use super::tool_prelude::*; use super::tool_prelude::*;
use crate::messages::portfolio::document::node_graph::document_node_types::resolve_document_node_type;
use crate::messages::portfolio::document::node_graph::document_node_types::{new_image_network, IMAGINATE_NODE};
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::tool::common_functionality::resize::Resize; use crate::messages::tool::common_functionality::resize::Resize;
use graph_craft::document::{generate_uuid, DocumentNodeMetadata, NodeId, NodeInput}; use graph_craft::document::{generate_uuid, NodeId};
#[derive(Default)] #[derive(Default)]
pub struct ImaginateTool { pub struct ImaginateTool {
@ -106,36 +104,36 @@ impl Fsm for ImaginateToolFsmState {
shape_data.layer = Some(LayerNodeIdentifier::new(NodeId(generate_uuid()), document.network())); shape_data.layer = Some(LayerNodeIdentifier::new(NodeId(generate_uuid()), document.network()));
responses.add(DocumentMessage::DeselectAllLayers); responses.add(DocumentMessage::DeselectAllLayers);
// Utility function to offset the position of each consecutive node // // Utility function to offset the position of each consecutive node
let mut pos = 8; // let mut pos = 8;
let mut next_pos = || { // let mut next_pos = || {
pos += 8; // pos += 8;
DocumentNodeMetadata::position((pos, 4)) // DocumentNodeMetadata::position((pos, 4))
}; // };
// Get the node type for the Transform and Imaginate nodes // // Get the node type for the Transform and Imaginate nodes
let Some(transform_node_type) = resolve_document_node_type("Transform") else { // let Some(transform_node_type) = resolve_document_node_type("Transform") else {
warn!("Transform node should be in registry"); // warn!("Transform node should be in registry");
return ImaginateToolFsmState::Drawing; // return ImaginateToolFsmState::Drawing;
}; // };
let imaginate_node_type = &*IMAGINATE_NODE; // let imaginate_node_type = &*IMAGINATE_NODE;
// Give them a unique ID // // Give them a unique ID
let transform_node_id = NodeId(100); // let transform_node_id = NodeId(100);
let imaginate_node_id = NodeId(101); let imaginate_node_id = NodeId(101);
// Create the network based on the Input -> Output passthrough default network // Create the network based on the Input -> Output passthrough default network
let mut network = new_image_network(16, imaginate_node_id); // let mut network = new_image_network(16, imaginate_node_id);
// Insert the nodes into the default network // // Insert the nodes into the default network
network.nodes.insert( // network.insert_node(
transform_node_id, // transform_node_id,
transform_node_type.to_document_node_default_inputs([Some(NodeInput::node(NodeId(0), 0))], next_pos()), // transform_node_type.to_document_node_default_inputs([Some(NodeInput::node(NodeId(0), 0))], next_pos()),
); // );
network.nodes.insert( // network.insert_node(
imaginate_node_id, // imaginate_node_id,
imaginate_node_type.to_document_node_default_inputs([Some(NodeInput::node(transform_node_id, 0))], next_pos()), // imaginate_node_type.to_document_node_default_inputs([Some(NodeInput::node(transform_node_id, 0))], next_pos()),
); // );
responses.add(NodeGraphMessage::ShiftNode { node_id: imaginate_node_id }); responses.add(NodeGraphMessage::ShiftNode { node_id: imaginate_node_id });
// // Add a layer with a frame to the document // // Add a layer with a frame to the document

View File

@ -6,7 +6,7 @@
import type { NodeGraphState } from "@graphite/state-providers/node-graph"; import type { NodeGraphState } from "@graphite/state-providers/node-graph";
import type { IconName } from "@graphite/utility-functions/icons"; import type { IconName } from "@graphite/utility-functions/icons";
import type { Editor } from "@graphite/wasm-communication/editor"; import type { Editor } from "@graphite/wasm-communication/editor";
import type { FrontendNodeWire, FrontendNodeType, FrontendNode, FrontendGraphInput, FrontendGraphOutput, FrontendGraphDataType } from "@graphite/wasm-communication/messages"; import type { FrontendNodeWire, FrontendNodeType, FrontendNode, FrontendGraphInput, FrontendGraphOutput, FrontendGraphDataType, WirePath } from "@graphite/wasm-communication/messages";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
@ -17,7 +17,6 @@
import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte"; import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte"; import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte"; import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
const WHEEL_RATE = (1 / 600) * 3;
const GRID_COLLAPSE_SPACING = 10; const GRID_COLLAPSE_SPACING = 10;
const GRID_SIZE = 24; const GRID_SIZE = 24;
const ADD_NODE_MENU_WIDTH = 180; const ADD_NODE_MENU_WIDTH = 180;
@ -26,32 +25,16 @@
const editor = getContext<Editor>("editor"); const editor = getContext<Editor>("editor");
const nodeGraph = getContext<NodeGraphState>("nodeGraph"); const nodeGraph = getContext<NodeGraphState>("nodeGraph");
type WirePath = { pathString: string; dataType: FrontendGraphDataType; thick: boolean; dashed: boolean };
let graph: HTMLDivElement | undefined; let graph: HTMLDivElement | undefined;
let nodesContainer: HTMLDivElement | undefined; let nodesContainer: HTMLDivElement | undefined;
let nodeSearchInput: TextInput | undefined; let nodeSearchInput: TextInput | undefined;
// TODO: MEMORY LEAK: Items never get removed from this array, so find a way to deal with garbage collection
let layerNameLabelWidths: Record<string, number> = {};
let transform = { scale: 1, x: 1200, y: 0 };
let panning = false;
let draggingNodes: { startX: number; startY: number; roundX: number; roundY: number } | undefined = undefined;
type Box = { startX: number; startY: number; endX: number; endY: number };
let boxSelection: Box | undefined = undefined;
let previousSelection: bigint[] = [];
let selectIfNotDragged: undefined | bigint = undefined;
let wireInProgressFromConnector: SVGSVGElement | undefined = undefined;
let wireInProgressToConnector: SVGSVGElement | DOMRect | undefined = undefined;
// TODO: Using this not-complete code, or another better approach, make it so the dragged in-progress connector correctly handles showing/hiding the SVG shape of the connector caps // TODO: Using this not-complete code, or another better approach, make it so the dragged in-progress connector correctly handles showing/hiding the SVG shape of the connector caps
// let wireInProgressFromLayerTop: bigint | undefined = undefined; // let wireInProgressFromLayerTop: bigint | undefined = undefined;
// let wireInProgressFromLayerBottom: bigint | undefined = undefined; // let wireInProgressFromLayerBottom: bigint | undefined = undefined;
let disconnecting: { nodeId: bigint; inputIndex: number; wireIndex: number } | undefined = undefined;
let nodeWirePaths: WirePath[] = []; let nodeWirePaths: WirePath[] = [];
let searchTerm = ""; let searchTerm = "";
let contextMenuOpenCoordinates: { x: number; y: number } | undefined = undefined;
let toggleDisplayAsLayerNodeId: bigint | undefined = undefined;
let toggleDisplayAsLayerCurrentlyIsNode: boolean = false;
let inputs: SVGSVGElement[][] = []; let inputs: SVGSVGElement[][] = [];
let outputs: SVGSVGElement[][] = []; let outputs: SVGSVGElement[][] = [];
@ -59,26 +42,17 @@
$: watchNodes($nodeGraph.nodes); $: watchNodes($nodeGraph.nodes);
$: gridSpacing = calculateGridSpacing(transform.scale); $: gridSpacing = calculateGridSpacing($nodeGraph.transform.scale);
$: dotRadius = 1 + Math.floor(transform.scale - 0.5 + 0.001) / 2; $: dotRadius = 1 + Math.floor($nodeGraph.transform.scale - 0.5 + 0.001) / 2;
$: nodeCategories = buildNodeCategories($nodeGraph.nodeTypes, searchTerm); $: nodeCategories = buildNodeCategories($nodeGraph.nodeTypes, searchTerm);
$: contextMenuX = ((contextMenuOpenCoordinates?.x || 0) + transform.x) * transform.scale;
$: contextMenuY = ((contextMenuOpenCoordinates?.y || 0) + transform.y) * transform.scale;
let appearAboveMouse = false;
let appearRightOfMouse = false;
$: (() => { $: (() => {
const bounds = graph?.getBoundingClientRect(); if ($nodeGraph.contextMenuInformation?.contextMenuData == "CreateNode") {
if (!bounds) return; setTimeout(() => nodeSearchInput?.focus(), 0);
const { width, height } = bounds; }
appearRightOfMouse = contextMenuX > width - ADD_NODE_MENU_WIDTH;
appearAboveMouse = contextMenuY > height - ADD_NODE_MENU_HEIGHT;
})(); })();
$: wirePathInProgress = createWirePathInProgress(wireInProgressFromConnector, wireInProgressToConnector); $: wirePaths = createWirePaths($nodeGraph.wirePathInProgress, nodeWirePaths);
$: wirePaths = createWirePaths(wirePathInProgress, nodeWirePaths);
function calculateGridSpacing(scale: number): number { function calculateGridSpacing(scale: number): number {
const dense = scale * GRID_SIZE; const dense = scale * GRID_SIZE;
@ -129,18 +103,6 @@
return Array.from(categories); return Array.from(categories);
} }
function createWirePathInProgress(wireInProgressFromConnector?: SVGSVGElement, wireInProgressToConnector?: SVGSVGElement | DOMRect): WirePath | undefined {
if (wireInProgressFromConnector && wireInProgressToConnector && nodesContainer) {
const from = connectorToNodeIndex(wireInProgressFromConnector);
const to = wireInProgressToConnector instanceof SVGSVGElement ? connectorToNodeIndex(wireInProgressToConnector) : undefined;
const wireStart = $nodeGraph.nodes.find((n) => n.id === from?.nodeId)?.isLayer || false;
const wireEnd = ($nodeGraph.nodes.find((n) => n.id === to?.nodeId)?.isLayer && to?.index == 0) || false;
return createWirePath(wireInProgressFromConnector, wireInProgressToConnector, wireStart, wireEnd, false);
}
return undefined;
}
function createWirePaths(wirePathInProgress: WirePath | undefined, nodeWirePaths: WirePath[]): WirePath[] { function createWirePaths(wirePathInProgress: WirePath | undefined, nodeWirePaths: WirePath[]): WirePath[] {
const maybeWirePathInProgress = wirePathInProgress ? [wirePathInProgress] : []; const maybeWirePathInProgress = wirePathInProgress ? [wirePathInProgress] : [];
return [...maybeWirePathInProgress, ...nodeWirePaths]; return [...maybeWirePathInProgress, ...nodeWirePaths];
@ -171,10 +133,9 @@
await tick(); await tick();
const wires = $nodeGraph.wires; const wires = $nodeGraph.wires;
nodeWirePaths = wires.flatMap((wire, index) => { nodeWirePaths = wires.flatMap((wire) => {
const { nodeInput, nodeOutput } = resolveWire(wire); const { nodeInput, nodeOutput } = resolveWire(wire);
if (!nodeInput || !nodeOutput) return []; if (!nodeInput || !nodeOutput) return [];
if (disconnecting?.wireIndex === index) return [];
const wireStart = $nodeGraph.nodes.find((n) => n.id === wire.wireStart)?.isLayer || false; const wireStart = $nodeGraph.nodes.find((n) => n.id === wire.wireStart)?.isLayer || false;
const wireEnd = ($nodeGraph.nodes.find((n) => n.id === wire.wireEnd)?.isLayer && Number(wire.wireEndInputIndex) == 0) || false; const wireEnd = ($nodeGraph.nodes.find((n) => n.id === wire.wireEnd)?.isLayer && Number(wire.wireEndInputIndex) == 0) || false;
@ -201,13 +162,13 @@
const outX = verticalOut ? outputBounds.x + outputBounds.width / 2 : outputBounds.x + outputBounds.width - 1; const outX = verticalOut ? outputBounds.x + outputBounds.width / 2 : outputBounds.x + outputBounds.width - 1;
const outY = verticalOut ? outputBounds.y + VERTICAL_WIRE_OVERLAP_ON_SHAPED_CAP : outputBounds.y + outputBounds.height / 2; const outY = verticalOut ? outputBounds.y + VERTICAL_WIRE_OVERLAP_ON_SHAPED_CAP : outputBounds.y + outputBounds.height / 2;
const outConnectorX = (outX - containerBounds.x) / transform.scale; const outConnectorX = (outX - containerBounds.x) / $nodeGraph.transform.scale;
const outConnectorY = (outY - containerBounds.y) / transform.scale; const outConnectorY = (outY - containerBounds.y) / $nodeGraph.transform.scale;
const inX = verticalIn ? inputBounds.x + inputBounds.width / 2 : inputBounds.x + 1; const inX = verticalIn ? inputBounds.x + inputBounds.width / 2 : inputBounds.x + 1;
const inY = verticalIn ? inputBounds.y + inputBounds.height - VERTICAL_WIRE_OVERLAP_ON_SHAPED_CAP : inputBounds.y + inputBounds.height / 2; const inY = verticalIn ? inputBounds.y + inputBounds.height - VERTICAL_WIRE_OVERLAP_ON_SHAPED_CAP : inputBounds.y + inputBounds.height / 2;
const inConnectorX = (inX - containerBounds.x) / transform.scale; const inConnectorX = (inX - containerBounds.x) / $nodeGraph.transform.scale;
const inConnectorY = (inY - containerBounds.y) / transform.scale; const inConnectorY = (inY - containerBounds.y) / $nodeGraph.transform.scale;
const horizontalGap = Math.abs(outConnectorX - inConnectorX); const horizontalGap = Math.abs(outConnectorX - inConnectorX);
const verticalGap = Math.abs(outConnectorY - inConnectorY); const verticalGap = Math.abs(outConnectorY - inConnectorY);
@ -269,307 +230,10 @@
return { pathString, dataType, thick: verticalIn && verticalOut, dashed }; return { pathString, dataType, thick: verticalIn && verticalOut, dashed };
} }
function scroll(e: WheelEvent) { function toggleLayerDisplay(displayAsLayer: boolean, toggleId: bigint) {
const [scrollX, scrollY] = [e.deltaX, e.deltaY]; let node = $nodeGraph.nodes.find((node) => node.id === toggleId);
// If zoom with scroll is enabled: horizontal pan with Ctrl, vertical pan with Shift
const zoomWithScroll = $nodeGraph.zoomWithScroll;
const zoom = zoomWithScroll ? !e.ctrlKey && !e.shiftKey : e.ctrlKey;
const horizontalPan = zoomWithScroll ? e.ctrlKey : !e.ctrlKey && e.shiftKey;
// Prevent the web page from being zoomed
if (e.ctrlKey) e.preventDefault();
// Always pan horizontally in response to a horizontal scroll wheel movement
transform.x -= scrollX / transform.scale;
// Zoom
if (zoom) {
let zoomFactor = 1 + Math.abs(scrollY) * WHEEL_RATE;
if (scrollY > 0) zoomFactor = 1 / zoomFactor;
const bounds = graph?.getBoundingClientRect();
if (!bounds) return;
const { x, y, width, height } = bounds;
transform.scale *= zoomFactor;
const newViewportX = width / zoomFactor;
const newViewportY = height / zoomFactor;
const deltaSizeX = width - newViewportX;
const deltaSizeY = height - newViewportY;
const deltaX = deltaSizeX * ((e.x - x) / width);
const deltaY = deltaSizeY * ((e.y - y) / height);
transform.x -= (deltaX / transform.scale) * zoomFactor;
transform.y -= (deltaY / transform.scale) * zoomFactor;
return;
}
// Pan
if (horizontalPan) {
transform.x -= scrollY / transform.scale;
} else {
transform.y -= scrollY / transform.scale;
}
}
function keydown(e: KeyboardEvent) {
if (e.key.toLowerCase() === "escape") {
contextMenuOpenCoordinates = undefined;
document.removeEventListener("keydown", keydown);
wireInProgressFromConnector = undefined;
// wireInProgressFromLayerTop = undefined;
// wireInProgressFromLayerBottom = undefined;
}
}
function loadNodeList(e: PointerEvent, graphBounds: DOMRect) {
contextMenuOpenCoordinates = {
x: (e.clientX - graphBounds.x) / transform.scale - transform.x,
y: (e.clientY - graphBounds.y) / transform.scale - transform.y,
};
// Find actual relevant child and focus it (setTimeout is required to actually focus the input element)
setTimeout(() => nodeSearchInput?.focus(), 0);
document.addEventListener("keydown", keydown);
}
// TODO: Move the event listener from the graph to the window so dragging outside the graph area (or even the whole browser window) works
function pointerDown(e: PointerEvent) {
const [lmb, rmb] = [e.button === 0, e.button === 2];
const nodeError = (e.target as SVGSVGElement).closest("[data-node-error]") as HTMLElement;
if (nodeError && lmb) return;
const port = (e.target as SVGSVGElement).closest("[data-port]") as SVGSVGElement;
const node = (e.target as HTMLElement).closest("[data-node]") as HTMLElement | undefined;
const nodeIdString = node?.getAttribute("data-node") || undefined;
const nodeId = nodeIdString ? BigInt(nodeIdString) : undefined;
const contextMenu = (e.target as HTMLElement).closest("[data-context-menu]") as HTMLElement | undefined;
// Create the add node popup on right click, then exit
if (rmb) {
toggleDisplayAsLayerNodeId = undefined;
if (node) {
toggleDisplayAsLayerNodeId = nodeId;
toggleDisplayAsLayerCurrentlyIsNode = !($nodeGraph.nodes.find((node) => node.id === nodeId)?.isLayer || false);
}
const graphBounds = graph?.getBoundingClientRect();
if (!graphBounds) return;
loadNodeList(e, graphBounds);
return;
}
// If the user is clicking on the add nodes list or context menu, exit here
if (lmb && contextMenu) return;
// Since the user is clicking elsewhere in the graph, ensure the add nodes list is closed
if (lmb) {
contextMenuOpenCoordinates = undefined;
wireInProgressFromConnector = undefined;
toggleDisplayAsLayerNodeId = undefined;
// wireInProgressFromLayerTop = undefined;
// wireInProgressFromLayerBottom = undefined;
}
// Alt-click sets the clicked node as previewed
if (lmb && e.altKey && nodeId !== undefined) {
editor.handle.togglePreview(nodeId);
}
// Clicked on a port dot
if (lmb && port && node) {
const isOutput = Boolean(port.getAttribute("data-port") === "output");
const frontendNode = (nodeId !== undefined && $nodeGraph.nodes.find((n) => n.id === nodeId)) || undefined;
// Output: Begin dragging out a new wire
if (isOutput) {
// Disallow creating additional vertical output wires from an already-connected layer
if (frontendNode?.isLayer && frontendNode.primaryOutput && frontendNode.primaryOutput.connected.length > 0) return;
wireInProgressFromConnector = port;
// // Since we are just beginning to drag out a wire from the top, we know the in-progress wire exists from this layer's top and has no connection to any other layer bottom yet
// wireInProgressFromLayerTop = nodeId !== undefined && frontendNode?.isLayer ? nodeId : undefined;
// wireInProgressFromLayerBottom = undefined;
}
// Input: Begin moving an existing wire
else {
const inputNodeInPorts = Array.from(node.querySelectorAll(`[data-port="input"]`));
const inputNodeConnectionIndexSearch = inputNodeInPorts.indexOf(port);
const inputIndex = inputNodeConnectionIndexSearch > -1 ? inputNodeConnectionIndexSearch : undefined;
if (inputIndex === undefined || nodeId === undefined) return;
// Set the wire to draw from the input that a previous wire was on
const wireIndex = $nodeGraph.wires.filter((wire) => !wire.dashed).findIndex((value) => value.wireEnd === nodeId && value.wireEndInputIndex === BigInt(inputIndex));
if (wireIndex === -1) return;
const nodeOutputConnectors = nodesContainer?.querySelectorAll(`[data-node="${String($nodeGraph.wires[wireIndex].wireStart)}"] [data-port="output"]`) || undefined;
wireInProgressFromConnector = nodeOutputConnectors?.[Number($nodeGraph.wires[wireIndex].wireStartOutputIndex)] as SVGSVGElement | undefined;
const nodeInputConnectors = nodesContainer?.querySelectorAll(`[data-node="${String($nodeGraph.wires[wireIndex].wireEnd)}"] [data-port="input"]`) || undefined;
wireInProgressToConnector = nodeInputConnectors?.[Number($nodeGraph.wires[wireIndex].wireEndInputIndex)] as SVGSVGElement | undefined;
disconnecting = { nodeId: nodeId, inputIndex, wireIndex };
refreshWires();
}
return;
}
// Clicked on a node, so we select it
if (lmb && nodeId !== undefined) {
let updatedSelected = [...$nodeGraph.selected];
let modifiedSelected = false;
// Add to/remove from selection if holding Shift or Ctrl
if (e.shiftKey || e.ctrlKey) {
modifiedSelected = true;
// Remove from selection if already selected
if (!updatedSelected.includes(nodeId)) updatedSelected.push(nodeId);
// Add to selection if not already selected
else updatedSelected.splice(updatedSelected.lastIndexOf(nodeId), 1);
}
// Replace selection with a non-selected node
else if (!updatedSelected.includes(nodeId)) {
modifiedSelected = true;
updatedSelected = [nodeId];
}
// Replace selection (of multiple nodes including this one) with just this one, but only upon pointer up if the user didn't drag the selected nodes
else {
selectIfNotDragged = nodeId;
}
// If this node is selected (whether from before or just now), prepare it for dragging
if (updatedSelected.includes(nodeId)) {
draggingNodes = { startX: e.x, startY: e.y, roundX: 0, roundY: 0 };
}
// Update the selection in the backend if it was modified
if (modifiedSelected) editor.handle.selectNodes(new BigUint64Array(updatedSelected));
return;
}
// Clicked on the graph background so we box select
if (lmb) {
previousSelection = $nodeGraph.selected;
// Clear current selection
if (!e.shiftKey) editor.handle.selectNodes(new BigUint64Array(0));
const graphBounds = graph?.getBoundingClientRect();
boxSelection = { startX: e.x - (graphBounds?.x || 0), startY: e.y - (graphBounds?.y || 0), endX: e.x - (graphBounds?.x || 0), endY: e.y - (graphBounds?.y || 0) };
return;
}
// LMB clicked on the graph background or MMB clicked anywhere
panning = true;
}
function doubleClick(e: MouseEvent) {
if ((e.target as HTMLElement).closest("[data-visibility-button]")) return;
const node = (e.target as HTMLElement).closest("[data-node]") as HTMLElement | undefined;
const nodeId = node?.getAttribute("data-node") || undefined;
if (nodeId !== undefined && !e.altKey) {
const id = BigInt(nodeId);
editor.handle.enterNestedNetwork(id);
}
}
function pointerMove(e: PointerEvent) {
if (panning) {
transform.x += e.movementX / transform.scale;
transform.y += e.movementY / transform.scale;
} else if (wireInProgressFromConnector && !contextMenuOpenCoordinates) {
const target = e.target as Element | undefined;
const dot = (target?.closest(`[data-port="input"]`) || undefined) as SVGSVGElement | undefined;
if (dot) {
wireInProgressToConnector = dot;
} else {
wireInProgressToConnector = new DOMRect(e.x, e.y);
}
} else if (draggingNodes) {
const deltaX = Math.round((e.x - draggingNodes.startX) / transform.scale / GRID_SIZE);
const deltaY = Math.round((e.y - draggingNodes.startY) / transform.scale / GRID_SIZE);
if (draggingNodes.roundX !== deltaX || draggingNodes.roundY !== deltaY) {
draggingNodes.roundX = deltaX;
draggingNodes.roundY = deltaY;
let stop = false;
const refresh = () => {
if (!stop) refreshWires();
requestAnimationFrame(refresh);
};
refresh();
// const DRAG_SMOOTHING_TIME = 0.1;
const DRAG_SMOOTHING_TIME = 0; // TODO: Reenable this after fixing the bugs with the wires, see the CSS `transition` attribute todo for other info
setTimeout(
() => {
stop = true;
},
DRAG_SMOOTHING_TIME * 1000 + 10,
);
}
} else if (boxSelection) {
// The mouse button was released but we missed the pointer up event
if ((e.buttons & 1) === 0) {
completeBoxSelection();
boxSelection = undefined;
} else if ((e.buttons & 2) !== 0) {
editor.handle.selectNodes(new BigUint64Array(previousSelection));
boxSelection = undefined;
} else {
const graphBounds = graph?.getBoundingClientRect();
boxSelection.endX = e.x - (graphBounds?.x || 0);
boxSelection.endY = e.y - (graphBounds?.y || 0);
}
}
}
function intersetNodeAABB(boxSelection: Box | undefined, nodeIndex: number): boolean {
const bounds = nodeElements[nodeIndex]?.getBoundingClientRect();
const graphBounds = graph?.getBoundingClientRect();
return (
boxSelection !== undefined &&
bounds &&
Math.min(boxSelection.startX, boxSelection.endX) < bounds.right - (graphBounds?.x || 0) &&
Math.max(boxSelection.startX, boxSelection.endX) > bounds.left - (graphBounds?.x || 0) &&
Math.min(boxSelection.startY, boxSelection.endY) < bounds.bottom - (graphBounds?.y || 0) &&
Math.max(boxSelection.startY, boxSelection.endY) > bounds.top - (graphBounds?.y || 0)
);
}
function completeBoxSelection() {
editor.handle.selectNodes(new BigUint64Array($nodeGraph.selected.concat($nodeGraph.nodes.filter((_, nodeIndex) => intersetNodeAABB(boxSelection, nodeIndex)).map((node) => node.id))));
}
function showSelected(selected: bigint[], boxSelect: Box | undefined, node: bigint, nodeIndex: number): boolean {
return selected.includes(node) || intersetNodeAABB(boxSelect, nodeIndex);
}
function toggleNodeVisibilityGraph(id: bigint) {
editor.handle.toggleNodeVisibilityGraph(id);
}
function toggleLayerDisplay(displayAsLayer: boolean) {
let node = $nodeGraph.nodes.find((node) => node.id === toggleDisplayAsLayerNodeId);
if (node !== undefined) { if (node !== undefined) {
contextMenuOpenCoordinates = undefined;
editor.handle.setToNodeOrLayer(node.id, displayAsLayer); editor.handle.setToNodeOrLayer(node.id, displayAsLayer);
toggleDisplayAsLayerCurrentlyIsNode = !($nodeGraph.nodes.find((node) => node.id === toggleDisplayAsLayerNodeId)?.isLayer || false);
toggleDisplayAsLayerNodeId = undefined;
} }
} }
@ -577,144 +241,10 @@
return $nodeGraph.nodes.find((node) => node.id === toggleDisplayAsLayerNodeId)?.canBeLayer || false; return $nodeGraph.nodes.find((node) => node.id === toggleDisplayAsLayerNodeId)?.canBeLayer || false;
} }
function connectorToNodeIndex(svg: SVGSVGElement): { nodeId: bigint; index: number } | undefined {
const node = svg.closest("[data-node]");
if (!node) return undefined;
const nodeIdAttribute = node.getAttribute("data-node");
if (!nodeIdAttribute) return undefined;
const nodeId = BigInt(nodeIdAttribute);
const inputPortElements = Array.from(node.querySelectorAll(`[data-port="input"]`));
const outputPortElements = Array.from(node.querySelectorAll(`[data-port="output"]`));
const inputNodeConnectionIndexSearch = inputPortElements.includes(svg) ? inputPortElements.indexOf(svg) : outputPortElements.indexOf(svg);
const index = inputNodeConnectionIndexSearch > -1 ? inputNodeConnectionIndexSearch : undefined;
if (nodeId !== undefined && index !== undefined) return { nodeId, index };
else return undefined;
}
// Check if this node should be inserted between two other nodes
function checkInsertBetween() {
if ($nodeGraph.selected.length !== 1) return;
const selectedNodeId = $nodeGraph.selected[0];
const selectedNode = nodesContainer?.querySelector(`[data-node="${String(selectedNodeId)}"]`) || undefined;
// Check that neither the primary input or output of the selected node are already connected.
const notConnected = $nodeGraph.wires.findIndex((wire) => wire.wireStart === selectedNodeId || (wire.wireEnd === selectedNodeId && wire.wireEndInputIndex === BigInt(0))) === -1;
const input = selectedNode?.querySelector(`[data-port="input"]`) || undefined;
const output = selectedNode?.querySelector(`[data-port="output"]`) || undefined;
// TODO: Make sure inputs are correctly typed
if (!selectedNode || !notConnected || !input || !output || !nodesContainer) return;
// Fixes typing for some reason?
const theNodesContainer = nodesContainer;
// Find the wire that the node has been dragged on top of
const wire = $nodeGraph.wires.find((wire) => {
const { nodeInput, nodeOutput } = resolveWire(wire);
if (!nodeInput || !nodeOutput) return false;
const wireCurveLocations = buildWirePathLocations(nodeOutput.getBoundingClientRect(), nodeInput.getBoundingClientRect(), false, false);
const selectedNodeBounds = selectedNode.getBoundingClientRect();
const containerBoundsBounds = theNodesContainer.getBoundingClientRect();
return (
wire.wireEnd != selectedNodeId &&
editor.handle.rectangleIntersects(
new Float64Array(wireCurveLocations.map((loc) => loc.x)),
new Float64Array(wireCurveLocations.map((loc) => loc.y)),
selectedNodeBounds.top - containerBoundsBounds.y,
selectedNodeBounds.left - containerBoundsBounds.x,
selectedNodeBounds.bottom - containerBoundsBounds.y,
selectedNodeBounds.right - containerBoundsBounds.x,
)
);
});
// If the node has been dragged on top of the wire then connect it into the middle.
if (wire) {
const isLayer = $nodeGraph.nodes.find((n) => n.id === selectedNodeId)?.isLayer;
editor.handle.insertNodeBetween(wire.wireEnd, Number(wire.wireEndInputIndex), 0, selectedNodeId, 0, Number(wire.wireStartOutputIndex), wire.wireStart);
if (!isLayer) editor.handle.shiftNode(selectedNodeId);
}
}
function pointerUp(e: PointerEvent) {
panning = false;
const initialDisconnecting = disconnecting;
if (disconnecting) {
editor.handle.disconnectNodes(BigInt(disconnecting.nodeId), disconnecting.inputIndex);
}
disconnecting = undefined;
if (wireInProgressToConnector instanceof SVGSVGElement && wireInProgressFromConnector) {
const from = connectorToNodeIndex(wireInProgressFromConnector);
const to = connectorToNodeIndex(wireInProgressToConnector);
if (from !== undefined && to !== undefined) {
const { nodeId: outputConnectedNodeID, index: outputNodeConnectionIndex } = from;
const { nodeId: inputConnectedNodeID, index: inputNodeConnectionIndex } = to;
editor.handle.connectNodesByWire(outputConnectedNodeID, outputNodeConnectionIndex, inputConnectedNodeID, inputNodeConnectionIndex);
}
} else if (wireInProgressFromConnector && !initialDisconnecting) {
// If the add node menu is already open, we don't want to open it again
if (contextMenuOpenCoordinates) return;
const graphBounds = graph?.getBoundingClientRect();
if (!graphBounds) return;
// Create the node list, which should set nodeListLocation to a valid value
loadNodeList(e, graphBounds);
if (!contextMenuOpenCoordinates) return;
let contextMenuLocation2: { x: number; y: number } = contextMenuOpenCoordinates;
wireInProgressToConnector = new DOMRect((contextMenuLocation2.x + transform.x) * transform.scale + graphBounds.x, (contextMenuLocation2.y + transform.y) * transform.scale + graphBounds.y);
return;
} else if (draggingNodes) {
if (draggingNodes.startX === e.x && draggingNodes.startY === e.y) {
if (selectIfNotDragged !== undefined && ($nodeGraph.selected.length !== 1 || $nodeGraph.selected[0] !== selectIfNotDragged)) {
editor.handle.selectNodes(new BigUint64Array([selectIfNotDragged]));
}
}
if ($nodeGraph.selected.length > 0 && (draggingNodes.roundX !== 0 || draggingNodes.roundY !== 0)) editor.handle.moveSelectedNodes(draggingNodes.roundX, draggingNodes.roundY);
checkInsertBetween();
draggingNodes = undefined;
selectIfNotDragged = undefined;
} else if (boxSelection) {
completeBoxSelection();
boxSelection = undefined;
}
wireInProgressFromConnector = undefined;
wireInProgressToConnector = undefined;
}
function createNode(nodeType: string) { function createNode(nodeType: string) {
if (!contextMenuOpenCoordinates) return; if ($nodeGraph.contextMenuInformation === undefined) return;
const inputNodeConnectionIndex = 0; editor.handle.createNode(nodeType, $nodeGraph.contextMenuInformation.contextMenuCoordinates.x, $nodeGraph.contextMenuInformation.contextMenuCoordinates.y);
const x = Math.round(contextMenuOpenCoordinates.x / GRID_SIZE);
const y = Math.round(contextMenuOpenCoordinates.y / GRID_SIZE) - 1;
const inputConnectedNodeID = editor.handle.createNode(nodeType, x, y);
contextMenuOpenCoordinates = undefined;
if (!wireInProgressFromConnector) return;
const from = connectorToNodeIndex(wireInProgressFromConnector);
if (from !== undefined) {
const { nodeId: outputConnectedNodeID, index: outputNodeConnectionIndex } = from;
editor.handle.connectNodesByWire(outputConnectedNodeID, outputNodeConnectionIndex, inputConnectedNodeID, inputNodeConnectionIndex);
}
wireInProgressFromConnector = undefined;
} }
function nodeBorderMask(nodeWidth: number, primaryInputExists: boolean, parameters: number, primaryOutputExists: boolean, exposedOutputs: number): string { function nodeBorderMask(nodeWidth: number, primaryInputExists: boolean, parameters: number, primaryOutputExists: boolean, exposedOutputs: number): string {
@ -779,35 +309,31 @@
<div <div
class="graph" class="graph"
bind:this={graph} bind:this={graph}
on:wheel|nonpassive={scroll}
on:pointerdown={pointerDown}
on:pointermove={pointerMove}
on:pointerup={pointerUp}
on:dblclick={doubleClick}
style:--grid-spacing={`${gridSpacing}px`} style:--grid-spacing={`${gridSpacing}px`}
style:--grid-offset-x={`${transform.x * transform.scale}px`} style:--grid-offset-x={`${$nodeGraph.transform.x}px`}
style:--grid-offset-y={`${transform.y * transform.scale}px`} style:--grid-offset-y={`${$nodeGraph.transform.y}px`}
style:--dot-radius={`${dotRadius}px`} style:--dot-radius={`${dotRadius}px`}
data-node-graph
> >
<BreadcrumbTrailButtons labels={["Document"].concat($nodeGraph.subgraphPath)} action={(index) => editor.handle.exitNestedNetwork($nodeGraph.subgraphPath?.length - index)} /> <BreadcrumbTrailButtons labels={["Document"].concat($nodeGraph.subgraphPath)} action={(index) => editor.handle.exitNestedNetwork($nodeGraph.subgraphPath?.length - index)} />
<!-- Right click menu for adding nodes --> <!-- Right click menu for adding nodes -->
{#if contextMenuOpenCoordinates} {#if $nodeGraph.contextMenuInformation}
<LayoutCol <LayoutCol
class="context-menu" class="context-menu"
data-context-menu data-context-menu
styles={{ styles={{
left: `${contextMenuX}px`, left: `${$nodeGraph.contextMenuInformation.contextMenuCoordinates.x * $nodeGraph.transform.scale + $nodeGraph.transform.x}px`,
top: `${contextMenuY}px`, top: `${$nodeGraph.contextMenuInformation.contextMenuCoordinates.y * $nodeGraph.transform.scale + $nodeGraph.transform.y}px`,
...(toggleDisplayAsLayerNodeId === undefined ...($nodeGraph.contextMenuInformation.contextMenuData === "CreateNode"
? { ? {
transform: `translate(${appearRightOfMouse ? -100 : 0}%, ${appearAboveMouse ? -100 : 0}%)`, transform: `translate(0%, 0%)`,
width: `${ADD_NODE_MENU_WIDTH}px`, width: `${ADD_NODE_MENU_WIDTH}px`,
height: `${ADD_NODE_MENU_HEIGHT}px`, height: `${ADD_NODE_MENU_HEIGHT}px`,
} }
: {}), : {}),
}} }}
> >
{#if toggleDisplayAsLayerNodeId === undefined} {#if $nodeGraph.contextMenuInformation.contextMenuData === "CreateNode"}
<TextInput placeholder="Search Nodes..." value={searchTerm} on:value={({ detail }) => (searchTerm = detail)} bind:this={nodeSearchInput} /> <TextInput placeholder="Search Nodes..." value={searchTerm} on:value={({ detail }) => (searchTerm = detail)} bind:this={nodeSearchInput} />
<div class="list-results" on:wheel|passive|stopPropagation> <div class="list-results" on:wheel|passive|stopPropagation>
{#each nodeCategories as nodeCategory} {#each nodeCategories as nodeCategory}
@ -824,34 +350,35 @@
{/each} {/each}
</div> </div>
{:else} {:else}
{@const contextMenuData = $nodeGraph.contextMenuInformation.contextMenuData}
<LayoutRow class="toggle-layer-or-node"> <LayoutRow class="toggle-layer-or-node">
<TextLabel>Display as</TextLabel> <TextLabel>Display as</TextLabel>
<RadioInput <RadioInput
selectedIndex={toggleDisplayAsLayerCurrentlyIsNode ? 0 : 1} selectedIndex={contextMenuData.currentlyIsNode ? 0 : 1}
entries={[ entries={[
{ {
value: "node", value: "node",
label: "Node", label: "Node",
action: () => { action: () => {
toggleLayerDisplay(false); toggleLayerDisplay(false, contextMenuData.nodeId);
}, },
}, },
{ {
value: "layer", value: "layer",
label: "Layer", label: "Layer",
action: () => { action: () => {
toggleLayerDisplay(true); toggleLayerDisplay(true, contextMenuData.nodeId);
}, },
}, },
]} ]}
disabled={!canBeToggledBetweenNodeAndLayer(toggleDisplayAsLayerNodeId)} disabled={!canBeToggledBetweenNodeAndLayer(contextMenuData.nodeId)}
/> />
</LayoutRow> </LayoutRow>
{/if} {/if}
</LayoutCol> </LayoutCol>
{/if} {/if}
<!-- Node connection wires --> <!-- Node connection wires -->
<div class="wires" style:transform={`scale(${transform.scale}) translate(${transform.x}px, ${transform.y}px)`} style:transform-origin={`0 0`}> <div class="wires" style:transform-origin={`0 0`} style:transform={`translate(${$nodeGraph.transform.x}px, ${$nodeGraph.transform.y}px) scale(${$nodeGraph.transform.scale})`}>
<svg> <svg>
{#each wirePaths as { pathString, dataType, thick, dashed }} {#each wirePaths as { pathString, dataType, thick, dashed }}
<path <path
@ -865,24 +392,28 @@
</svg> </svg>
</div> </div>
<!-- Layers and nodes --> <!-- Layers and nodes -->
<div class="layers-and-nodes" style:transform={`scale(${transform.scale}) translate(${transform.x}px, ${transform.y}px)`} style:transform-origin={`0 0`} bind:this={nodesContainer}> <div
class="layers-and-nodes"
style:transform-origin={`0 0`}
style:transform={`translate(${$nodeGraph.transform.x}px, ${$nodeGraph.transform.y}px) scale(${$nodeGraph.transform.scale})`}
bind:this={nodesContainer}
>
<!-- Layers --> <!-- Layers -->
{#each $nodeGraph.nodes.flatMap((node, nodeIndex) => (node.isLayer ? [{ node, nodeIndex }] : [])) as { node, nodeIndex } (nodeIndex)} {#each $nodeGraph.nodes.flatMap((node, nodeIndex) => (node.isLayer ? [{ node, nodeIndex }] : [])) as { node, nodeIndex } (nodeIndex)}
{@const clipPathId = String(Math.random()).substring(2)} {@const clipPathId = String(Math.random()).substring(2)}
{@const stackDataInput = node.exposedInputs[0]} {@const stackDataInput = node.exposedInputs[0]}
{@const extraWidthToReachGridMultiple = 8} {@const layerAreaWidth = $nodeGraph.layerWidths.get(node.id) || 8}
{@const labelWidthGridCells = Math.ceil(((layerNameLabelWidths?.[String(node.id)] || 0) - extraWidthToReachGridMultiple) / 24)}
<div <div
class="layer" class="layer"
class:selected={showSelected($nodeGraph.selected, boxSelection, node.id, nodeIndex)} class:selected={$nodeGraph.selected.includes(node.id)}
class:previewed={node.previewed} class:previewed={node.previewed}
class:disabled={!node.visible} class:disabled={!node.visible}
style:--offset-left={(node.position?.x || 0) + ($nodeGraph.selected.includes(node.id) ? draggingNodes?.roundX || 0 : 0)} style:--offset-left={(node.position?.x || 0) - 1}
style:--offset-top={(node.position?.y || 0) + ($nodeGraph.selected.includes(node.id) ? draggingNodes?.roundY || 0 : 0)} style:--offset-top={node.position?.y || 0}
style:--clip-path-id={`url(#${clipPathId})`} style:--clip-path-id={`url(#${clipPathId})`}
style:--data-color={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()})`} style:--data-color={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`} style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`}
style:--label-width={labelWidthGridCells} style:--layer-area-width={layerAreaWidth}
style:--node-chain-area-left-extension={node.exposedInputs.length === 0 ? 0 : 1.5} style:--node-chain-area-left-extension={node.exposedInputs.length === 0 ? 0 : 1.5}
data-node={node.id} data-node={node.id}
bind:this={nodeElements[nodeIndex]} bind:this={nodeElements[nodeIndex]}
@ -966,16 +497,18 @@
{/if} {/if}
<div class="details"> <div class="details">
<!-- TODO: Allow the user to edit the name, just like in the Layers panel --> <!-- TODO: Allow the user to edit the name, just like in the Layers panel -->
<span title={editor.handle.inDevelopmentMode() ? `Node ID: ${node.id}` : undefined} bind:offsetWidth={layerNameLabelWidths[String(node.id)]}> <span title={editor.handle.inDevelopmentMode() ? `Node ID: ${node.id}` : undefined}>
{node.alias} {node.alias}
</span> </span>
</div> </div>
<IconButton <IconButton
class={"visibility"} class={"visibility"}
data-visibility-button data-visibility-button
action={(e) => (toggleNodeVisibilityGraph(node.id), e?.stopPropagation())}
size={24} size={24}
icon={node.visible ? "EyeVisible" : "EyeHidden"} icon={node.visible ? "EyeVisible" : "EyeHidden"}
action={() => {
/*Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown*/
}}
tooltip={node.visible ? "Visible" : "Hidden"} tooltip={node.visible ? "Visible" : "Hidden"}
/> />
@ -983,10 +516,7 @@
<defs> <defs>
<clipPath id={clipPathId}> <clipPath id={clipPathId}>
<!-- Keep this equation in sync with the equivalent one in the CSS rule for `.layer { width: ... }` below --> <!-- Keep this equation in sync with the equivalent one in the CSS rule for `.layer { width: ... }` below -->
<path <path clip-rule="evenodd" d={layerBorderMask(24 * layerAreaWidth - 12, node.exposedInputs.length === 0 ? 0 : 36)} />
clip-rule="evenodd"
d={layerBorderMask(72 + 8 + 24 * Math.max(3, labelWidthGridCells) + 8 + 12 + extraWidthToReachGridMultiple, node.exposedInputs.length === 0 ? 0 : 36)}
/>
</clipPath> </clipPath>
</defs> </defs>
</svg> </svg>
@ -998,11 +528,11 @@
{@const clipPathId = String(Math.random()).substring(2)} {@const clipPathId = String(Math.random()).substring(2)}
<div <div
class="node" class="node"
class:selected={showSelected($nodeGraph.selected, boxSelection, node.id, nodeIndex)} class:selected={$nodeGraph.selected.includes(node.id)}
class:previewed={node.previewed} class:previewed={node.previewed}
class:disabled={!node.visible} class:disabled={!node.visible}
style:--offset-left={(node.position?.x || 0) + ($nodeGraph.selected.includes(node.id) ? draggingNodes?.roundX || 0 : 0)} style:--offset-left={node.position?.x || 0}
style:--offset-top={(node.position?.y || 0) + ($nodeGraph.selected.includes(node.id) ? draggingNodes?.roundY || 0 : 0)} style:--offset-top={node.position?.y || 0}
style:--clip-path-id={`url(#${clipPathId})`} style:--clip-path-id={`url(#${clipPathId})`}
style:--data-color={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()})`} style:--data-color={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`} style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`}
@ -1129,13 +659,13 @@
</div> </div>
<!-- Box select widget --> <!-- Box select widget -->
{#if boxSelection} {#if $nodeGraph.box}
<div <div
class="box-selection" class="box-selection"
style:left={`${Math.min(boxSelection.startX, boxSelection.endX)}px`} style:left={`${Math.min($nodeGraph.box.startX, $nodeGraph.box.endX)}px`}
style:top={`${Math.min(boxSelection.startY, boxSelection.endY)}px`} style:top={`${Math.min($nodeGraph.box.startY, $nodeGraph.box.endY)}px`}
style:width={`${Math.abs(boxSelection.startX - boxSelection.endX)}px`} style:width={`${Math.abs($nodeGraph.box.startX - $nodeGraph.box.endX)}px`}
style:height={`${Math.abs(boxSelection.startY - boxSelection.endY)}px`} style:height={`${Math.abs($nodeGraph.box.startY - $nodeGraph.box.endY)}px`}
></div> ></div>
{/if} {/if}
@ -1155,9 +685,8 @@
height: 100%; height: 100%;
background-size: var(--grid-spacing) var(--grid-spacing); background-size: var(--grid-spacing) var(--grid-spacing);
background-position: calc(var(--grid-offset-x) - var(--dot-radius)) calc(var(--grid-offset-y) - var(--dot-radius)); background-position: calc(var(--grid-offset-x) - var(--dot-radius)) calc(var(--grid-offset-y) - var(--dot-radius));
background-image: radial-gradient(circle at var(--dot-radius) var(--dot-radius), var(--color-f-white) var(--dot-radius), transparent 0), background-image: radial-gradient(circle at var(--dot-radius) var(--dot-radius), var(--color-3-darkgray) var(--dot-radius), transparent 0);
radial-gradient(circle at var(--dot-radius) var(--dot-radius), var(--color-3-darkgray) var(--dot-radius), transparent 0); background-repeat: repeat;
background-repeat: no-repeat, repeat;
image-rendering: pixelated; image-rendering: pixelated;
mix-blend-mode: screen; mix-blend-mode: screen;
} }
@ -1403,7 +932,7 @@
--extra-width-to-reach-grid-multiple: 8px; --extra-width-to-reach-grid-multiple: 8px;
--node-chain-area-left-extension: 0; --node-chain-area-left-extension: 0;
// Keep this equation in sync with the equivalent one in the Svelte template `<clipPath><path d="layerBorderMask(...)" /></clipPath>` above // Keep this equation in sync with the equivalent one in the Svelte template `<clipPath><path d="layerBorderMask(...)" /></clipPath>` above
width: calc(72px + 8px + 24px * Max(3, var(--label-width)) + 8px + 12px + var(--extra-width-to-reach-grid-multiple)); width: calc(24px * var(--layer-area-width) - 12px);
padding-left: calc(var(--node-chain-area-left-extension) * 24px); padding-left: calc(var(--node-chain-area-left-extension) * 24px);
margin-left: calc((1.5 - var(--node-chain-area-left-extension)) * 24px); margin-left: calc((1.5 - var(--node-chain-area-left-extension)) * 24px);

View File

@ -159,7 +159,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
function onPointerDown(e: PointerEvent) { function onPointerDown(e: PointerEvent) {
const { target } = e; const { target } = e;
const isTargetingCanvas = target instanceof Element && target.closest("[data-viewport]"); const isTargetingCanvas = target instanceof Element && (target.closest("[data-viewport]") || target.closest("[data-node-graph]"));
const inDialog = target instanceof Element && target.closest("[data-dialog] [data-floating-menu-content]"); const inDialog = target instanceof Element && target.closest("[data-dialog] [data-floating-menu-content]");
const inTextInput = target === textToolInteractiveInputElement; const inTextInput = target === textToolInteractiveInputElement;
@ -209,7 +209,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
function onWheelScroll(e: WheelEvent) { function onWheelScroll(e: WheelEvent) {
const { target } = e; const { target } = e;
const isTargetingCanvas = target instanceof Element && target.closest("[data-viewport]"); const isTargetingCanvas = target instanceof Element && (target.closest("[data-viewport]") || target.closest("[data-node-graph]"));
// Redirect vertical scroll wheel movement into a horizontal scroll on a horizontally scrollable element // Redirect vertical scroll wheel movement into a horizontal scroll on a horizontally scrollable element
// There seems to be no possible way to properly employ the browser's smooth scrolling interpolation // There seems to be no possible way to properly employ the browser's smooth scrolling interpolation

View File

@ -2,30 +2,62 @@ import { writable } from "svelte/store";
import { type Editor } from "@graphite/wasm-communication/editor"; import { type Editor } from "@graphite/wasm-communication/editor";
import { import {
type Box,
type ContextMenuInformation,
type FrontendNode, type FrontendNode,
type FrontendNodeWire as FrontendNodeWire, type FrontendNodeWire as FrontendNodeWire,
type FrontendNodeType, type FrontendNodeType,
type WirePath,
UpdateBox,
UpdateContextMenuInformation,
UpdateLayerWidths,
UpdateNodeGraph, UpdateNodeGraph,
UpdateNodeGraphSelection, UpdateNodeGraphSelection,
UpdateNodeGraphTransform,
UpdateNodeTypes, UpdateNodeTypes,
UpdateNodeThumbnail, UpdateNodeThumbnail,
UpdateSubgraphPath, UpdateSubgraphPath,
UpdateWirePathInProgress,
UpdateZoomWithScroll, UpdateZoomWithScroll,
} from "@graphite/wasm-communication/messages"; } from "@graphite/wasm-communication/messages";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function createNodeGraphState(editor: Editor) { export function createNodeGraphState(editor: Editor) {
const { subscribe, update } = writable({ const { subscribe, update } = writable({
box: undefined as Box | undefined,
contextMenuInformation: undefined as ContextMenuInformation | undefined,
layerWidths: new Map<bigint, number>(),
nodes: [] as FrontendNode[], nodes: [] as FrontendNode[],
wires: [] as FrontendNodeWire[], wires: [] as FrontendNodeWire[],
wirePathInProgress: undefined as WirePath | undefined,
nodeTypes: [] as FrontendNodeType[], nodeTypes: [] as FrontendNodeType[],
zoomWithScroll: false as boolean, zoomWithScroll: false as boolean,
thumbnails: new Map<bigint, string>(), thumbnails: new Map<bigint, string>(),
selected: [] as bigint[], selected: [] as bigint[],
subgraphPath: [] as string[], subgraphPath: [] as string[],
transform: { scale: 1, x: 0, y: 0 },
}); });
// Set up message subscriptions on creation // Set up message subscriptions on creation
editor.subscriptions.subscribeJsMessage(UpdateBox, (updateBox) => {
update((state) => {
state.box = updateBox.box;
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateContextMenuInformation, (updateContextMenuInformation) => {
update((state) => {
state.contextMenuInformation = updateContextMenuInformation.contextMenuInformation;
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateLayerWidths, (updateLayerWidths) => {
update((state) => {
state.layerWidths = updateLayerWidths.layerWidths;
return state;
});
});
// TODO: Add a way to only update the nodes that have changed
editor.subscriptions.subscribeJsMessage(UpdateNodeGraph, (updateNodeGraph) => { editor.subscriptions.subscribeJsMessage(UpdateNodeGraph, (updateNodeGraph) => {
update((state) => { update((state) => {
state.nodes = updateNodeGraph.nodes; state.nodes = updateNodeGraph.nodes;
@ -39,6 +71,12 @@ export function createNodeGraphState(editor: Editor) {
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateNodeGraphTransform, (updateNodeGraphTransform) => {
update((state) => {
state.transform = updateNodeGraphTransform.transform;
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateNodeTypes, (updateNodeTypes) => { editor.subscriptions.subscribeJsMessage(UpdateNodeTypes, (updateNodeTypes) => {
update((state) => { update((state) => {
state.nodeTypes = updateNodeTypes.nodeTypes; state.nodeTypes = updateNodeTypes.nodeTypes;
@ -57,6 +95,12 @@ export function createNodeGraphState(editor: Editor) {
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateWirePathInProgress, (updateWirePathInProgress) => {
update((state) => {
state.wirePathInProgress = updateWirePathInProgress.wirePath;
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateZoomWithScroll, (updateZoomWithScroll) => { editor.subscriptions.subscribeJsMessage(UpdateZoomWithScroll, (updateZoomWithScroll) => {
update((state) => { update((state) => {
state.zoomWithScroll = updateZoomWithScroll.zoomWithScroll; state.zoomWithScroll = updateZoomWithScroll.zoomWithScroll;

View File

@ -25,6 +25,31 @@ export type XY = { x: number; y: number };
// for details about how to transform the JSON from wasm-bindgen into classes. // for details about how to transform the JSON from wasm-bindgen into classes.
// ============================================================================ // ============================================================================
export class UpdateBox extends JsMessage {
readonly box!: Box | undefined;
}
const ContextTupleToVec2 = Transform((data) => {
if (data.obj.contextMenuInformation === undefined) return undefined;
const contextMenuCoordinates = { x: data.obj.contextMenuInformation.contextMenuCoordinates[0], y: data.obj.contextMenuInformation.contextMenuCoordinates[1] };
let contextMenuData = data.obj.contextMenuInformation.contextMenuData;
if (contextMenuData.ToggleLayer !== undefined) {
contextMenuData = { nodeId: contextMenuData.ToggleLayer.nodeId, currentlyIsNode: contextMenuData.ToggleLayer.currentlyIsNode };
}
return { contextMenuCoordinates, contextMenuData };
});
export class UpdateContextMenuInformation extends JsMessage {
@ContextTupleToVec2
readonly contextMenuInformation!: ContextMenuInformation | undefined;
}
const LayerWidths = Transform(({ obj }) => obj.layerWidths);
export class UpdateLayerWidths extends JsMessage {
@LayerWidths
readonly layerWidths!: Map<bigint, number>;
}
export class UpdateNodeGraph extends JsMessage { export class UpdateNodeGraph extends JsMessage {
@Type(() => FrontendNode) @Type(() => FrontendNode)
readonly nodes!: FrontendNode[]; readonly nodes!: FrontendNode[];
@ -33,6 +58,10 @@ export class UpdateNodeGraph extends JsMessage {
readonly wires!: FrontendNodeWire[]; readonly wires!: FrontendNodeWire[];
} }
export class UpdateNodeGraphTransform extends JsMessage {
readonly transform!: NodeGraphTransform;
}
export class UpdateNodeTypes extends JsMessage { export class UpdateNodeTypes extends JsMessage {
@Type(() => FrontendNode) @Type(() => FrontendNode)
readonly nodeTypes!: FrontendNodeType[]; readonly nodeTypes!: FrontendNodeType[];
@ -58,6 +87,10 @@ export class UpdateSubgraphPath extends JsMessage {
readonly subgraphPath!: string[]; readonly subgraphPath!: string[];
} }
export class UpdateWirePathInProgress extends JsMessage {
readonly wirePath!: WirePath | undefined;
}
export class UpdateZoomWithScroll extends JsMessage { export class UpdateZoomWithScroll extends JsMessage {
readonly zoomWithScroll!: boolean; readonly zoomWithScroll!: boolean;
} }
@ -84,6 +117,22 @@ export class FrontendDocumentDetails extends DocumentDetails {
readonly id!: bigint; readonly id!: bigint;
} }
export class Box {
readonly startX!: number;
readonly startY!: number;
readonly endX!: number;
readonly endY!: number;
}
export type ContextMenuInformation = {
contextMenuCoordinates: XY;
contextMenuData: "CreateNode" | { nodeId: bigint; currentlyIsNode: boolean };
};
export type FrontendGraphDataType = "General" | "Raster" | "VectorData" | "Number" | "Graphic" | "Artboard"; export type FrontendGraphDataType = "General" | "Raster" | "VectorData" | "Number" | "Graphic" | "Artboard";
export class FrontendGraphInput { export class FrontendGraphInput {
@ -130,6 +179,8 @@ export class FrontendNode {
@TupleToVec2 @TupleToVec2
readonly position!: XY | undefined; readonly position!: XY | undefined;
//TODO: Store field for the width of the left node chain
readonly previewed!: boolean; readonly previewed!: boolean;
readonly visible!: boolean; readonly visible!: boolean;
@ -159,6 +210,19 @@ export class FrontendNodeType {
readonly category!: string; readonly category!: string;
} }
export class NodeGraphTransform {
readonly scale!: number;
readonly x!: number;
readonly y!: number;
}
export class WirePath {
readonly pathString!: string;
readonly dataType!: FrontendGraphDataType;
readonly thick!: boolean;
readonly dashed!: boolean;
}
export class IndexedDbDocumentDetails extends DocumentDetails { export class IndexedDbDocumentDetails extends DocumentDetails {
@Transform(({ value }: { value: bigint }) => value.toString()) @Transform(({ value }: { value: bigint }) => value.toString())
id!: string; id!: string;
@ -1378,6 +1442,9 @@ export const messageMakers: Record<string, MessageMaker> = {
TriggerViewportResize, TriggerViewportResize,
TriggerVisitLink, TriggerVisitLink,
UpdateActiveDocument, UpdateActiveDocument,
UpdateBox,
UpdateContextMenuInformation,
UpdateLayerWidths,
UpdateDialogButtons, UpdateDialogButtons,
UpdateDialogColumn1, UpdateDialogColumn1,
UpdateDialogColumn2, UpdateDialogColumn2,
@ -1396,6 +1463,7 @@ export const messageMakers: Record<string, MessageMaker> = {
UpdateNodeGraph, UpdateNodeGraph,
UpdateNodeGraphBarLayout, UpdateNodeGraphBarLayout,
UpdateNodeGraphSelection, UpdateNodeGraphSelection,
UpdateNodeGraphTransform,
UpdateNodeThumbnail, UpdateNodeThumbnail,
UpdateNodeTypes, UpdateNodeTypes,
UpdateOpenDocumentsList, UpdateOpenDocumentsList,
@ -1405,6 +1473,7 @@ export const messageMakers: Record<string, MessageMaker> = {
UpdateToolOptionsLayout, UpdateToolOptionsLayout,
UpdateToolShelfLayout, UpdateToolShelfLayout,
UpdateWorkingColorsLayout, UpdateWorkingColorsLayout,
UpdateWirePathInProgress,
UpdateZoomWithScroll, UpdateZoomWithScroll,
} as const; } as const;
export type JsMessageType = keyof typeof messageMakers; export type JsMessageType = keyof typeof messageMakers;

View File

@ -25,6 +25,7 @@ const ALLOWED_LICENSES = [
"MIT", "MIT",
"MPL-2.0", "MPL-2.0",
"OpenSSL", "OpenSSL",
"Unicode-3.0",
"Unicode-DFS-2016", "Unicode-DFS-2016",
"Zlib", "Zlib",
]; ];

View File

@ -551,87 +551,12 @@ impl EditorHandle {
self.dispatch(message); self.dispatch(message);
} }
/// Notifies the backend that the user connected a node's primary output to one of another node's inputs
#[wasm_bindgen(js_name = connectNodesByWire)]
pub fn connect_nodes_by_wire(&self, output_node: u64, output_node_connector_index: usize, input_node: u64, input_node_connector_index: usize) {
let output_node = NodeId(output_node);
let input_node = NodeId(input_node);
let message = NodeGraphMessage::ConnectNodesByWire {
output_node,
output_node_connector_index,
input_node,
input_node_connector_index,
};
self.dispatch(message);
}
/// Inserts node in-between two other nodes
#[wasm_bindgen(js_name = insertNodeBetween)]
pub fn insert_node_between(
&self,
post_node_id: u64,
post_node_input_index: usize,
insert_node_output_index: usize,
insert_node_id: u64,
insert_node_input_index: usize,
pre_node_output_index: usize,
pre_node_id: u64,
) {
let message = NodeGraphMessage::InsertNodeBetween {
post_node_id: NodeId(post_node_id),
post_node_input_index,
insert_node_output_index,
insert_node_id: NodeId(insert_node_id),
insert_node_input_index,
pre_node_output_index,
pre_node_id: NodeId(pre_node_id),
};
self.dispatch(message);
}
/// Shifts the node and its children to stop nodes going on top of each other
#[wasm_bindgen(js_name = shiftNode)]
pub fn shift_node(&self, node_id: u64) {
let node_id = NodeId(node_id);
let message = NodeGraphMessage::ShiftNode { node_id };
self.dispatch(message);
}
/// Notifies the backend that the user disconnected a node
#[wasm_bindgen(js_name = disconnectNodes)]
pub fn disconnect_nodes(&self, node_id: u64, input_index: usize) {
let node_id = NodeId(node_id);
let message = NodeGraphMessage::DisconnectInput { node_id, input_index };
self.dispatch(message);
}
/// Check for intersections between the curve and a rectangle defined by opposite corners
#[wasm_bindgen(js_name = rectangleIntersects)]
pub fn rectangle_intersects(&self, bezier_x: Vec<f64>, bezier_y: Vec<f64>, top: f64, left: f64, bottom: f64, right: f64) -> bool {
let bezier = bezier_rs::Bezier::from_cubic_dvec2(
(bezier_x[0], bezier_y[0]).into(),
(bezier_x[1], bezier_y[1]).into(),
(bezier_x[2], bezier_y[2]).into(),
(bezier_x[3], bezier_y[3]).into(),
);
!bezier.rectangle_intersections((left, top).into(), (right, bottom).into()).is_empty() || bezier.is_contained_within((left, top).into(), (right, bottom).into())
}
/// Creates a new document node in the node graph /// Creates a new document node in the node graph
#[wasm_bindgen(js_name = createNode)] #[wasm_bindgen(js_name = createNode)]
pub fn create_node(&self, node_type: String, x: i32, y: i32) -> u64 { pub fn create_node(&self, node_type: String, x: i32, y: i32) {
let id = NodeId(generate_uuid()); let id = NodeId(generate_uuid());
let message = NodeGraphMessage::CreateNode { node_id: Some(id), node_type, x, y }; let message = NodeGraphMessage::CreateNode { node_id: Some(id), node_type, x, y };
self.dispatch(message); self.dispatch(message);
id.0
}
/// Notifies the backend that the user selected a node in the node graph
#[wasm_bindgen(js_name = selectNodes)]
pub fn select_nodes(&self, nodes: Vec<u64>) {
let nodes = nodes.into_iter().map(NodeId).collect::<Vec<_>>();
let message = NodeGraphMessage::SelectedNodesSet { nodes };
self.dispatch(message);
} }
/// Pastes the nodes based on serialized data /// Pastes the nodes based on serialized data
@ -641,14 +566,6 @@ impl EditorHandle {
self.dispatch(message); self.dispatch(message);
} }
/// Notifies the backend that the user double clicked a node
#[wasm_bindgen(js_name = enterNestedNetwork)]
pub fn enter_nested_network(&self, node: u64) {
let node = NodeId(node);
let message = NodeGraphMessage::EnterNestedNetwork { node };
self.dispatch(message);
}
/// Go back a certain number of nested levels /// Go back a certain number of nested levels
#[wasm_bindgen(js_name = exitNestedNetwork)] #[wasm_bindgen(js_name = exitNestedNetwork)]
pub fn exit_nested_network(&self, steps_back: usize) { pub fn exit_nested_network(&self, steps_back: usize) {
@ -656,24 +573,6 @@ impl EditorHandle {
self.dispatch(message); self.dispatch(message);
} }
/// Notifies the backend that the selected nodes have been moved
#[wasm_bindgen(js_name = moveSelectedNodes)]
pub fn move_selected_nodes(&self, displacement_x: i32, displacement_y: i32) {
let message = DocumentMessage::StartTransaction;
self.dispatch(message);
let message = NodeGraphMessage::MoveSelectedNodes { displacement_x, displacement_y };
self.dispatch(message);
}
/// Toggle preview on node
#[wasm_bindgen(js_name = togglePreview)]
pub fn toggle_preview(&self, node_id: u64) {
let node_id = NodeId(node_id);
let message = NodeGraphMessage::TogglePreview { node_id };
self.dispatch(message);
}
/// Pastes an image /// Pastes an image
#[wasm_bindgen(js_name = pasteImage)] #[wasm_bindgen(js_name = pasteImage)]
pub fn paste_image(&self, image_data: Vec<u8>, width: u32, height: u32, mouse_x: Option<f64>, mouse_y: Option<f64>) { pub fn paste_image(&self, image_data: Vec<u8>, width: u32, height: u32, mouse_x: Option<f64>, mouse_y: Option<f64>) {
@ -698,14 +597,6 @@ impl EditorHandle {
self.dispatch(message); self.dispatch(message);
} }
/// Toggle visibility of a layer or node given its node ID
#[wasm_bindgen(js_name = toggleNodeVisibilityGraph)]
pub fn toggle_node_visibility_graph(&self, id: u64) {
let node_id = NodeId(id);
let message = NodeGraphMessage::ToggleVisibility { node_id };
self.dispatch(message);
}
/// Delete a layer or node given its node ID /// Delete a layer or node given its node ID
#[wasm_bindgen(js_name = deleteNode)] #[wasm_bindgen(js_name = deleteNode)]
pub fn delete_node(&self, id: u64) { pub fn delete_node(&self, id: u64) {

View File

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

View File

@ -5,7 +5,7 @@ use dyn_any::{DynAny, StaticType};
pub use graphene_core::uuid::generate_uuid; pub use graphene_core::uuid::generate_uuid;
use graphene_core::{ProtoNodeIdentifier, Type}; use graphene_core::{ProtoNodeIdentifier, Type};
use glam::IVec2; use glam::{DAffine2, IVec2};
use std::collections::hash_map::DefaultHasher; use std::collections::hash_map::DefaultHasher;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
@ -125,6 +125,7 @@ where
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DocumentNode { 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. /// 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)] #[serde(default)]
pub alias: String, pub alias: String,
// TODO: Replace this name with a reference to the [`DocumentNodeDefinition`] node definition to use the name from there instead. // 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 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. /// - 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. /// 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")] #[serde(deserialize_with = "deserialize_inputs")]
pub inputs: Vec<NodeInput>, pub inputs: Vec<NodeInput>,
/// Manual composition is a way to override the default composition flow of one node into another. /// 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, pub has_primary_output: bool,
// A nested document network or a proto-node identifier. // A nested document network or a proto-node identifier.
pub implementation: DocumentNodeImplementation, 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)] #[serde(default)]
pub is_layer: bool, 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. /// 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. /// 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)] #[serde(default)]
pub locked: bool, 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, 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. /// 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. /// See [`crate::proto::ProtoNetwork::generate_stable_node_ids`] for details.
@ -288,7 +290,7 @@ impl Default for DocumentNode {
is_layer: false, is_layer: false,
visible: true, visible: true,
locked: Default::default(), locked: Default::default(),
metadata: Default::default(), metadata: DocumentNodeMetadata::default(),
skip_deduplication: Default::default(), skip_deduplication: Default::default(),
world_state_hash: Default::default(), world_state_hash: Default::default(),
original_location: OriginalLocation::default(), original_location: OriginalLocation::default(),
@ -645,7 +647,7 @@ fn default_export_metadata() -> (NodeId, IVec2) {
(NodeId(generate_uuid()), IVec2::new(8, -4)) (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))] #[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 /// 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 { pub struct NodeNetwork {
@ -663,6 +665,9 @@ pub struct NodeNetwork {
pub imports_metadata: (NodeId, IVec2), pub imports_metadata: (NodeId, IVec2),
#[serde(default = "default_export_metadata")] #[serde(default = "default_export_metadata")]
pub exports_metadata: (NodeId, IVec2), 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 { impl std::hash::Hash for NodeNetwork {
@ -685,9 +690,16 @@ impl Default for NodeNetwork {
previewing: Default::default(), previewing: Default::default(),
imports_metadata: default_import_metadata(), imports_metadata: default_import_metadata(),
exports_metadata: default_export_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 /// Graph modification functions
impl NodeNetwork { impl NodeNetwork {
pub fn current_hash(&self) -> u64 { 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 /// 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 { // 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")); // let id = NodeId(self.nodes.len().try_into().expect("Too many nodes in network"));
// Set the correct position for the new node // // Set the correct position for the new node
if node.metadata.position == IVec2::default() { // 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) { // 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); // node.metadata.position = pos + IVec2::new(8, 0);
} // }
} // }
if !self.exports.is_empty() { // if !self.exports.is_empty() {
let input = self.exports[0].clone(); // let input = self.exports[0].clone();
if node.inputs.is_empty() { // if node.inputs.is_empty() {
node.inputs.push(input); // node.inputs.push(input);
} else { // } else {
node.inputs[0] = input; // node.inputs[0] = input;
} // }
} // }
self.nodes.insert(id, node); // // Use node_graph.insert_node
self.exports = vec![NodeInput::node(id, 0)]; // self.insert_node(id, node);
id // self.exports = vec![NodeInput::node(id, 0)];
} // id
// }
/// Get the nested network given by the path of node ids /// Get the nested network given by the path of node ids
pub fn nested_network(&self, nested_path: &[NodeId]) -> Option<&Self> { pub fn nested_network(&self, nested_path: &[NodeId]) -> Option<&Self> {
@ -816,7 +829,7 @@ impl NodeNetwork {
network 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> { 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) { 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) 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> { 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)) { if selected_nodes.any(|node_id| self.nodes.contains_key(node_id)) {
Some(self) 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) { let compute_pass_descriptor = if self.cache.borrow().contains_key(&node.name) {
self.cache.borrow().get(&node.name).unwrap().clone() self.cache.borrow().get(&node.name).unwrap().clone()
} else { } 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 { 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()"); log::error!("Error creating compute pass descriptor in 'map_gpu()");
return ImageFrame::empty(); return ImageFrame::empty();