Skip to content
6 changes: 5 additions & 1 deletion editor/src/messages/frontend/frontend_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -152,6 +152,10 @@ pub enum FrontendMessage {
#[serde(rename = "box")]
box_selection: Option<BoxSelection>,
},
UpdateLasso {
#[serde(rename = "lasso")]
lasso_selection: Option<LassoSelection>,
},
UpdateContextMenuInformation {
#[serde(rename = "contextMenuInformation")]
context_menu_information: Option<ContextMenuInformation>,
Expand Down
1 change: 1 addition & 0 deletions editor/src/messages/input_mapper/input_mappings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ pub enum NodeGraphMessage {
},
UpdateEdges,
UpdateBoxSelection,
UpdateLassoSelection,
UpdateImportsExports,
UpdateLayerPanel,
UpdateNewNodeGraph,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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;

Expand Down Expand Up @@ -63,7 +64,12 @@ pub struct NodeGraphMessageHandler {
pub drag_start_chain_nodes: Vec<NodeId>,
/// 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<Vec<DVec2>>,
/// Restore the selection before box selection if it is aborted
selection_before_pointer_down: Vec<NodeId>,
/// If the grip icon is held during a drag, then shift without pushing other nodes
Expand Down Expand Up @@ -765,6 +771,15 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> 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;
Expand Down Expand Up @@ -974,7 +989,13 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> 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 } => {
Expand Down Expand Up @@ -1109,6 +1130,9 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> 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");
Expand Down Expand Up @@ -1391,6 +1415,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> 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;
Expand All @@ -1399,12 +1424,17 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> 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
Expand Down Expand Up @@ -1892,15 +1922,6 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> 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;
Expand Down Expand Up @@ -1956,6 +1977,70 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> 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::<HashSet<_>>();
let mut nodes = if shift || alt {
selected_nodes.selected_nodes_ref().iter().cloned().collect::<HashSet<_>>()
} else {
HashSet::new()
};
let all_nodes = network_metadata.persistent_metadata.node_metadata.keys().cloned().collect::<Vec<_>>();
let path: Vec<PathSeg> = {
fn points_to_polygon(points: &[DVec2]) -> Vec<PathSeg> {
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::<Vec<_>>(),
});
}
}
}
NodeGraphMessage::UpdateImportsExports => {
let imports = network_interface.frontend_imports(breadcrumb_network_path);
let exports = network_interface.frontend_exports(breadcrumb_network_path);
Expand Down Expand Up @@ -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;
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions editor/src/messages/portfolio/document/node_graph/utility_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<glam::DVec2> for LassoSelection {
fn from_iter<I: IntoIterator<Item = glam::DVec2>>(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 {
Expand Down
2 changes: 1 addition & 1 deletion editor/src/messages/tool/tool_messages/select_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/components/views/Graph.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,12 @@
></div>
{/if}

{#if $nodeGraph.lasso}
<svg class="lasso-selection" style:clip-path={`polygon(${$nodeGraph.lasso.points})`}>
<polygon points={$nodeGraph.lasso.points} stroke="#00a8ff" stroke-width="1px" fill="rgba(0, 168, 255, 0.05)" />
</svg>
{/if}

<style lang="scss" global>
.graph {
position: relative;
Expand Down Expand Up @@ -1369,4 +1375,14 @@
background: rgba(0, 168, 255, 0.05);
border: 1px solid #00a8ff;
}

.lasso-selection {
position: absolute;
pointer-events: none;
z-index: 2;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
9 changes: 9 additions & 0 deletions frontend/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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[];
Expand Down Expand Up @@ -1665,6 +1673,7 @@ export const messageMakers: Record<string, MessageMaker> = {
TriggerVisitLink,
UpdateActiveDocument,
UpdateBox,
UpdateLasso,
UpdateClickTargets,
UpdateContextMenuInformation,
UpdateDialogButtons,
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/state-providers/node-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -12,6 +13,7 @@ import {
ClearAllNodeGraphWires,
SendUIMetadata,
UpdateBox,
UpdateLasso,
UpdateClickTargets,
UpdateContextMenuInformation,
UpdateInSelectedNetwork,
Expand All @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
Loading