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
pub const GRID_SIZE: u32 = 24;
pub const EXPORTS_TO_EDGE_PIXEL_GAP: u32 = 120;
pub const IMPORTS_TO_EDGE_PIXEL_GAP: u32 = 120;
pub const EXPORTS_TO_TOP_EDGE_PIXEL_GAP: u32 = 72;
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
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()],
});
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.handle_message(PortfolioMessage::PasteIntoFolder {
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::misc::MappingEntry;
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::prelude::*;
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!(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(Backspace); modifiers=[Accel], action_dispatch=NodeGraphMessage::DeleteSelectedNodes { reconnect: false }),
entry!(KeyDown(Delete); action_dispatch=NodeGraphMessage::DeleteSelectedNodes { reconnect: true }),
entry!(KeyDown(Backspace); action_dispatch=NodeGraphMessage::DeleteSelectedNodes { reconnect: true }),
entry!(KeyDown(Delete); modifiers=[Accel], action_dispatch=NodeGraphMessage::DeleteSelectedNodes { delete_children: false }),
entry!(KeyDown(Backspace); modifiers=[Accel], action_dispatch=NodeGraphMessage::DeleteSelectedNodes { delete_children: false }),
entry!(KeyDown(Delete); action_dispatch=NodeGraphMessage::DeleteSelectedNodes { delete_children: true }),
entry!(KeyDown(Backspace); action_dispatch=NodeGraphMessage::DeleteSelectedNodes { delete_children: true }),
entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=NodeGraphMessage::Cut),
entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=NodeGraphMessage::Copy),
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=[Alt], action_dispatch=NodeGraphMessage::SendClickTargets),
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
entry!(KeyDown(Enter); action_dispatch=TransformLayerMessage::ApplyTransformOperation),
@ -301,7 +305,7 @@ pub fn input_mappings() -> Mapping {
//
// DocumentMessage
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(Backspace); action_dispatch=DocumentMessage::DeleteSelectedLayers),
entry!(KeyDown(KeyP); modifiers=[Alt], action_dispatch=DocumentMessage::DebugPrintDocument),

View File

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

View File

