Add shifting of layers in stacks as blocks that collide and bump other layers/nodes (#1940)

* Shift nodes as blocks

* Implement rubber banding

* Improve upstream locking when shifting layers

* WIP: Reworked shifting

* WIP: Reworked node shifting

* Finish shifting

* Fix demo artwork

* Code review

* Right click to end shift

* Improve rubber banding

* Fix clippy issues

* Skip collision for intersecting nodes

* Rubber banding bug fix

* Fix ctrl+delete node in chain

* Grip drag

* Fix layer width

* Add icon to frontend for the solo drag grip

* Reconnect during ctrl+delete

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
adamgerhant 2024-08-22 01:56:32 -07:00 committed by GitHub
parent a7840b252d
commit 78337f9b8e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 1238 additions and 308 deletions

View File

@ -1,7 +1,9 @@
// Graph // Graph
pub const GRID_SIZE: u32 = 24; pub const GRID_SIZE: u32 = 24;
pub const EXPORTS_TO_EDGE_PIXEL_GAP: u32 = 120; pub const EXPORTS_TO_TOP_EDGE_PIXEL_GAP: u32 = 72;
pub const IMPORTS_TO_EDGE_PIXEL_GAP: u32 = 120; pub const EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP: u32 = 120;
pub const IMPORTS_TO_TOP_EDGE_PIXEL_GAP: u32 = 72;
pub const IMPORTS_TO_LEFT_EDGE_PIXEL_GAP: u32 = 120;
// Viewport // Viewport
pub const VIEWPORT_ZOOM_WHEEL_RATE: f64 = (1. / 600.) * 3.; pub const VIEWPORT_ZOOM_WHEEL_RATE: f64 = (1. / 600.) * 3.;

View File

@ -386,7 +386,7 @@ mod test {
nodes: vec![rect_id.to_node(), ellipse_id.to_node()], nodes: vec![rect_id.to_node(), ellipse_id.to_node()],
}); });
editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::Internal }); editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::Internal });
editor.handle_message(NodeGraphMessage::DeleteSelectedNodes { reconnect: true }); editor.handle_message(NodeGraphMessage::DeleteSelectedNodes { delete_children: true });
editor.draw_rect(0., 800., 12., 200.); editor.draw_rect(0., 800., 12., 200.);
editor.handle_message(PortfolioMessage::PasteIntoFolder { editor.handle_message(PortfolioMessage::PasteIntoFolder {
clipboard: Clipboard::Internal, clipboard: Clipboard::Internal,

View File

@ -5,6 +5,7 @@ use crate::messages::input_mapper::utility_types::input_mouse::MouseButton;
use crate::messages::input_mapper::utility_types::macros::*; use crate::messages::input_mapper::utility_types::macros::*;
use crate::messages::input_mapper::utility_types::misc::MappingEntry; use crate::messages::input_mapper::utility_types::misc::MappingEntry;
use crate::messages::input_mapper::utility_types::misc::{KeyMappingEntries, Mapping}; use crate::messages::input_mapper::utility_types::misc::{KeyMappingEntries, Mapping};
use crate::messages::portfolio::document::node_graph::utility_types::Direction;
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard; use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use crate::messages::tool::tool_messages::brush_tool::BrushToolMessageOptionsUpdate; use crate::messages::tool::tool_messages::brush_tool::BrushToolMessageOptionsUpdate;
@ -61,11 +62,10 @@ pub fn input_mappings() -> Mapping {
entry!(DoubleClick(MouseButton::Left); action_dispatch=NodeGraphMessage::EnterNestedNetwork), entry!(DoubleClick(MouseButton::Left); action_dispatch=NodeGraphMessage::EnterNestedNetwork),
entry!(PointerMove; refresh_keys=[Shift], action_dispatch=NodeGraphMessage::PointerMove {shift: Shift}), entry!(PointerMove; refresh_keys=[Shift], action_dispatch=NodeGraphMessage::PointerMove {shift: Shift}),
entry!(KeyUp(Lmb); action_dispatch=NodeGraphMessage::PointerUp), entry!(KeyUp(Lmb); action_dispatch=NodeGraphMessage::PointerUp),
entry!(KeyUp(Escape); action_dispatch=NodeGraphMessage::CloseCreateNodeMenu), entry!(KeyDown(Delete); modifiers=[Accel], action_dispatch=NodeGraphMessage::DeleteSelectedNodes { delete_children: false }),
entry!(KeyDown(Delete); modifiers=[Accel], action_dispatch=NodeGraphMessage::DeleteSelectedNodes { reconnect: false }), entry!(KeyDown(Backspace); modifiers=[Accel], action_dispatch=NodeGraphMessage::DeleteSelectedNodes { delete_children: false }),
entry!(KeyDown(Backspace); modifiers=[Accel], action_dispatch=NodeGraphMessage::DeleteSelectedNodes { reconnect: false }), entry!(KeyDown(Delete); action_dispatch=NodeGraphMessage::DeleteSelectedNodes { delete_children: true }),
entry!(KeyDown(Delete); action_dispatch=NodeGraphMessage::DeleteSelectedNodes { reconnect: true }), entry!(KeyDown(Backspace); action_dispatch=NodeGraphMessage::DeleteSelectedNodes { delete_children: true }),
entry!(KeyDown(Backspace); action_dispatch=NodeGraphMessage::DeleteSelectedNodes { reconnect: true }),
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),
@ -75,6 +75,10 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(KeyC); modifiers=[Shift], action_dispatch=NodeGraphMessage::PrintSelectedNodeCoordinates), entry!(KeyDown(KeyC); modifiers=[Shift], action_dispatch=NodeGraphMessage::PrintSelectedNodeCoordinates),
entry!(KeyDown(KeyC); modifiers=[Alt], action_dispatch=NodeGraphMessage::SendClickTargets), entry!(KeyDown(KeyC); modifiers=[Alt], action_dispatch=NodeGraphMessage::SendClickTargets),
entry!(KeyUp(KeyC); action_dispatch=NodeGraphMessage::EndSendClickTargets), entry!(KeyUp(KeyC); action_dispatch=NodeGraphMessage::EndSendClickTargets),
entry!(KeyDown(ArrowUp); action_dispatch=NodeGraphMessage::ShiftSelectedNodes { direction: Direction::Up, rubber_band: false }),
entry!(KeyDown(ArrowRight); action_dispatch=NodeGraphMessage::ShiftSelectedNodes { direction: Direction::Right, rubber_band: false }),
entry!(KeyDown(ArrowDown); action_dispatch=NodeGraphMessage::ShiftSelectedNodes { direction: Direction::Down, rubber_band: false }),
entry!(KeyDown(ArrowLeft); action_dispatch=NodeGraphMessage::ShiftSelectedNodes { direction: Direction::Left, rubber_band: false }),
// //
// TransformLayerMessage // TransformLayerMessage
entry!(KeyDown(Enter); action_dispatch=TransformLayerMessage::ApplyTransformOperation), entry!(KeyDown(Enter); action_dispatch=TransformLayerMessage::ApplyTransformOperation),
@ -301,7 +305,7 @@ pub fn input_mappings() -> Mapping {
// //
// DocumentMessage // DocumentMessage
entry!(KeyDown(Space); modifiers=[Control], action_dispatch=DocumentMessage::GraphViewOverlayToggle), entry!(KeyDown(Space); modifiers=[Control], action_dispatch=DocumentMessage::GraphViewOverlayToggle),
entry!(KeyUp(Escape); action_dispatch=DocumentMessage::GraphViewOverlay { open: false }), entry!(KeyUp(Escape); action_dispatch=DocumentMessage::Escape),
entry!(KeyDown(Delete); action_dispatch=DocumentMessage::DeleteSelectedLayers), entry!(KeyDown(Delete); action_dispatch=DocumentMessage::DeleteSelectedLayers),
entry!(KeyDown(Backspace); action_dispatch=DocumentMessage::DeleteSelectedLayers), entry!(KeyDown(Backspace); action_dispatch=DocumentMessage::DeleteSelectedLayers),
entry!(KeyDown(KeyP); modifiers=[Alt], action_dispatch=DocumentMessage::DebugPrintDocument), entry!(KeyDown(KeyP); modifiers=[Alt], action_dispatch=DocumentMessage::DebugPrintDocument),

View File

@ -57,6 +57,7 @@ pub enum DocumentMessage {
EnterNestedNetwork { EnterNestedNetwork {
node_id: NodeId, node_id: NodeId,
}, },
Escape,
ExitNestedNetwork { ExitNestedNetwork {
steps_back: usize, steps_back: usize,
}, },

View File

@ -366,7 +366,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
info!("{:#?}", self.network_interface); info!("{:#?}", self.network_interface);
} }
DocumentMessage::DeleteSelectedLayers => { DocumentMessage::DeleteSelectedLayers => {
responses.add(NodeGraphMessage::DeleteSelectedNodes { reconnect: true }); responses.add(NodeGraphMessage::DeleteSelectedNodes { delete_children: true });
} }
DocumentMessage::DeselectAllLayers => { DocumentMessage::DeselectAllLayers => {
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] }); responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] });
@ -400,6 +400,26 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
responses.add(NodeGraphMessage::SendGraph); responses.add(NodeGraphMessage::SendGraph);
responses.add(DocumentMessage::ZoomCanvasToFitAll); responses.add(DocumentMessage::ZoomCanvasToFitAll);
} }
DocumentMessage::Escape => {
if self.node_graph_handler.drag_start.is_some() {
responses.add(DocumentMessage::AbortTransaction);
self.node_graph_handler.drag_start = None;
} else if self
.node_graph_handler
.context_menu
.as_ref()
.is_some_and(|context_menu| matches!(context_menu.context_menu_data, super::node_graph::utility_types::ContextMenuData::CreateNode))
{
// Close the context menu
self.node_graph_handler.context_menu = None;
responses.add(FrontendMessage::UpdateContextMenuInformation { context_menu_information: None });
self.node_graph_handler.wire_in_progress_from_connector = None;
self.node_graph_handler.wire_in_progress_to_connector = None;
responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: None });
} else {
responses.add(DocumentMessage::GraphViewOverlay { open: false });
}
}
DocumentMessage::ExitNestedNetwork { steps_back } => { DocumentMessage::ExitNestedNetwork { steps_back } => {
for _ in 0..steps_back { for _ in 0..steps_back {
self.breadcrumb_network_path.pop(); self.breadcrumb_network_path.pop();
@ -434,10 +454,12 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
responses.add(FrontendMessage::TriggerGraphViewOverlay { open }); responses.add(FrontendMessage::TriggerGraphViewOverlay { open });
// Update the tilt menu bar buttons to be disabled when the graph is open // Update the tilt menu bar buttons to be disabled when the graph is open
responses.add(MenuBarMessage::SendLayout); responses.add(MenuBarMessage::SendLayout);
responses.add(DocumentMessage::RenderRulers);
responses.add(DocumentMessage::RenderScrollbars);
if open { if open {
responses.add(NavigationMessage::CanvasTiltSet { angle_radians: 0. });
responses.add(NodeGraphMessage::SetGridAlignedEdges); responses.add(NodeGraphMessage::SetGridAlignedEdges);
responses.add(NodeGraphMessage::SendGraph); responses.add(NodeGraphMessage::SendGraph);
responses.add(NavigationMessage::CanvasTiltSet { angle_radians: 0. });
} }
} }
DocumentMessage::GraphViewOverlayToggle => { DocumentMessage::GraphViewOverlayToggle => {
@ -639,16 +661,6 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
resize_opposite_corner, resize_opposite_corner,
} => { } => {
self.backup(responses); self.backup(responses);
if self.graph_view_overlay_open {
responses.add(NodeGraphMessage::ShiftNodes {
node_ids: self.network_interface.selected_nodes(&[]).unwrap().selected_nodes().cloned().collect(),
displacement_x: if delta_x == 0.0 { 0 } else { delta_x.signum() as i32 },
displacement_y: if delta_y == 0.0 { 0 } else { delta_y.signum() as i32 },
move_upstream: ipp.keyboard.get(Key::Shift as usize),
});
return;
}
let opposite_corner = ipp.keyboard.key(resize_opposite_corner); let opposite_corner = ipp.keyboard.key(resize_opposite_corner);
let delta = DVec2::new(delta_x, delta_y); let delta = DVec2::new(delta_x, delta_y);
@ -862,7 +874,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
self.selected_layers_reorder(relative_index_offset, responses); self.selected_layers_reorder(relative_index_offset, responses);
} }
DocumentMessage::SelectLayer { id, ctrl, shift } => { DocumentMessage::SelectLayer { id, ctrl, shift } => {
let layer = LayerNodeIdentifier::new(id, &self.network_interface); let layer = LayerNodeIdentifier::new(id, &self.network_interface, &[]);
let mut nodes = vec![]; let mut nodes = vec![];
@ -965,7 +977,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
} }
DocumentMessage::StartTransaction => self.backup(responses), DocumentMessage::StartTransaction => self.backup(responses),
DocumentMessage::ToggleLayerExpansion { id } => { DocumentMessage::ToggleLayerExpansion { id } => {
let layer = LayerNodeIdentifier::new(id, &self.network_interface); let layer = LayerNodeIdentifier::new(id, &self.network_interface, &[]);
if self.collapsed.0.contains(&layer) { if self.collapsed.0.contains(&layer) {
self.collapsed.0.retain(|&collapsed_layer| collapsed_layer != layer); self.collapsed.0.retain(|&collapsed_layer| collapsed_layer != layer);
} else { } else {
@ -1040,7 +1052,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
// Delete empty group folder // Delete empty group folder
responses.add(NodeGraphMessage::DeleteNodes { responses.add(NodeGraphMessage::DeleteNodes {
node_ids: vec![layer.to_node()], node_ids: vec![layer.to_node()],
reconnect: true, delete_children: true,
}); });
} }
DocumentMessage::PTZUpdate => { DocumentMessage::PTZUpdate => {
@ -1123,23 +1135,26 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
// Additional actions if there are any selected layers // Additional actions if there are any selected layers
if self.network_interface.selected_nodes(&[]).unwrap().selected_layers(self.metadata()).next().is_some() { if self.network_interface.selected_nodes(&[]).unwrap().selected_layers(self.metadata()).next().is_some() {
let select = actions!(DocumentMessageDiscriminant; let mut select = actions!(DocumentMessageDiscriminant;
DeleteSelectedLayers, DeleteSelectedLayers,
DuplicateSelectedLayers, DuplicateSelectedLayers,
GroupSelectedLayers, GroupSelectedLayers,
NudgeSelectedLayers,
SelectedLayersLower, SelectedLayersLower,
SelectedLayersLowerToBack, SelectedLayersLowerToBack,
SelectedLayersRaise, SelectedLayersRaise,
SelectedLayersRaiseToFront, SelectedLayersRaiseToFront,
UngroupSelectedLayers, UngroupSelectedLayers,
); );
if !self.graph_view_overlay_open {
select.extend(actions!(DocumentMessageDiscriminant; NudgeSelectedLayers));
}
common.extend(select); common.extend(select);
} }
// Additional actions if the node graph is open // Additional actions if the node graph is open
if self.graph_view_overlay_open { if self.graph_view_overlay_open {
common.extend(actions!(DocumentMessageDiscriminant; common.extend(actions!(DocumentMessageDiscriminant;
GraphViewOverlay, Escape
)); ));
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());
} }

View File

@ -124,7 +124,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
let primary_input = artboard.inputs.first().expect("Artboard should have a primary input").clone(); let primary_input = artboard.inputs.first().expect("Artboard should have a primary input").clone();
if let NodeInput::Node { node_id, .. } = &primary_input { if let NodeInput::Node { node_id, .. } = &primary_input {
if network_interface.is_layer(node_id, &[]) && !network_interface.is_artboard(node_id, &[]) { if network_interface.is_layer(node_id, &[]) && !network_interface.is_artboard(node_id, &[]) {
network_interface.move_layer_to_stack(LayerNodeIdentifier::new(*node_id, network_interface), artboard_layer, 0, &[]); network_interface.move_layer_to_stack(LayerNodeIdentifier::new(*node_id, network_interface, &[]), artboard_layer, 0, &[]);
} else { } else {
network_interface.disconnect_input(&InputConnector::node(artboard_layer.to_node(), 0), &[]); network_interface.disconnect_input(&InputConnector::node(artboard_layer.to_node(), 0), &[]);
network_interface.set_input(&InputConnector::node(id, 0), primary_input, &[]); network_interface.set_input(&InputConnector::node(id, 0), primary_input, &[]);
@ -202,7 +202,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
for artboard in network_interface.all_artboards() { for artboard in network_interface.all_artboards() {
responses.add(NodeGraphMessage::DeleteNodes { responses.add(NodeGraphMessage::DeleteNodes {
node_ids: vec![artboard.to_node()], node_ids: vec![artboard.to_node()],
reconnect: false, delete_children: false,
}); });
} }
// TODO: Replace deleted artboards with merge nodes // TODO: Replace deleted artboards with merge nodes

View File

@ -116,7 +116,7 @@ impl<'a> ModifyInputsContext<'a> {
pub fn create_layer(&mut self, new_id: NodeId) -> LayerNodeIdentifier { pub fn create_layer(&mut self, new_id: NodeId) -> LayerNodeIdentifier {
let new_merge_node = resolve_document_node_type("Merge").expect("Merge node").default_node_template(); let new_merge_node = resolve_document_node_type("Merge").expect("Merge node").default_node_template();
self.network_interface.insert_node(new_id, new_merge_node, &[]); self.network_interface.insert_node(new_id, new_merge_node, &[]);
LayerNodeIdentifier::new(new_id, self.network_interface) LayerNodeIdentifier::new(new_id, self.network_interface, &[])
} }
/// Creates an artboard as the primary export for the document network /// Creates an artboard as the primary export for the document network
@ -130,7 +130,7 @@ impl<'a> ModifyInputsContext<'a> {
Some(NodeInput::value(TaggedValue::Bool(artboard.clip), false)), Some(NodeInput::value(TaggedValue::Bool(artboard.clip), false)),
]); ]);
self.network_interface.insert_node(new_id, artboard_node_template, &[]); self.network_interface.insert_node(new_id, artboard_node_template, &[]);
LayerNodeIdentifier::new(new_id, self.network_interface) LayerNodeIdentifier::new(new_id, self.network_interface, &[])
} }
pub fn insert_boolean_data(&mut self, operation: graphene_std::vector::misc::BooleanOperation, layer: LayerNodeIdentifier) { pub fn insert_boolean_data(&mut self, operation: graphene_std::vector::misc::BooleanOperation, layer: LayerNodeIdentifier) {
@ -222,7 +222,7 @@ impl<'a> ModifyInputsContext<'a> {
}; };
let export_node = network.exports.first().and_then(|export| export.as_node())?; let export_node = network.exports.first().and_then(|export| export.as_node())?;
if self.network_interface.is_layer(&export_node, &[]) { if self.network_interface.is_layer(&export_node, &[]) {
Some(LayerNodeIdentifier::new(export_node, self.network_interface)) Some(LayerNodeIdentifier::new(export_node, self.network_interface, &[]))
} else { } else {
None None
} }

View File

@ -1,3 +1,4 @@
use super::utility_types::Direction;
use crate::messages::input_mapper::utility_types::input_keyboard::Key; use crate::messages::input_mapper::utility_types::input_keyboard::Key;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, OutputConnector}; use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, OutputConnector};
@ -18,7 +19,6 @@ pub enum NodeGraphMessage {
Init, Init,
SelectedNodesUpdated, SelectedNodesUpdated,
Copy, Copy,
CloseCreateNodeMenu,
CreateNodeFromContextMenu { CreateNodeFromContextMenu {
node_id: Option<NodeId>, node_id: Option<NodeId>,
node_type: String, node_type: String,
@ -32,10 +32,10 @@ pub enum NodeGraphMessage {
Cut, Cut,
DeleteNodes { DeleteNodes {
node_ids: Vec<NodeId>, node_ids: Vec<NodeId>,
reconnect: bool, delete_children: bool,
}, },
DeleteSelectedNodes { DeleteSelectedNodes {
reconnect: bool, delete_children: bool,
}, },
DisconnectInput { DisconnectInput {
input_connector: InputConnector, input_connector: InputConnector,
@ -111,15 +111,18 @@ pub enum NodeGraphMessage {
node_id: NodeId, node_id: NodeId,
alias: String, alias: String,
}, },
ShiftNodePosition {
node_id: NodeId,
x: i32,
y: i32,
},
SetToNodeOrLayer { SetToNodeOrLayer {
node_id: NodeId, node_id: NodeId,
is_layer: bool, is_layer: bool,
}, },
ShiftNodes { ShiftSelectedNodes {
node_ids: Vec<NodeId>, direction: Direction,
displacement_x: i32, rubber_band: bool,
displacement_y: i32,
move_upstream: bool,
}, },
TogglePreview { TogglePreview {
node_id: NodeId, node_id: NodeId,

View File

@ -4,7 +4,7 @@ use crate::application::generate_uuid;
use crate::messages::input_mapper::utility_types::macros::action_keys; use crate::messages::input_mapper::utility_types::macros::action_keys;
use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::node_graph::document_node_definitions::NodePropertiesContext; use crate::messages::portfolio::document::node_graph::document_node_definitions::NodePropertiesContext;
use crate::messages::portfolio::document::node_graph::utility_types::{ContextMenuData, FrontendGraphDataType}; use crate::messages::portfolio::document::node_graph::utility_types::{ContextMenuData, Direction, FrontendGraphDataType};
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{self, InputConnector, NodeNetworkInterface, NodeTemplate, OutputConnector, Previewing}; use crate::messages::portfolio::document::utility_types::network_interface::{self, InputConnector, NodeNetworkInterface, NodeTemplate, OutputConnector, Previewing};
use crate::messages::portfolio::document::utility_types::nodes::{CollapsedLayers, LayerPanelEntry}; use crate::messages::portfolio::document::utility_types::nodes::{CollapsedLayers, LayerPanelEntry};
@ -17,6 +17,7 @@ use graphene_core::*;
use renderer::{ClickTarget, Quad}; use renderer::{ClickTarget, Quad};
use glam::{DAffine2, DVec2, IVec2}; use glam::{DAffine2, DVec2, IVec2};
use std::cmp::Ordering;
#[derive(Debug)] #[derive(Debug)]
pub struct NodeGraphHandlerData<'a> { pub struct NodeGraphHandlerData<'a> {
@ -36,21 +37,23 @@ pub struct NodeGraphMessageHandler {
pub node_graph_errors: GraphErrors, pub node_graph_errors: GraphErrors,
has_selection: bool, has_selection: bool,
widgets: [LayoutGroup; 2], widgets: [LayoutGroup; 2],
drag_start: Option<DragStart>, pub drag_start: Option<DragStart>,
/// Used to add a transaction for the first node move when dragging. /// Used to add a transaction for the first node move when dragging.
begin_dragging: bool, begin_dragging: bool,
/// Stored in node graph coordinates /// Stored in node graph coordinates
box_selection_start: Option<DVec2>, box_selection_start: Option<DVec2>,
/// If the grip icon is held during a drag, then shift without pushing other nodes
shift_without_push: bool,
disconnecting: Option<InputConnector>, disconnecting: Option<InputConnector>,
initial_disconnecting: bool, initial_disconnecting: bool,
/// Node to select on pointer up if multiple nodes are selected and they were not dragged. /// Node to select on pointer up if multiple nodes are selected and they were not dragged.
select_if_not_dragged: Option<NodeId>, select_if_not_dragged: Option<NodeId>,
/// The start of the dragged line that cannot be moved, stored in node graph coordinates /// The start of the dragged line that cannot be moved, stored in node graph coordinates
wire_in_progress_from_connector: Option<DVec2>, pub wire_in_progress_from_connector: Option<DVec2>,
/// The end point of the dragged line that can be moved, stored in node graph coordinates /// The end point of the dragged line that can be moved, stored in node graph coordinates
wire_in_progress_to_connector: Option<DVec2>, pub wire_in_progress_to_connector: Option<DVec2>,
/// State for the context menu popups. /// State for the context menu popups.
context_menu: Option<ContextMenuInformation>, pub context_menu: Option<ContextMenuInformation>,
/// Index of selected node to be deselected on pointer up when shift clicking an already selected node /// Index of selected node to be deselected on pointer up when shift clicking an already selected node
pub deselect_on_pointer_up: Option<usize>, pub deselect_on_pointer_up: Option<usize>,
/// Adds the auto panning functionality to the node graph when dragging a node or selection box to the edge of the viewport. /// Adds the auto panning functionality to the node graph when dragging a node or selection box to the edge of the viewport.
@ -126,15 +129,6 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
responses.add(FrontendMessage::TriggerTextCopy { copy_text }); responses.add(FrontendMessage::TriggerTextCopy { copy_text });
} }
NodeGraphMessage::CloseCreateNodeMenu => {
self.context_menu = None;
responses.add(FrontendMessage::UpdateContextMenuInformation {
context_menu_information: self.context_menu.clone(),
});
self.wire_in_progress_from_connector = None;
self.wire_in_progress_to_connector = None;
responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: None });
}
NodeGraphMessage::CreateNodeFromContextMenu { node_id, node_type, x, y } => { NodeGraphMessage::CreateNodeFromContextMenu { node_id, node_type, x, y } => {
let node_id = node_id.unwrap_or_else(|| NodeId(generate_uuid())); let node_id = node_id.unwrap_or_else(|| NodeId(generate_uuid()));
@ -154,12 +148,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
node_id, node_id,
node_template: node_template.clone(), node_template: node_template.clone(),
}); });
responses.add(NodeGraphMessage::ShiftNodes { responses.add(NodeGraphMessage::ShiftNodePosition { node_id, x, y });
node_ids: vec![node_id],
displacement_x: x,
displacement_y: y,
move_upstream: false,
});
// Only auto connect to the dragged wire if the node is being added to the currently opened network // Only auto connect to the dragged wire if the node is being added to the currently opened network
if let Some(output_connector_position) = self.wire_in_progress_from_connector { if let Some(output_connector_position) = self.wire_in_progress_from_connector {
let Some(network_metadata) = network_interface.network_metadata(selection_network_path) else { let Some(network_metadata) = network_interface.network_metadata(selection_network_path) else {
@ -203,16 +192,16 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
} }
NodeGraphMessage::Cut => { NodeGraphMessage::Cut => {
responses.add(NodeGraphMessage::Copy); responses.add(NodeGraphMessage::Copy);
responses.add(NodeGraphMessage::DeleteSelectedNodes { reconnect: true }); responses.add(NodeGraphMessage::DeleteSelectedNodes { delete_children: true });
} }
NodeGraphMessage::DeleteNodes { node_ids, reconnect } => { NodeGraphMessage::DeleteNodes { node_ids, delete_children } => {
network_interface.delete_nodes(node_ids, reconnect, selection_network_path); network_interface.delete_nodes(node_ids, delete_children, selection_network_path);
responses.add(NodeGraphMessage::SelectedNodesUpdated); responses.add(NodeGraphMessage::SelectedNodesUpdated);
responses.add(NodeGraphMessage::SendGraph); responses.add(NodeGraphMessage::SendGraph);
} }
// Deletes selected_nodes. If `reconnect` is true, then all children nodes (secondary input) of the selected nodes are deleted and the siblings (primary input/output) are reconnected. // Deletes selected_nodes. If `reconnect` is true, then all children nodes (secondary input) of the selected nodes are deleted and the siblings (primary input/output) are reconnected.
// If `reconnect` is false, then only the selected nodes are deleted and not reconnected. // If `reconnect` is false, then only the selected nodes are deleted and not reconnected.
NodeGraphMessage::DeleteSelectedNodes { reconnect } => { NodeGraphMessage::DeleteSelectedNodes { delete_children } => {
let Some(selected_nodes) = network_interface.selected_nodes(selection_network_path) else { let Some(selected_nodes) = network_interface.selected_nodes(selection_network_path) else {
log::error!("Could not get selected nodes in DeleteSelectedNodes"); log::error!("Could not get selected nodes in DeleteSelectedNodes");
return; return;
@ -220,7 +209,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
responses.add(NodeGraphMessage::DeleteNodes { responses.add(NodeGraphMessage::DeleteNodes {
node_ids: selected_nodes.selected_nodes().cloned().collect::<Vec<_>>(), node_ids: selected_nodes.selected_nodes().cloned().collect::<Vec<_>>(),
reconnect, delete_children,
}) })
} }
NodeGraphMessage::DisconnectInput { input_connector } => { NodeGraphMessage::DisconnectInput { input_connector } => {
@ -232,15 +221,17 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
NodeGraphMessage::DuplicateSelectedNodes => { NodeGraphMessage::DuplicateSelectedNodes => {
let all_selected_nodes = network_interface.upstream_chain_nodes(selection_network_path); let all_selected_nodes = network_interface.upstream_chain_nodes(selection_network_path);
responses.add(DocumentMessage::StartTransaction);
let copy_ids = all_selected_nodes.iter().enumerate().map(|(new, id)| (*id, NodeId(new as u64))).collect::<HashMap<NodeId, NodeId>>(); let copy_ids = all_selected_nodes.iter().enumerate().map(|(new, id)| (*id, NodeId(new as u64))).collect::<HashMap<NodeId, NodeId>>();
// Copy the selected nodes // Copy the selected nodes
let nodes = network_interface.copy_nodes(&copy_ids, selection_network_path).collect::<Vec<_>>(); let nodes = network_interface.copy_nodes(&copy_ids, selection_network_path).collect::<Vec<_>>();
let new_ids = nodes.iter().map(|(id, _)| (*id, NodeId(generate_uuid()))).collect::<HashMap<_, _>>(); let new_ids = nodes.iter().map(|(id, _)| (*id, NodeId(generate_uuid()))).collect::<HashMap<_, _>>();
responses.add(NodeGraphMessage::AddNodes { nodes, new_ids }); responses.add(DocumentMessage::StartTransaction);
responses.add(NodeGraphMessage::AddNodes { nodes, new_ids: new_ids.clone() });
responses.add(NodeGraphMessage::SelectedNodesSet {
nodes: new_ids.values().cloned().collect(),
});
} }
NodeGraphMessage::EnterNestedNetwork => { NodeGraphMessage::EnterNestedNetwork => {
let Some(node_id) = network_interface.node_from_click(ipp.mouse.position, selection_network_path) else { let Some(node_id) = network_interface.node_from_click(ipp.mouse.position, selection_network_path) else {
@ -348,6 +339,10 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
return; return;
} }
if network_interface.grip_from_click(click, selection_network_path).is_some() {
self.shift_without_push = true;
}
let clicked_id = network_interface.node_from_click(click, selection_network_path); let clicked_id = network_interface.node_from_click(click, selection_network_path);
let clicked_input = network_interface.input_connector_from_click(click, selection_network_path); let clicked_input = network_interface.input_connector_from_click(click, selection_network_path);
let clicked_output = network_interface.output_connector_from_click(click, selection_network_path); let clicked_output = network_interface.output_connector_from_click(click, selection_network_path);
@ -355,6 +350,12 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
// Create the add node popup on right click, then exit // Create the add node popup on right click, then exit
if right_click { if right_click {
if self.drag_start.is_some() {
responses.add(DocumentMessage::AbortTransaction);
self.drag_start = None;
return;
}
let context_menu_data = if let Some(node_id) = clicked_id { let context_menu_data = if let Some(node_id) = clicked_id {
ContextMenuData::ToggleLayer { ContextMenuData::ToggleLayer {
node_id, node_id,
@ -613,24 +614,57 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: Some(wire_path) }); responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: Some(wire_path) });
} }
} else if let Some(drag_start) = &mut self.drag_start { } else if let Some(drag_start) = &mut self.drag_start {
let Some(selected_nodes) = network_interface.selected_nodes(selection_network_path) else {
log::error!("Could not get selected nodes in PointerMove");
return;
};
if self.begin_dragging { if self.begin_dragging {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
self.begin_dragging = false; self.begin_dragging = false;
} }
let graph_delta = IVec2::new(((point.x - drag_start.start_x) / 24.).round() as i32, ((point.y - drag_start.start_y) / 24.).round() as i32);
if drag_start.round_x != graph_delta.x || drag_start.round_y != graph_delta.y { let mut graph_delta = IVec2::new(((point.x - drag_start.start_x) / 24.).round() as i32, ((point.y - drag_start.start_y) / 24.).round() as i32);
responses.add(NodeGraphMessage::ShiftNodes { let previous_round_x = drag_start.round_x;
node_ids: selected_nodes.selected_nodes().cloned().collect(), let previous_round_y = drag_start.round_y;
displacement_x: graph_delta.x - drag_start.round_x,
displacement_y: graph_delta.y - drag_start.round_y, drag_start.round_x = graph_delta.x;
move_upstream: ipp.keyboard.get(shift as usize), drag_start.round_y = graph_delta.y;
});
drag_start.round_x = graph_delta.x; graph_delta.x -= previous_round_x;
drag_start.round_y = graph_delta.y; graph_delta.y -= previous_round_y;
while graph_delta != IVec2::ZERO {
match graph_delta.x.cmp(&0) {
Ordering::Greater => {
responses.add(NodeGraphMessage::ShiftSelectedNodes {
direction: Direction::Right,
rubber_band: true,
});
graph_delta.x -= 1;
}
Ordering::Less => {
responses.add(NodeGraphMessage::ShiftSelectedNodes {
direction: Direction::Left,
rubber_band: true,
});
graph_delta.x += 1;
}
Ordering::Equal => {}
}
match graph_delta.y.cmp(&0) {
Ordering::Greater => {
responses.add(NodeGraphMessage::ShiftSelectedNodes {
direction: Direction::Down,
rubber_band: true,
});
graph_delta.y -= 1;
}
Ordering::Less => {
responses.add(NodeGraphMessage::ShiftSelectedNodes {
direction: Direction::Up,
rubber_band: true,
});
graph_delta.y += 1;
}
Ordering::Equal => {}
}
} }
} else if self.box_selection_start.is_some() { } else if self.box_selection_start.is_some() {
responses.add(NodeGraphMessage::UpdateBoxSelection); responses.add(NodeGraphMessage::UpdateBoxSelection);
@ -649,6 +683,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
warn!("No network_metadata"); warn!("No network_metadata");
return; return;
}; };
if let Some(node_to_deselect) = self.deselect_on_pointer_up { if let Some(node_to_deselect) = self.deselect_on_pointer_up {
let mut new_selected_nodes = selected_nodes.selected_nodes_ref().clone(); let mut new_selected_nodes = selected_nodes.selected_nodes_ref().clone();
new_selected_nodes.remove(node_to_deselect); new_selected_nodes.remove(node_to_deselect);
@ -703,7 +738,13 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
}); });
return; return;
} }
} else if let Some(drag_start) = &self.drag_start { }
// End of dragging a node
else if let Some(drag_start) = &self.drag_start {
self.shift_without_push = false;
// Reset all offsets to end the rubber banding while dragging
network_interface.unload_stack_dependents_y_offset(selection_network_path);
let Some(selected_nodes) = network_interface.selected_nodes(selection_network_path) else { return };
// Only select clicked node if multiple are selected and they were not dragged // Only select clicked node if multiple are selected and they were not dragged
if let Some(select_if_not_dragged) = self.select_if_not_dragged { if let Some(select_if_not_dragged) = self.select_if_not_dragged {
if drag_start.start_x == point.x if drag_start.start_x == point.x
@ -718,6 +759,20 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
} }
} }
// Try expand the upstream chain for all layers if there is an eligible node
let Some(network) = network_interface.network(selection_network_path) else { return };
for layer in network
.nodes
.keys()
.filter(|node_id| network_interface.is_layer(node_id, selection_network_path))
.cloned()
.collect::<Vec<_>>()
{
network_interface.try_set_upstream_to_chain(&InputConnector::node(layer, 1), selection_network_path);
}
responses.add(NodeGraphMessage::SendGraph);
let Some(selected_nodes) = network_interface.selected_nodes(selection_network_path) else { return };
// Check if a single node was dragged onto a wire and that the node was dragged onto the wire // Check if a single node was dragged onto a wire and that the node was dragged onto the wire
if selected_nodes.selected_nodes_ref().len() == 1 && !self.begin_dragging { if selected_nodes.selected_nodes_ref().len() == 1 && !self.begin_dragging {
let selected_node_id = selected_nodes.selected_nodes_ref()[0]; let selected_node_id = selected_nodes.selected_nodes_ref()[0];
@ -732,7 +787,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
let primary_input_is_value = selected_node.inputs.first().is_some_and(|first_input| first_input.as_value().is_some()); let primary_input_is_value = selected_node.inputs.first().is_some_and(|first_input| first_input.as_value().is_some());
// Check that neither the primary input or output of the selected node are already connected. // Check that neither the primary input or output of the selected node are already connected.
if !has_primary_output_connection && primary_input_is_value { if !has_primary_output_connection && primary_input_is_value {
let Some(bounding_box) = network_interface.node_bounding_box(selected_node_id, selection_network_path) else { let Some(bounding_box) = network_interface.node_bounding_box(&selected_node_id, selection_network_path) else {
log::error!("Could not get bounding box for node: {selected_node_id}"); log::error!("Could not get bounding box for node: {selected_node_id}");
return; return;
}; };
@ -857,8 +912,9 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
} }
} }
} }
self.select_if_not_dragged = None self.select_if_not_dragged = None;
} }
self.drag_start = None; self.drag_start = None;
self.begin_dragging = false; self.begin_dragging = false;
self.box_selection_start = None; self.box_selection_start = None;
@ -991,13 +1047,12 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
NodeGraphMessage::SetInput { input_connector, input } => { NodeGraphMessage::SetInput { input_connector, input } => {
network_interface.set_input(&input_connector, input, selection_network_path); network_interface.set_input(&input_connector, input, selection_network_path);
} }
NodeGraphMessage::ShiftNodes { NodeGraphMessage::ShiftSelectedNodes { direction, rubber_band } => {
node_ids, network_interface.shift_selected_nodes(direction, self.shift_without_push, selection_network_path);
displacement_x,
displacement_y, if !rubber_band {
move_upstream, network_interface.unload_stack_dependents_y_offset(selection_network_path);
} => { }
network_interface.shift_selected_nodes(node_ids, displacement_x, displacement_y, move_upstream, selection_network_path);
if graph_view_overlay_open { if graph_view_overlay_open {
responses.add(NodeGraphMessage::SendGraph); responses.add(NodeGraphMessage::SendGraph);
@ -1022,6 +1077,9 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::RunDocumentGraph);
} }
} }
NodeGraphMessage::ShiftNodePosition { node_id, x, y } => {
network_interface.shift_absolute_node_position(&node_id, IVec2::new(x, y), selection_network_path);
}
NodeGraphMessage::SetToNodeOrLayer { node_id, is_layer } => { NodeGraphMessage::SetToNodeOrLayer { node_id, is_layer } => {
if is_layer && !network_interface.is_eligible_to_be_layer(&node_id, selection_network_path) { if is_layer && !network_interface.is_eligible_to_be_layer(&node_id, selection_network_path) {
return; return;
@ -1253,16 +1311,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
} }
fn actions(&self) -> ActionList { fn actions(&self) -> ActionList {
let mut common = vec![]; let common = vec![];
if self
.context_menu
.as_ref()
.is_some_and(|context_menu| matches!(context_menu.context_menu_data, ContextMenuData::CreateNode))
{
common.extend(actions!(NodeGraphMessageDiscriminant; CloseCreateNodeMenu));
}
common common
} }
} }
@ -1282,6 +1331,7 @@ impl NodeGraphMessageHandler {
ToggleSelectedLocked, ToggleSelectedLocked,
ToggleSelectedVisibility, ToggleSelectedVisibility,
PrintSelectedNodeCoordinates, PrintSelectedNodeCoordinates,
ShiftSelectedNodes,
)); ));
} }
@ -1732,7 +1782,7 @@ impl NodeGraphMessageHandler {
let mut selected_parents = HashSet::new(); let mut selected_parents = HashSet::new();
for selected_layer in &selected_layers { for selected_layer in &selected_layers {
for ancestor in LayerNodeIdentifier::new(*selected_layer, network_interface).ancestors(network_interface.document_metadata()) { for ancestor in LayerNodeIdentifier::new(*selected_layer, network_interface, &[]).ancestors(network_interface.document_metadata()) {
if ancestor != LayerNodeIdentifier::ROOT_PARENT && !selected_layers.contains(&ancestor.to_node()) { if ancestor != LayerNodeIdentifier::ROOT_PARENT && !selected_layers.contains(&ancestor.to_node()) {
selected_parents.insert(ancestor.to_node()); selected_parents.insert(ancestor.to_node());
} }
@ -1741,7 +1791,7 @@ impl NodeGraphMessageHandler {
for (&node_id, node_metadata) in &network_interface.network_metadata(&[]).unwrap().persistent_metadata.node_metadata { for (&node_id, node_metadata) in &network_interface.network_metadata(&[]).unwrap().persistent_metadata.node_metadata {
if node_metadata.persistent_metadata.is_layer() { if node_metadata.persistent_metadata.is_layer() {
let layer = LayerNodeIdentifier::new(node_id, network_interface); let layer = LayerNodeIdentifier::new(node_id, network_interface, &[]);
let children_allowed = let children_allowed =
// The layer has other layers as children along the secondary input's horizontal flow // The layer has other layers as children along the secondary input's horizontal flow
@ -1883,6 +1933,7 @@ impl Default for NodeGraphMessageHandler {
widgets: [LayoutGroup::Row { widgets: Vec::new() }, LayoutGroup::Row { widgets: right_side_widgets }], widgets: [LayoutGroup::Row { widgets: Vec::new() }, LayoutGroup::Row { widgets: right_side_widgets }],
drag_start: None, drag_start: None,
begin_dragging: false, begin_dragging: false,
shift_without_push: false,
box_selection_start: None, box_selection_start: None,
disconnecting: None, disconnecting: None,
initial_disconnecting: false, initial_disconnecting: false,

View File

@ -174,10 +174,18 @@ pub struct FrontendClickTargets {
pub layer_click_targets: Vec<String>, pub layer_click_targets: Vec<String>,
#[serde(rename = "portClickTargets")] #[serde(rename = "portClickTargets")]
pub port_click_targets: Vec<String>, pub port_click_targets: Vec<String>,
#[serde(rename = "visibilityClickTargets")] #[serde(rename = "iconClickTargets")]
pub visibility_click_targets: Vec<String>, pub icon_click_targets: Vec<String>,
#[serde(rename = "allNodesBoundingBox")] #[serde(rename = "allNodesBoundingBox")]
pub all_nodes_bounding_box: String, pub all_nodes_bounding_box: String,
#[serde(rename = "importExportsBoundingBox")] #[serde(rename = "importExportsBoundingBox")]
pub import_exports_bounding_box: String, pub import_exports_bounding_box: String,
} }
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum Direction {
Up,
Down,
Left,
Right,
}

View File

@ -226,13 +226,13 @@ impl LayerNodeIdentifier {
Self(unsafe { NonZeroU64::new_unchecked(node_id.0 + 1) }) Self(unsafe { NonZeroU64::new_unchecked(node_id.0 + 1) })
} }
/// Construct a [`LayerNodeIdentifier`], debug asserting that it is a layer node in the document network /// Construct a [`LayerNodeIdentifier`], debug asserting that it is a layer node. This should only be used in the document network since the structure is not loaded in nested networks.
#[track_caller] #[track_caller]
pub fn new(node_id: NodeId, network_interface: &NodeNetworkInterface) -> Self { pub fn new(node_id: NodeId, network_interface: &NodeNetworkInterface, network_path: &[NodeId]) -> Self {
debug_assert!( debug_assert!(
network_interface.is_layer(&node_id, &Vec::new()), network_interface.is_layer(&node_id, network_path),
"Layer identifier constructed from non-layer node {node_id}: {:#?}", "Layer identifier constructed from non-layer node {node_id}: {:#?}",
network_interface.network(&[]).unwrap().nodes.get(&node_id) network_interface.network(network_path).unwrap().nodes.get(&node_id)
); );
Self::new_unchecked(node_id) Self::new_unchecked(node_id)
} }

View File

@ -552,7 +552,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
continue; continue;
}; };
// If the downstream node is a layer and the input is the first input and the current layer is not in a stack // If the downstream node is a layer and the input is the first input and the current layer is not in a stack
if input_index == 0 && document.network_interface.is_layer(&downstream_node, &[]) && document.network_interface.is_absolute(&layer.to_node(), &[]) { if input_index == 0 && document.network_interface.is_layer(&downstream_node, &[]) && !document.network_interface.is_stack(&layer.to_node(), &[]) {
// Ensure the layer is horizontally aligned with the downstream layer to prevent changing the layout of old files // Ensure the layer is horizontally aligned with the downstream layer to prevent changing the layout of old files
let (Some(layer_position), Some(downstream_position)) = let (Some(layer_position), Some(downstream_position)) =
(document.network_interface.position(&layer.to_node(), &[]), document.network_interface.position(&downstream_node, &[])) (document.network_interface.position(&layer.to_node(), &[]), document.network_interface.position(&downstream_node, &[]))

View File

@ -1,9 +1,6 @@
use super::tool_prelude::*; use super::tool_prelude::*;
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, NodeId};
#[derive(Default)] #[derive(Default)]
pub struct ImaginateTool { pub struct ImaginateTool {
fsm_state: ImaginateToolFsmState, fsm_state: ImaginateToolFsmState,
@ -101,7 +98,7 @@ impl Fsm for ImaginateToolFsmState {
(ImaginateToolFsmState::Ready, ImaginateToolMessage::DragStart) => { (ImaginateToolFsmState::Ready, ImaginateToolMessage::DragStart) => {
shape_data.start(document, input); shape_data.start(document, input);
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
shape_data.layer = Some(LayerNodeIdentifier::new(NodeId(generate_uuid()), &document.network_interface)); //shape_data.layer = Some(LayerNodeIdentifier::new(NodeId(generate_uuid()), &document.network_interface));
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

View File

@ -367,7 +367,7 @@ impl SelectToolData {
for layer in document.network_interface.shallowest_unique_layers(&[]) { for layer in document.network_interface.shallowest_unique_layers(&[]) {
responses.add(NodeGraphMessage::DeleteNodes { responses.add(NodeGraphMessage::DeleteNodes {
node_ids: vec![layer.to_node()], node_ids: vec![layer.to_node()],
reconnect: true, delete_children: true,
}); });
} }
@ -1203,6 +1203,7 @@ fn drag_deepest_manipulation(responses: &mut VecDeque<Message>, selected: Vec<La
tool_data.layers_dragging.append(&mut vec![document.find_deepest(&selected).unwrap_or(LayerNodeIdentifier::new( tool_data.layers_dragging.append(&mut vec![document.find_deepest(&selected).unwrap_or(LayerNodeIdentifier::new(
document.network_interface.root_node(&[]).expect("Root node should exist when dragging layers").node_id, document.network_interface.root_node(&[]).expect("Root node should exist when dragging layers").node_id,
&document.network_interface, &document.network_interface,
&[],
))]); ))]);
responses.add(NodeGraphMessage::SelectedNodesSet { responses.add(NodeGraphMessage::SelectedNodesSet {
nodes: tool_data nodes: tool_data

View File

@ -162,15 +162,40 @@
transparent calc(6px * sqrt(2) / 2) transparent calc(6px * sqrt(2) / 2)
); );
// Arrow triangle (#eee fill) // Array of 2x3 dots (fill: --color-e-nearwhite)
--icon-drag-grip: url('data:image/svg+xml;utf8,\
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 24" fill="%23eee">\
<circle cx="0.5" cy="1.5" r="0.5" /><circle cx="3.5" cy="1.5" r="0.5" />\
<circle cx="0.5" cy="4.5" r="0.5" /><circle cx="3.5" cy="4.5" r="0.5" />\
<circle cx="0.5" cy="7.5" r="0.5" /><circle cx="3.5" cy="7.5" r="0.5" />\
</svg>\
');
// Array of 2x3 dots (fill: --color-f-white)
--icon-drag-grip-hover: url('data:image/svg+xml;utf8,\
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 24" fill="%23fff">\
<circle cx="0.5" cy="1.5" r="0.5" /><circle cx="3.5" cy="1.5" r="0.5" />\
<circle cx="0.5" cy="4.5" r="0.5" /><circle cx="3.5" cy="4.5" r="0.5" />\
<circle cx="0.5" cy="7.5" r="0.5" /><circle cx="3.5" cy="7.5" r="0.5" />\
</svg>\
');
// Array of 2x3 dots (fill: --color-8-uppergray)
--icon-drag-grip-disabled: url('data:image/svg+xml;utf8,\
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 24" fill="%23888">\
<circle cx="0.5" cy="1.5" r="0.5" /><circle cx="3.5" cy="1.5" r="0.5" />\
<circle cx="0.5" cy="4.5" r="0.5" /><circle cx="3.5" cy="4.5" r="0.5" />\
<circle cx="0.5" cy="7.5" r="0.5" /><circle cx="3.5" cy="7.5" r="0.5" />\
</svg>\
');
// Arrow triangle (fill: --color-e-nearwhite)
--icon-expand-collapse-arrow: url('data:image/svg+xml;utf8,\ --icon-expand-collapse-arrow: url('data:image/svg+xml;utf8,\
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8"><polygon fill="%23eee" points="3,0 1,0 5,4 1,8 3,8 7,4" /></svg>\ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8"><polygon fill="%23eee" points="3,0 1,0 5,4 1,8 3,8 7,4" /></svg>\
'); ');
// Arrow triangle (#fff fill) // Arrow triangle (fill: --color-f-white)
--icon-expand-collapse-arrow-hover: url('data:image/svg+xml;utf8,\ --icon-expand-collapse-arrow-hover: url('data:image/svg+xml;utf8,\
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8"><polygon fill="%23fff" points="3,0 1,0 5,4 1,8 3,8 7,4" /></svg>\ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8"><polygon fill="%23fff" points="3,0 1,0 5,4 1,8 3,8 7,4" /></svg>\
'); ');
// Arrow triangle (#888 fill) // Arrow triangle (fill: --color-8-uppergray)
--icon-expand-collapse-arrow-disabled: url('data:image/svg+xml;utf8,\ --icon-expand-collapse-arrow-disabled: url('data:image/svg+xml;utf8,\
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8"><polygon fill="%23888" points="3,0 1,0 5,4 1,8 3,8 7,4" /></svg>\ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8"><polygon fill="%23888" points="3,0 1,0 5,4 1,8 3,8 7,4" /></svg>\
'); ');

View File

@ -456,7 +456,7 @@
{#each $nodeGraph.clickTargets.portClickTargets as pathString} {#each $nodeGraph.clickTargets.portClickTargets as pathString}
<path class="port" d={pathString} /> <path class="port" d={pathString} />
{/each} {/each}
{#each $nodeGraph.clickTargets.visibilityClickTargets as pathString} {#each $nodeGraph.clickTargets.iconClickTargets as pathString}
<path class="visibility" d={pathString} /> <path class="visibility" d={pathString} />
{/each} {/each}
<path class="all-nodes-bounding-box" d={$nodeGraph.clickTargets.allNodesBoundingBox} /> <path class="all-nodes-bounding-box" d={$nodeGraph.clickTargets.allNodesBoundingBox} />
@ -642,6 +642,7 @@
{node.displayName} {node.displayName}
</span> </span>
</div> </div>
<div class="solo-drag-grip" title="Drag only this layer without pushing others outside the stack"></div>
<IconButton <IconButton
class={"visibility"} class={"visibility"}
data-visibility-button data-visibility-button
@ -1238,6 +1239,23 @@
} }
} }
.solo-drag-grip {
width: 8px;
height: 24px;
background-position: 2px 8px;
right: calc(-12px + 24px);
border-radius: 2px;
}
.solo-drag-grip:hover,
&.selected .solo-drag-grip {
background-image: var(--icon-drag-grip);
&:hover {
background-image: var(--icon-drag-grip-hover);
}
}
.visibility { .visibility {
position: absolute; position: absolute;
right: -12px; right: -12px;
@ -1247,6 +1265,7 @@
left: calc(-3px + var(--node-chain-area-left-extension) * 24px - 36px); left: calc(-3px + var(--node-chain-area-left-extension) * 24px - 36px);
} }
.solo-drag-grip,
.visibility, .visibility,
.input.ports, .input.ports,
.input.ports .port { .input.ports .port {

View File

@ -173,7 +173,7 @@ export type FrontendClickTargets = {
readonly nodeClickTargets: string[]; readonly nodeClickTargets: string[];
readonly layerClickTargets: string[]; readonly layerClickTargets: string[];
readonly portClickTargets: string[]; readonly portClickTargets: string[];
readonly visibilityClickTargets: string[]; readonly iconClickTargets: string[];
readonly allNodesBoundingBox: string; readonly allNodesBoundingBox: string;
readonly importExportsBoundingBox: string; readonly importExportsBoundingBox: string;
}; };

View File

@ -622,7 +622,10 @@ impl EditorHandle {
self.dispatch(message); self.dispatch(message);
let id = NodeId(id); let id = NodeId(id);
let message = NodeGraphMessage::DeleteNodes { node_ids: vec![id], reconnect: true }; let message = NodeGraphMessage::DeleteNodes {
node_ids: vec![id],
delete_children: true,
};
self.dispatch(message); self.dispatch(message);
} }
@ -779,7 +782,7 @@ impl EditorHandle {
.map(|(id, _)| *id) .map(|(id, _)| *id)
.collect::<Vec<_>>() .collect::<Vec<_>>()
{ {
let layer = LayerNodeIdentifier::new(node, &document.network_interface); let layer = LayerNodeIdentifier::new(node, &document.network_interface, &[]);
if layer.has_children(document.metadata()) { if layer.has_children(document.metadata()) {
continue; continue;
} }