diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 0195180f1b..1060420e11 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -2,7 +2,7 @@ use super::utility_types::{DocumentDetails, MouseCursorIcon, OpenDocument}; use crate::messages::app_window::app_window_message_handler::AppWindowPlatform; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::node_graph::utility_types::{ - BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeType, NodeGraphErrorDiagnostic, Transform, + BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeType, LassoSelection, NodeGraphErrorDiagnostic, Transform, }; use crate::messages::portfolio::document::utility_types::nodes::{JsRawBuffer, LayerPanelEntry, RawBuffer}; use crate::messages::portfolio::document::utility_types::wires::{WirePath, WirePathUpdate}; @@ -152,6 +152,10 @@ pub enum FrontendMessage { #[serde(rename = "box")] box_selection: Option, }, + UpdateLasso { + #[serde(rename = "lasso")] + lasso_selection: Option, + }, UpdateContextMenuInformation { #[serde(rename = "contextMenuInformation")] context_menu_information: Option, diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index ce278ea7f2..622de5f184 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -58,6 +58,7 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(MouseLeft); modifiers=[Shift], action_dispatch=NodeGraphMessage::PointerDown { shift_click: true, control_click: false, alt_click: false, right_click: false }), entry!(KeyDown(MouseLeft); modifiers=[Accel], action_dispatch=NodeGraphMessage::PointerDown { shift_click: false, control_click: true, alt_click: false, right_click: false }), entry!(KeyDown(MouseLeft); modifiers=[Shift, Accel], action_dispatch=NodeGraphMessage::PointerDown { shift_click: true, control_click: true, alt_click: false, right_click: false }), + entry!(KeyDown(MouseLeft); modifiers=[Accel, Alt], action_dispatch=NodeGraphMessage::PointerDown { shift_click: false, control_click: true, alt_click: true, right_click: false }), entry!(KeyDown(MouseLeft); modifiers=[Alt], action_dispatch=NodeGraphMessage::PointerDown { shift_click: false, control_click: false, alt_click: true, right_click: false }), entry!(KeyDown(MouseRight); action_dispatch=NodeGraphMessage::PointerDown { shift_click: false, control_click: false, alt_click: false, right_click: true }), entry!(DoubleClick(MouseButton::Left); action_dispatch=NodeGraphMessage::EnterNestedNetwork), diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs index 330d123efe..0a60626dc7 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs @@ -221,6 +221,7 @@ pub enum NodeGraphMessage { }, UpdateEdges, UpdateBoxSelection, + UpdateLassoSelection, UpdateImportsExports, UpdateLayerPanel, UpdateNewNodeGraph, diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 85c7fde361..f797b8270c 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -6,7 +6,7 @@ use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::document_message_handler::navigation_controls; use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext; use crate::messages::portfolio::document::node_graph::document_node_definitions::NodePropertiesContext; -use crate::messages::portfolio::document::node_graph::utility_types::{ContextMenuData, Direction, FrontendGraphDataType, NodeGraphErrorDiagnostic}; +use crate::messages::portfolio::document::node_graph::utility_types::{ContextMenuData, Direction, FrontendGraphDataType, LassoSelection, NodeGraphErrorDiagnostic}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::misc::GroupFolderType; use crate::messages::portfolio::document::utility_types::network_interface::{ @@ -25,8 +25,9 @@ use glam::{DAffine2, DVec2, IVec2}; use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput}; use graphene_std::math::math_ext::QuadExt; use graphene_std::vector::algorithms::bezpath_algorithms::bezpath_is_inside_bezpath; +use graphene_std::vector::misc::dvec2_to_point; use graphene_std::*; -use kurbo::{DEFAULT_ACCURACY, Shape}; +use kurbo::{DEFAULT_ACCURACY, Line, PathSeg, Shape}; use renderer::Quad; use std::cmp::Ordering; @@ -63,7 +64,12 @@ pub struct NodeGraphMessageHandler { pub drag_start_chain_nodes: Vec, /// If dragging the background to create a box selection, this stores its starting point in node graph coordinates, /// plus a flag indicating if it has been dragged since the mousedown began. + /// (We should only update hints when it has been dragged after the initial mousedown.) box_selection_start: Option<(DVec2, bool)>, + /// If dragging the background to create a lasso selection, this stores its current lasso polygon in node graph coordinates. + /// Notice that it has been dragged since the mousedown began iff the polygon has at least two points. + /// (We should only update hints when it has been dragged after the initial mousedown.) + lasso_selection_curr: Option>, /// Restore the selection before box selection if it is aborted selection_before_pointer_down: Vec, /// If the grip icon is held during a drag, then shift without pushing other nodes @@ -765,6 +771,15 @@ impl<'a> MessageHandler> for NodeG responses.add(FrontendMessage::UpdateBox { box_selection: None }); return; } + // Abort a lasso selection + if self.lasso_selection_curr.is_some() { + self.lasso_selection_curr = None; + responses.add(NodeGraphMessage::SelectedNodesSet { + nodes: self.selection_before_pointer_down.clone(), + }); + responses.add(FrontendMessage::UpdateLasso { lasso_selection: None }); + return; + } // Abort dragging a wire if self.wire_in_progress_from_connector.is_some() { self.wire_in_progress_from_connector = None; @@ -974,7 +989,13 @@ impl<'a> MessageHandler> for NodeG if !shift_click && !alt_click { responses.add(NodeGraphMessage::SelectedNodesSet { nodes: Vec::new() }) } - self.box_selection_start = Some((node_graph_point, false)); + + if control_click { + self.lasso_selection_curr = Some(vec![node_graph_point]); + } else { + self.box_selection_start = Some((node_graph_point, false)); + } + self.update_node_graph_hints(responses); } NodeGraphMessage::PointerMove { shift } => { @@ -1109,6 +1130,9 @@ impl<'a> MessageHandler> for NodeG *box_selection_dragged = true; responses.add(NodeGraphMessage::UpdateBoxSelection); self.update_node_graph_hints(responses); + } else if self.lasso_selection_curr.is_some() { + responses.add(NodeGraphMessage::UpdateLassoSelection); + self.update_node_graph_hints(responses); } else if self.reordering_import.is_some() { let Some(modify_import_export) = network_interface.modify_import_export(selection_network_path) else { log::error!("Could not get modify import export in PointerMove"); @@ -1391,6 +1415,7 @@ impl<'a> MessageHandler> for NodeG self.drag_start = None; self.begin_dragging = false; self.box_selection_start = None; + self.lasso_selection_curr = None; self.wire_in_progress_from_connector = None; self.wire_in_progress_type = FrontendGraphDataType::General; self.wire_in_progress_to_connector = None; @@ -1399,12 +1424,17 @@ impl<'a> MessageHandler> for NodeG responses.add(DocumentMessage::EndTransaction); responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: None }); responses.add(FrontendMessage::UpdateBox { box_selection: None }); + responses.add(FrontendMessage::UpdateLasso { lasso_selection: None }); responses.add(FrontendMessage::UpdateImportReorderIndex { index: None }); responses.add(FrontendMessage::UpdateExportReorderIndex { index: None }); self.update_node_graph_hints(responses); } NodeGraphMessage::PointerOutsideViewport { shift } => { - if self.drag_start.is_some() || self.box_selection_start.is_some() || (self.wire_in_progress_from_connector.is_some() && self.context_menu.is_none()) { + if self.drag_start.is_some() + || self.box_selection_start.is_some() + || self.lasso_selection_curr.is_some() + || (self.wire_in_progress_from_connector.is_some() && self.context_menu.is_none()) + { let _ = self.auto_panning.shift_viewport(ipp, viewport, responses); } else { // Auto-panning @@ -1892,15 +1922,6 @@ impl<'a> MessageHandler> for NodeG } NodeGraphMessage::UpdateBoxSelection => { if let Some((box_selection_start, _)) = self.box_selection_start { - // The mouse button was released but we missed the pointer up event - // if ((e.buttons & 1) === 0) { - // completeBoxSelection(); - // boxSelection = undefined; - // } else if ((e.buttons & 2) !== 0) { - // editor.handle.selectNodes(new BigUint64Array(previousSelection)); - // boxSelection = undefined; - // } - let Some(network_metadata) = network_interface.network_metadata(selection_network_path) else { log::error!("Could not get network metadata in UpdateBoxSelection"); return; @@ -1956,6 +1977,70 @@ impl<'a> MessageHandler> for NodeG responses.add(FrontendMessage::UpdateBox { box_selection }) } } + NodeGraphMessage::UpdateLassoSelection => { + if let Some(lasso_selection_curr) = &mut self.lasso_selection_curr { + let Some(network_metadata) = network_interface.network_metadata(selection_network_path) else { + log::error!("Could not get network metadata in UpdateLassoSelection"); + return; + }; + + let node_graph_to_viewport = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport; + let viewport_to_node_graph = node_graph_to_viewport.inverse(); + + lasso_selection_curr.push(viewport_to_node_graph.transform_point2(ipp.mouse.position)); + + responses.add(FrontendMessage::UpdateLasso { + lasso_selection: Some(LassoSelection::from_iter( + lasso_selection_curr.iter().map(|selection_point| node_graph_to_viewport.transform_point2(*selection_point)), + )), + }); + + let shift = ipp.keyboard.get(Key::Shift as usize); + let alt = ipp.keyboard.get(Key::Alt as usize); + let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else { + log::error!("Could not get selected nodes in UpdateLassoSelection"); + return; + }; + let previous_selection = selected_nodes.selected_nodes_ref().iter().cloned().collect::>(); + let mut nodes = if shift || alt { + selected_nodes.selected_nodes_ref().iter().cloned().collect::>() + } else { + HashSet::new() + }; + let all_nodes = network_metadata.persistent_metadata.node_metadata.keys().cloned().collect::>(); + let path: Vec = { + fn points_to_polygon(points: &[DVec2]) -> Vec { + points + .windows(2) + .map(|w| PathSeg::Line(Line::new(dvec2_to_point(w[0]), dvec2_to_point(w[1])))) + .chain(std::iter::once(PathSeg::Line(Line::new( + dvec2_to_point(*points.last().unwrap()), + dvec2_to_point(*points.first().unwrap()), + )))) + .collect() + } + points_to_polygon(lasso_selection_curr) + }; + for node_id in all_nodes { + let Some(click_targets) = network_interface.node_click_targets(&node_id, selection_network_path) else { + log::error!("Could not get transient metadata for node {node_id}"); + continue; + }; + if click_targets.node_click_target.intersect_path(|| path.iter().cloned(), DAffine2::IDENTITY) { + if alt { + nodes.remove(&node_id); + } else { + nodes.insert(node_id); + } + } + } + if nodes != previous_selection { + responses.add(NodeGraphMessage::SelectedNodesSet { + nodes: nodes.into_iter().collect::>(), + }); + } + } + } NodeGraphMessage::UpdateImportsExports => { let imports = network_interface.frontend_imports(breadcrumb_network_path); let exports = network_interface.frontend_exports(breadcrumb_network_path); @@ -2711,12 +2796,19 @@ impl NodeGraphMessageHandler { // Node gragging is in progress (having already moved at least one pixel from the mouse down position) let dragging_nodes = self.drag_start.as_ref().is_some_and(|(_, dragged)| *dragged); - // A box selection is in progress - let dragging_box_selection = self.box_selection_start.is_some_and(|(_, box_selection_dragged)| box_selection_dragged); + // A box or lasso selection is in progress + let dragging_selection = self.box_selection_start.as_ref().is_some_and(|(_, box_selection_dragged)| *box_selection_dragged) + || self.lasso_selection_curr.as_ref().is_some_and(|lasso_selection| lasso_selection.len() >= 2); // Cancel the ongoing action - if wiring || dragging_nodes || dragging_box_selection { - let hint_data = HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]); + if wiring || dragging_nodes || dragging_selection { + let hint_data = HintData(vec![ + HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), + HintGroup(vec![HintInfo::keys([Key::Shift], "Extend"), HintInfo::keys([Key::Alt], "Subtract")]), + // TODO: Re-select deselected layers during drag when Shift is pressed, and re-deselect if Shift is released before drag ends. + // TODO: (See https://discord.com/channels/731730685944922173/1216976541947531264/1321360311298818048) + // TODO: (Also remember to do this for the select tool; grep for these todo comments.) + ]); responses.add(FrontendMessage::UpdateInputHints { hint_data }); return; } @@ -2729,6 +2821,7 @@ impl NodeGraphMessageHandler { HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), HintInfo::keys([Key::Shift], "Extend").prepend_plus(), HintInfo::keys([Key::Alt], "Subtract").prepend_plus(), + HintInfo::keys([Key::Accel], "Lasso").prepend_plus(), ]), ]); if self.has_selection { @@ -2760,6 +2853,7 @@ impl Default for NodeGraphMessageHandler { node_has_moved_in_drag: false, shift_without_push: false, box_selection_start: None, + lasso_selection_curr: None, drag_start_chain_nodes: Vec::new(), selection_before_pointer_down: Vec::new(), disconnecting: None, @@ -2790,6 +2884,7 @@ impl PartialEq for NodeGraphMessageHandler { && self.begin_dragging == other.begin_dragging && self.node_has_moved_in_drag == other.node_has_moved_in_drag && self.box_selection_start == other.box_selection_start + && self.lasso_selection_curr == other.lasso_selection_curr && self.initial_disconnecting == other.initial_disconnecting && self.select_if_not_dragged == other.select_if_not_dragged && self.wire_in_progress_from_connector == other.wire_in_progress_from_connector diff --git a/editor/src/messages/portfolio/document/node_graph/utility_types.rs b/editor/src/messages/portfolio/document/node_graph/utility_types.rs index 60869acadc..5e1c269391 100644 --- a/editor/src/messages/portfolio/document/node_graph/utility_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/utility_types.rs @@ -152,6 +152,22 @@ pub struct BoxSelection { pub end_y: u32, } +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct LassoSelection { + pub points: String, +} + +impl FromIterator for LassoSelection { + fn from_iter>(iter: I) -> Self { + let mut points = String::new(); + for coordinate in iter { + use std::fmt::Write; + write!(&mut points, "{},{} ", coordinate.x, coordinate.y).unwrap(); + } + LassoSelection { points } + } +} + #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] #[serde(tag = "type", content = "data")] pub enum ContextMenuData { diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 938b5ff6f9..b329fd177a 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -1754,7 +1754,7 @@ impl Fsm for SelectToolFsmState { HintGroup(vec![HintInfo::keys([Key::Shift], "Extend"), HintInfo::keys([Key::Alt], "Subtract")]), // TODO: Re-select deselected layers during drag when Shift is pressed, and re-deselect if Shift is released before drag ends. // TODO: (See https://discord.com/channels/731730685944922173/1216976541947531264/1321360311298818048) - // HintGroup(vec![HintInfo::keys([Key::Shift], "Extend")]) + // TODO: (Also remember to do this for the node graph; grep for these todo comments.) ]); responses.add(FrontendMessage::UpdateInputHints { hint_data }); } diff --git a/frontend/src/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index 819d309229..5252f82e0e 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -777,6 +777,12 @@ > {/if} +{#if $nodeGraph.lasso} + + + +{/if} + diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index c7320bfadf..c2969c426d 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -29,6 +29,10 @@ export class UpdateBox extends JsMessage { readonly box!: Box | undefined; } +export class UpdateLasso extends JsMessage { + readonly lasso!: Lasso | undefined; +} + export class UpdateClickTargets extends JsMessage { readonly clickTargets!: FrontendClickTargets | undefined; } @@ -154,6 +158,10 @@ export class Box { readonly endY!: number; } +export class Lasso { + readonly points!: string; +} + export type FrontendClickTargets = { readonly nodeClickTargets: string[]; readonly layerClickTargets: string[]; @@ -1665,6 +1673,7 @@ export const messageMakers: Record = { TriggerVisitLink, UpdateActiveDocument, UpdateBox, + UpdateLasso, UpdateClickTargets, UpdateContextMenuInformation, UpdateDialogButtons, diff --git a/frontend/src/state-providers/node-graph.ts b/frontend/src/state-providers/node-graph.ts index 80d35eadb8..063dba3781 100644 --- a/frontend/src/state-providers/node-graph.ts +++ b/frontend/src/state-providers/node-graph.ts @@ -4,6 +4,7 @@ import { type Editor } from "@graphite/editor"; import type { NodeGraphError } from "@graphite/messages"; import { type Box, + type Lasso, type FrontendClickTargets, type ContextMenuInformation, type FrontendNode, @@ -12,6 +13,7 @@ import { ClearAllNodeGraphWires, SendUIMetadata, UpdateBox, + UpdateLasso, UpdateClickTargets, UpdateContextMenuInformation, UpdateInSelectedNetwork, @@ -32,6 +34,7 @@ import { export function createNodeGraphState(editor: Editor) { const { subscribe, update } = writable({ box: undefined as Box | undefined, + lasso: undefined as Lasso | undefined, clickTargets: undefined as FrontendClickTargets | undefined, contextMenuInformation: undefined as ContextMenuInformation | undefined, error: undefined as NodeGraphError | undefined, @@ -68,6 +71,12 @@ export function createNodeGraphState(editor: Editor) { return state; }); }); + editor.subscriptions.subscribeJsMessage(UpdateLasso, (updateLasso) => { + update((state) => { + state.lasso = updateLasso.lasso; + return state; + }); + }); editor.subscriptions.subscribeJsMessage(UpdateClickTargets, (UpdateClickTargets) => { update((state) => { state.clickTargets = UpdateClickTargets.clickTargets;