@ -366,7 +366,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
info!("{:#?}", self.network_interface);
}
DocumentMessage::DeleteSelectedLayers => {
responses.add(NodeGraphMessage::DeleteSelectedNodes { reconnect: true });
responses.add(NodeGraphMessage::DeleteSelectedNodes { delete_children: true });
}
DocumentMessage::DeselectAllLayers => {
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] });
@ -400,6 +400,26 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
responses.add(NodeGraphMessage::SendGraph);
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 } => {
for _ in 0..steps_back {
self.breadcrumb_network_path.pop();
@ -434,10 +454,12 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
responses.add(FrontendMessage::TriggerGraphViewOverlay { open });
// Update the tilt menu bar buttons to be disabled when the graph is open
responses.add(MenuBarMessage::SendLayout);
responses.add(DocumentMessage::RenderRulers);
responses.add(DocumentMessage::RenderScrollbars);
if open {
responses.add(NavigationMessage::CanvasTiltSet { angle_radians: 0. });
responses.add(NodeGraphMessage::SetGridAlignedEdges);
responses.add(NodeGraphMessage::SendGraph);
responses.add(NavigationMessage::CanvasTiltSet { angle_radians: 0. });
}
}
DocumentMessage::GraphViewOverlayToggle => {
@ -639,16 +661,6 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
resize_opposite_corner,
} => {
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 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);
}
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![];
@ -965,7 +977,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
}
DocumentMessage::StartTransaction => self.backup(responses),
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) {
self.collapsed.0.retain(|&collapsed_layer| collapsed_layer != layer);
} else {
@ -1040,7 +1052,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
// Delete empty group folder
responses.add(NodeGraphMessage::DeleteNodes {
node_ids: vec![layer.to_node()],
reconnect: true,
delete_children: true,
});
}
DocumentMessage::PTZUpdate => {
@ -1123,23 +1135,26 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
// Additional actions if there are any selected layers
if self.network_interface.selected_nodes(&[]).unwrap().selected_layers(self.metadata()).next().is_some() {
let select = actions!(DocumentMessageDiscriminant;
let mut select = actions!(DocumentMessageDiscriminant;
DeleteSelectedLayers,
DuplicateSelectedLayers,
GroupSelectedLayers,
NudgeSelectedLayers,
SelectedLayersLower,
SelectedLayersLowerToBack,
SelectedLayersRaise,
SelectedLayersRaiseToFront,
UngroupSelectedLayers,
);
if !self.graph_view_overlay_open {
select.extend(actions!(DocumentMessageDiscriminant; NudgeSelectedLayers));
}
common.extend(select);
}
// Additional actions if the node graph is open
if self.graph_view_overlay_open {
common.extend(actions!(DocumentMessageDiscriminant;
GraphViewOverlay,
Escape
));
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();
if let NodeInput::Node { node_id, .. } = &primary_input {
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 {
network_interface.disconnect_input(&InputConnector::node(artboard_layer.to_node(), 0), &[]);
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() {
responses.add(NodeGraphMessage::DeleteNodes {
node_ids: vec![artboard.to_node()],
reconnect: false,
delete_children: false,
});
}
// 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 {
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, &[]);
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
@ -130,7 +130,7 @@ impl<'a> ModifyInputsContext<'a> {
Some(NodeInput::value(TaggedValue::Bool(artboard.clip), false)),
]);
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) {
@ -222,7 +222,7 @@ impl<'a> ModifyInputsContext<'a> {
};
let export_node = network.exports.first().and_then(|export| export.as_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 {
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::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, OutputConnector};
@ -18,7 +19,6 @@ pub enum NodeGraphMessage {
Init,
SelectedNodesUpdated,
Copy,
CloseCreateNodeMenu,
CreateNodeFromContextMenu {
node_id: Option<NodeId>,
node_type: String,
@ -32,10 +32,10 @@ pub enum NodeGraphMessage {
Cut,
DeleteNodes {
node_ids: Vec<NodeId>,
reconnect: bool,
delete_children: bool,
},
DeleteSelectedNodes {
reconnect: bool,
delete_children: bool,
},
DisconnectInput {
input_connector: InputConnector,
@ -111,15 +111,18 @@ pub enum NodeGraphMessage {
node_id: NodeId,
alias: String,
},
ShiftNodePosition {
node_id: NodeId,
x: i32,
y: i32,
},
SetToNodeOrLayer {
node_id: NodeId,
is_layer: bool,
},
ShiftNodes {
node_ids: Vec<NodeId>,
displacement_x: i32,
displacement_y: i32,
move_upstream: bool,
ShiftSelectedNodes {
direction: Direction,
rubber_band: bool,
},
TogglePreview {
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::layout::utility_types::widget_prelude::*;
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::network_interface::{self, InputConnector, NodeNetworkInterface, NodeTemplate, OutputConnector, Previewing};
use crate::messages::portfolio::document::utility_types::nodes::{CollapsedLayers, LayerPanelEntry};
@ -17,6 +17,7 @@ use graphene_core::*;
use renderer::{ClickTarget, Quad};
use glam::{DAffine2, DVec2, IVec2};
use std::cmp::Ordering;
#[derive(Debug)]
pub struct NodeGraphHandlerData<'a> {
@ -36,21 +37,23 @@ pub struct NodeGraphMessageHandler {
pub node_graph_errors: GraphErrors,
has_selection: bool,
widgets: [LayoutGroup; 2],
drag_start: Option<DragStart>,
pub drag_start: Option<DragStart>,
/// Used to add a transaction for the first node move when dragging.
begin_dragging: bool,
/// Stored in node graph coordinates
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>,
initial_disconnecting: bool,
/// Node to select on pointer up if multiple nodes are selected and they were not dragged.
select_if_not_dragged: Option<NodeId>,
/// 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
wire_in_progress_to_connector: Option<DVec2>,
pub wire_in_progress_to_connector: Option<DVec2>,
/// 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
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.
@ -126,15 +129,6 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
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 } => {
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_template: node_template.clone(),
});
responses.add(NodeGraphMessage::ShiftNodes {
node_ids: vec![node_id],
displacement_x: x,
displacement_y: y,
move_upstream: false,
});
responses.add(NodeGraphMessage::ShiftNodePosition { node_id, x, y });
// 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 {
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 => {
responses.add(NodeGraphMessage::Copy);
responses.add(NodeGraphMessage::DeleteSelectedNodes { reconnect: true });
responses.add(NodeGraphMessage::DeleteSelectedNodes { delete_children: true });
}
NodeGraphMessage::DeleteNodes { node_ids, reconnect } => {
network_interface.delete_nodes(node_ids, reconnect, selection_network_path);
NodeGraphMessage::DeleteNodes { node_ids, delete_children } => {
network_interface.delete_nodes(node_ids, delete_children, selection_network_path);
responses.add(NodeGraphMessage::SelectedNodesUpdated);
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.
// 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 {
log::error!("Could not get selected nodes in DeleteSelectedNodes");
return;
@ -220,7 +209,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
responses.add(DocumentMessage::StartTransaction);
responses.add(NodeGraphMessage::DeleteNodes {
node_ids: selected_nodes.selected_nodes().cloned().collect::<Vec<_>>(),
reconnect,
delete_children,
})
}
NodeGraphMessage::DisconnectInput { input_connector } => {
@ -232,15 +221,17 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
NodeGraphMessage::DuplicateSelectedNodes => {
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>>();
// Copy the selected nodes
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<_, _>>();
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 => {
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;
}
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_input = network_interface.input_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
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 {
ContextMenuData::ToggleLayer {
node_id,
@ -613,24 +614,57 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: Some(wire_path) });
}
} 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 {
responses.add(DocumentMessage::StartTransaction);
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 {
responses.add(NodeGraphMessage::ShiftNodes {
node_ids: selected_nodes.selected_nodes().cloned().collect(),
displacement_x: graph_delta.x - drag_start.round_x,
displacement_y: graph_delta.y - drag_start.round_y,
move_upstream: ipp.keyboard.get(shift as usize),
});
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);
let previous_round_x = drag_start.round_x;
let previous_round_y = drag_start.round_y;
drag_start.round_x = graph_delta.x;
drag_start.round_y = graph_delta.y;
graph_delta.x -= previous_round_x;
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() {
responses.add(NodeGraphMessage::UpdateBoxSelection);
@ -649,6 +683,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
warn!("No network_metadata");
return;
};
if let Some(node_to_deselect) = self.deselect_on_pointer_up {
let mut new_selected_nodes = selected_nodes.selected_nodes_ref().clone();
new_selected_nodes.remove(node_to_deselect);
@ -703,7 +738,13 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
});
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
if let Some(select_if_not_dragged) = self.select_if_not_dragged {
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
if selected_nodes.selected_nodes_ref().len() == 1 && !self.begin_dragging {
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());
// Check that neither the primary input or output of the selected node are already connected.
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}");
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.begin_dragging = false;
self.box_selection_start = None;
@ -991,13 +1047,12 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
NodeGraphMessage::SetInput { input_connector, input } => {
network_interface.set_input(&input_connector, input, selection_network_path);
}
NodeGraphMessage::ShiftNodes {
node_ids,
displacement_x,
displacement_y,
move_upstream,
} => {
network_interface.shift_selected_nodes(node_ids, displacement_x, displacement_y, move_upstream, selection_network_path);
NodeGraphMessage::ShiftSelectedNodes { direction, rubber_band } => {
network_interface.shift_selected_nodes(direction, self.shift_without_push, selection_network_path);
if !rubber_band {
network_interface.unload_stack_dependents_y_offset(selection_network_path);
}
if graph_view_overlay_open {
responses.add(NodeGraphMessage::SendGraph);
@ -1022,6 +1077,9 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
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 } => {
if is_layer && !network_interface.is_eligible_to_be_layer(&node_id, selection_network_path) {
return;
@ -1253,16 +1311,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
}
fn actions(&self) -> ActionList {
let mut 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));
}
let common = vec![];
common
}
}
@ -1282,6 +1331,7 @@ impl NodeGraphMessageHandler {
ToggleSelectedLocked,
ToggleSelectedVisibility,
PrintSelectedNodeCoordinates,
ShiftSelectedNodes,
));
}
@ -1732,7 +1782,7 @@ impl NodeGraphMessageHandler {
let mut selected_parents = HashSet::new();
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()) {
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 {
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 =
// 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 }],
drag_start: None,
begin_dragging: false,
shift_without_push: false,
box_selection_start: None,
disconnecting: None,
initial_disconnecting: false,

View File

@ -174,10 +174,18 @@ pub struct FrontendClickTargets {
pub layer_click_targets: Vec<String>,
#[serde(rename = "portClickTargets")]
pub port_click_targets: Vec<String>,
#[serde(rename = "visibilityClickTargets")]
pub visibility_click_targets: Vec<String>,
#[serde(rename = "iconClickTargets")]
pub icon_click_targets: Vec<String>,
#[serde(rename = "allNodesBoundingBox")]
pub all_nodes_bounding_box: String,
#[serde(rename = "importExportsBoundingBox")]
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) })
}
/// 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]
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!(
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}: {:#?}",
network_interface.network(&[]).unwrap().nodes.get(&node_id)
network_interface.network(network_path).unwrap().nodes.get(&node_id)
);
Self::new_unchecked(node_id)
}

View File

@ -552,7 +552,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
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 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
let (Some(layer_position), Some(downstream_position)) =
(document.network_interface.position(&layer.to_node(), &[]), document.network_interface.position(&downstream_node, &[]))

View File

@ -1,9 +1,6 @@
use super::tool_prelude::*;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::tool::common_functionality::resize::Resize;
use graph_craft::document::{generate_uuid, NodeId};
#[derive(Default)]
pub struct ImaginateTool {
fsm_state: ImaginateToolFsmState,
@ -101,7 +98,7 @@ impl Fsm for ImaginateToolFsmState {
(ImaginateToolFsmState::Ready, ImaginateToolMessage::DragStart) => {
shape_data.start(document, input);
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);
// // 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(&[]) {
responses.add(NodeGraphMessage::DeleteNodes {
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(
document.network_interface.root_node(&[]).expect("Root node should exist when dragging layers").node_id,
&document.network_interface,
&[],
))]);
responses.add(NodeGraphMessage::SelectedNodesSet {
nodes: tool_data

View File

@ -162,15 +162,40 @@
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,\
<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,\
<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,\
<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}
<path class="port" d={pathString} />
{/each}
{#each $nodeGraph.clickTargets.visibilityClickTargets as pathString}
{#each $nodeGraph.clickTargets.iconClickTargets as pathString}
<path class="visibility" d={pathString} />
{/each}
<path class="all-nodes-bounding-box" d={$nodeGraph.clickTargets.allNodesBoundingBox} />
@ -642,6 +642,7 @@
{node.displayName}
</span>
</div>
<div class="solo-drag-grip" title="Drag only this layer without pushing others outside the stack"></div>
<IconButton
class={"visibility"}
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 {
position: absolute;
right: -12px;
@ -1247,6 +1265,7 @@
left: calc(-3px + var(--node-chain-area-left-extension) * 24px - 36px);
}
.solo-drag-grip,
.visibility,
.input.ports,
.input.ports .port {

View File

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

View File

@ -622,7 +622,10 @@ impl EditorHandle {
self.dispatch(message);
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);
}
@ -779,7 +782,7 @@ impl EditorHandle {
.map(|(id, _)| *id)
.collect::<Vec<_>>()
{
let layer = LayerNodeIdentifier::new(node, &document.network_interface);
let layer = LayerNodeIdentifier::new(node, &document.network_interface, &[]);
if layer.has_children(document.metadata()) {
continue;
}