Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Comment on lines +2012 to +2022
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than allocating, consider a simplye point in polygon algorithm https://wrfranklin.org/Research/Short_Notes/pnpoly.html.

Copy link
Contributor Author

@wade-cheng wade-cheng Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I feel the disappeal of needing to allocate, but I'm not sure how point-in-polygon helps. I stole click_targets.node_click_target.intersect_path from... uh, probably the box selection code, and I didn't look further because I knew it worked. I'd need to think about it lots and dig deeper into the code, but first impression: aren't nodes not points? We rather need nodes (rounded rectangles) in polygon?

That is, without some digging, I currently don't know how to get the bounding boxes of all the nodes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it wouldn't matter too much.

};
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<_>>(),
});
}
Comment on lines +2037 to +2041
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is probably easiest to just always send this message (rather than allocating the previous_selection HashMap each frame).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't intuit which is better, but I'll take your word for it. When I or whoever makes the change, I assume they should also make the identical analogous change to the box selection code?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it doesn't matter very much either way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving it for now, then.

}
}
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,11 +2796,12 @@ 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 {
if wiring || dragging_nodes || dragging_selection {
let hint_data = HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]);
responses.add(FrontendMessage::UpdateInputHints { hint_data });
return;
Expand All @@ -2729,6 +2815,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 +2847,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 +2878,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
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