Skip to content

Commit 93f7004

Browse files
Make the node catalog, originating from a wire dropped in the graph, filter for valid types (#2423)
* Add InputType based filtering capabilites to NodeCatalog. Send InputTypes through SendUiMetadata under odeTypes. Update NodeCatalog.svelte component to support ype based filtering. Update ContextMenuData to support compatibleType as an input to the searchTerm for the NodeCatalog. Update Graph.svelte component to support new ContextMenuData enum types. Send CompatibleType data from rust backend on wire drag and release to NodeCatalog to already show filtered data. * Add InputType based filtering capabilites to NodeCatalog. Send InputTypes through SendUiMetadata under odeTypes. Update NodeCatalog.svelte component to support ype based filtering. Update ContextMenuData to support compatibleType as an input to the searchTerm for the NodeCatalog. Update Graph.svelte component to support new ContextMenuData enum types. Send CompatibleType data from rust backend on wire drag and release to NodeCatalog to already show filtered data. * Open NodeCatalog on DoubleClick in empty node graph area * Capture Node implementations and filter out uncatogrised nodes before sending metadata. Update NodeCatalog Search filter to support single type search alongside name and category search * Take union of DocumentNodeTypes and registered node implementations, Update missing categories and make sure to remove nodes with empty categories * Code review --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
1 parent 8b0f16e commit 93f7004

File tree

8 files changed

+193
-21
lines changed

8 files changed

+193
-21
lines changed

editor/src/messages/portfolio/document/document_message_handler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
420420
.node_graph_handler
421421
.context_menu
422422
.as_ref()
423-
.is_some_and(|context_menu| matches!(context_menu.context_menu_data, super::node_graph::utility_types::ContextMenuData::CreateNode))
423+
.is_some_and(|context_menu| matches!(context_menu.context_menu_data, super::node_graph::utility_types::ContextMenuData::CreateNode { compatible_type: None }))
424424
{
425425
// Close the context menu
426426
self.node_graph_handler.context_menu = None;

editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3444,11 +3444,86 @@ pub fn resolve_document_node_type(identifier: &str) -> Option<&DocumentNodeDefin
34443444
}
34453445

34463446
pub fn collect_node_types() -> Vec<FrontendNodeType> {
3447-
DOCUMENT_NODE_TYPES
3447+
// Create a mapping from registry ID to document node identifier
3448+
let id_to_identifier_map: HashMap<String, &'static str> = DOCUMENT_NODE_TYPES
3449+
.iter()
3450+
.filter_map(|definition| {
3451+
if let DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier { name }) = &definition.node_template.document_node.implementation {
3452+
Some((name.to_string(), definition.identifier))
3453+
} else {
3454+
None
3455+
}
3456+
})
3457+
.collect();
3458+
let mut extracted_node_types = Vec::new();
3459+
3460+
let node_registry = graphene_core::registry::NODE_REGISTRY.lock().unwrap();
3461+
let node_metadata = graphene_core::registry::NODE_METADATA.lock().unwrap();
3462+
for (id, metadata) in node_metadata.iter() {
3463+
if let Some(implementations) = node_registry.get(id) {
3464+
let identifier = match id_to_identifier_map.get(id) {
3465+
Some(&id) => id.to_string(),
3466+
None => continue,
3467+
};
3468+
3469+
// Extract category from metadata (already creates an owned String)
3470+
let category = metadata.category.unwrap_or_default().to_string();
3471+
3472+
// Extract input types (already creates owned Strings)
3473+
let input_types = implementations
3474+
.iter()
3475+
.flat_map(|(_, node_io)| node_io.inputs.iter().map(|ty| ty.clone().nested_type().to_string()))
3476+
.collect::<HashSet<String>>()
3477+
.into_iter()
3478+
.collect::<Vec<String>>();
3479+
3480+
// Create a FrontendNodeType
3481+
let node_type = FrontendNodeType::with_owned_strings_and_input_types(identifier, category, input_types);
3482+
3483+
// Store the created node_type
3484+
extracted_node_types.push(node_type);
3485+
}
3486+
}
3487+
3488+
let node_types: Vec<FrontendNodeType> = DOCUMENT_NODE_TYPES
34483489
.iter()
34493490
.filter(|definition| !definition.category.is_empty())
3450-
.map(|definition| FrontendNodeType::new(definition.identifier, definition.category))
3451-
.collect()
3491+
.map(|definition| {
3492+
let input_types = definition
3493+
.node_template
3494+
.document_node
3495+
.inputs
3496+
.iter()
3497+
.filter_map(|node_input| node_input.as_value().map(|node_value| node_value.ty().nested_type().to_string()))
3498+
.collect::<Vec<String>>();
3499+
3500+
FrontendNodeType::with_input_types(definition.identifier, definition.category, input_types)
3501+
})
3502+
.collect();
3503+
3504+
// Update categories in extracted_node_types from node_types
3505+
for extracted_node in &mut extracted_node_types {
3506+
if extracted_node.category.is_empty() {
3507+
// Find matching node in node_types and update category if found
3508+
if let Some(matching_node) = node_types.iter().find(|node_type| node_type.name == extracted_node.name) {
3509+
extracted_node.category = matching_node.category.clone();
3510+
}
3511+
}
3512+
}
3513+
let missing_nodes: Vec<FrontendNodeType> = node_types
3514+
.iter()
3515+
.filter(|node| !extracted_node_types.iter().any(|extracted| extracted.name == node.name))
3516+
.cloned()
3517+
.collect();
3518+
3519+
// Add the missing nodes to extracted_node_types
3520+
for node in missing_nodes {
3521+
extracted_node_types.push(node);
3522+
}
3523+
// Remove entries with empty categories
3524+
extracted_node_types.retain(|node| !node.category.is_empty());
3525+
3526+
extracted_node_types
34523527
}
34533528

34543529
pub fn collect_node_descriptions() -> Vec<(String, String)> {

editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,30 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
302302
return;
303303
}
304304

305+
let Some(network_metadata) = network_interface.network_metadata(selection_network_path) else {
306+
log::error!("Could not get network metadata in NodeGraphMessage::EnterNestedNetwork");
307+
return;
308+
};
309+
310+
let click = ipp.mouse.position;
311+
let node_graph_point = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.inverse().transform_point2(click);
312+
313+
// Check if clicked on empty area (no node, no input/output connector)
314+
let clicked_id = network_interface.node_from_click(click, selection_network_path);
315+
let clicked_input = network_interface.input_connector_from_click(click, selection_network_path);
316+
let clicked_output = network_interface.output_connector_from_click(click, selection_network_path);
317+
318+
if clicked_id.is_none() && clicked_input.is_none() && clicked_output.is_none() && self.context_menu.is_none() {
319+
// Create a context menu with node creation options
320+
self.context_menu = Some(ContextMenuInformation {
321+
context_menu_coordinates: (node_graph_point.x as i32, node_graph_point.y as i32),
322+
context_menu_data: ContextMenuData::CreateNode { compatible_type: None },
323+
});
324+
325+
responses.add(FrontendMessage::UpdateContextMenuInformation {
326+
context_menu_information: self.context_menu.clone(),
327+
});
328+
}
305329
let Some(node_id) = network_interface.node_from_click(ipp.mouse.position, selection_network_path) else {
306330
return;
307331
};
@@ -613,11 +637,11 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
613637
let currently_is_node = !network_interface.is_layer(&node_id, selection_network_path);
614638
ContextMenuData::ToggleLayer { node_id, currently_is_node }
615639
} else {
616-
ContextMenuData::CreateNode
640+
ContextMenuData::CreateNode { compatible_type: None }
617641
};
618642

619643
// TODO: Create function
620-
let node_graph_shift = if matches!(context_menu_data, ContextMenuData::CreateNode) {
644+
let node_graph_shift = if matches!(context_menu_data, ContextMenuData::CreateNode { compatible_type: None }) {
621645
let appear_right_of_mouse = if click.x > ipp.viewport_bounds.size().x - 180. { -180. } else { 0. };
622646
let appear_above_mouse = if click.y > ipp.viewport_bounds.size().y - 200. { -200. } else { 0. };
623647
DVec2::new(appear_right_of_mouse, appear_above_mouse) / network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.matrix2.x_axis.x
@@ -1012,14 +1036,27 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
10121036
warn!("No network_metadata");
10131037
return;
10141038
};
1015-
1039+
// Get the compatible type from the output connector
1040+
let compatible_type = output_connector.and_then(|output_connector| {
1041+
output_connector.node_id().and_then(|node_id| {
1042+
let output_index = output_connector.index();
1043+
// Get the output types from the network interface
1044+
let output_types = network_interface.output_types(&node_id, selection_network_path);
1045+
1046+
// Extract the type if available
1047+
output_types.get(output_index).and_then(|type_option| type_option.as_ref()).map(|(output_type, _)| {
1048+
// Create a search term based on the type
1049+
format!("type:{}", output_type.clone().nested_type())
1050+
})
1051+
})
1052+
});
10161053
let appear_right_of_mouse = if ipp.mouse.position.x > ipp.viewport_bounds.size().x - 173. { -173. } else { 0. };
10171054
let appear_above_mouse = if ipp.mouse.position.y > ipp.viewport_bounds.size().y - 34. { -34. } else { 0. };
10181055
let node_graph_shift = DVec2::new(appear_right_of_mouse, appear_above_mouse) / network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.matrix2.x_axis.x;
10191056

10201057
self.context_menu = Some(ContextMenuInformation {
10211058
context_menu_coordinates: ((point.x + node_graph_shift.x) as i32, (point.y + node_graph_shift.y) as i32),
1022-
context_menu_data: ContextMenuData::CreateNode,
1059+
context_menu_data: ContextMenuData::CreateNode { compatible_type },
10231060
});
10241061

10251062
responses.add(FrontendMessage::UpdateContextMenuInformation {

editor/src/messages/portfolio/document/node_graph/utility_types.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,32 @@ pub struct FrontendNodeWire {
107107
pub struct FrontendNodeType {
108108
pub name: String,
109109
pub category: String,
110+
#[serde(rename = "inputTypes")]
111+
pub input_types: Option<Vec<String>>,
110112
}
111113

112114
impl FrontendNodeType {
113115
pub fn new(name: &'static str, category: &'static str) -> Self {
114116
Self {
115117
name: name.to_string(),
116118
category: category.to_string(),
119+
input_types: None,
120+
}
121+
}
122+
123+
pub fn with_input_types(name: &'static str, category: &'static str, input_types: Vec<String>) -> Self {
124+
Self {
125+
name: name.to_string(),
126+
category: category.to_string(),
127+
input_types: Some(input_types),
128+
}
129+
}
130+
131+
pub fn with_owned_strings_and_input_types(name: String, category: String, input_types: Vec<String>) -> Self {
132+
Self {
133+
name,
134+
category,
135+
input_types: Some(input_types),
117136
}
118137
}
119138
}
@@ -162,7 +181,11 @@ pub enum ContextMenuData {
162181
#[serde(rename = "currentlyIsNode")]
163182
currently_is_node: bool,
164183
},
165-
CreateNode,
184+
CreateNode {
185+
#[serde(rename = "compatibleType")]
186+
#[serde(default)]
187+
compatible_type: Option<String>,
188+
},
166189
}
167190

168191
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]

frontend/src/components/floating-menus/NodeCatalog.svelte

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
const nodeGraph = getContext<NodeGraphState>("nodeGraph");
1313
1414
export let disabled = false;
15+
export let initialSearchTerm = "";
1516
1617
let nodeSearchInput: TextInput | undefined = undefined;
17-
let searchTerm = "";
18+
let searchTerm = initialSearchTerm;
1819
1920
$: nodeCategories = buildNodeCategories($nodeGraph.nodeTypes, searchTerm);
2021
@@ -25,33 +26,60 @@
2526
2627
function buildNodeCategories(nodeTypes: FrontendNodeType[], searchTerm: string): [string, NodeCategoryDetails][] {
2728
const categories = new Map<string, NodeCategoryDetails>();
29+
const isTypeSearch = searchTerm.toLowerCase().startsWith("type:");
30+
let typeSearchTerm = "";
31+
let remainingSearchTerms = [searchTerm.toLowerCase()];
32+
33+
if (isTypeSearch) {
34+
// Extract the first word after "type:" as the type search
35+
const searchParts = searchTerm.substring(5).trim().split(/\s+/);
36+
typeSearchTerm = searchParts[0].toLowerCase();
37+
38+
remainingSearchTerms = searchParts.slice(1).map((term) => term.toLowerCase());
39+
}
2840
2941
nodeTypes.forEach((node) => {
30-
let nameIncludesSearchTerm = node.name.toLowerCase().includes(searchTerm.toLowerCase());
42+
let matchesTypeSearch = true;
43+
let matchesRemainingTerms = true;
44+
45+
if (isTypeSearch && typeSearchTerm) {
46+
matchesTypeSearch = node.inputTypes?.some((inputType) => inputType.toLowerCase().includes(typeSearchTerm)) || false;
47+
}
3148
32-
// Quick and dirty hack to alias "Layer" to "Merge" in the search
33-
if (node.name === "Merge") {
34-
nameIncludesSearchTerm = nameIncludesSearchTerm || "Layer".toLowerCase().includes(searchTerm.toLowerCase());
49+
if (remainingSearchTerms.length > 0) {
50+
matchesRemainingTerms = remainingSearchTerms.every((term) => {
51+
const nameMatch = node.name.toLowerCase().includes(term);
52+
const categoryMatch = node.category.toLowerCase().includes(term);
53+
54+
// Quick and dirty hack to alias "Layer" to "Merge" in the search
55+
const layerAliasMatch = node.name === "Merge" && "layer".includes(term);
56+
57+
return nameMatch || categoryMatch || layerAliasMatch;
58+
});
3559
}
3660
37-
if (searchTerm.length > 0 && !nameIncludesSearchTerm && !node.category.toLowerCase().includes(searchTerm.toLowerCase())) {
61+
// Node matches if it passes both type search and remaining terms filters
62+
const includesSearchTerm = matchesTypeSearch && matchesRemainingTerms;
63+
64+
if (searchTerm.length > 0 && !includesSearchTerm) {
3865
return;
3966
}
4067
4168
const category = categories.get(node.category);
42-
let open = nameIncludesSearchTerm;
69+
let open = includesSearchTerm;
4370
if (searchTerm.length === 0) {
4471
open = false;
4572
}
4673
4774
if (category) {
48-
category.open = open;
75+
category.open = category.open || open;
4976
category.nodes.push(node);
50-
} else
77+
} else {
5178
categories.set(node.category, {
5279
open,
5380
nodes: [node],
5481
});
82+
}
5583
});
5684
5785
const START_CATEGORIES_ORDER = ["UNCATEGORIZED", "General", "Value", "Math", "Style"];

frontend/src/components/views/Graph.svelte

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -653,8 +653,10 @@
653653
top: `${$nodeGraph.contextMenuInformation.contextMenuCoordinates.y * $nodeGraph.transform.scale + $nodeGraph.transform.y}px`,
654654
}}
655655
>
656-
{#if $nodeGraph.contextMenuInformation.contextMenuData === "CreateNode"}
656+
{#if typeof $nodeGraph.contextMenuInformation.contextMenuData === "string" && $nodeGraph.contextMenuInformation.contextMenuData === "CreateNode"}
657657
<NodeCatalog on:selectNodeType={(e) => createNode(e.detail)} />
658+
{:else if $nodeGraph.contextMenuInformation.contextMenuData && "compatibleType" in $nodeGraph.contextMenuInformation.contextMenuData}
659+
<NodeCatalog initialSearchTerm={$nodeGraph.contextMenuInformation.contextMenuData.compatibleType || ""} on:selectNodeType={(e) => createNode(e.detail)} />
658660
{:else}
659661
{@const contextMenuData = $nodeGraph.contextMenuInformation.contextMenuData}
660662
<LayoutRow class="toggle-layer-or-node">

frontend/src/messages.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ const ContextTupleToVec2 = Transform((data) => {
4646
let contextMenuData = data.obj.contextMenuInformation.contextMenuData;
4747
if (contextMenuData.ToggleLayer !== undefined) {
4848
contextMenuData = { nodeId: contextMenuData.ToggleLayer.nodeId, currentlyIsNode: contextMenuData.ToggleLayer.currentlyIsNode };
49+
} else if (contextMenuData.CreateNode !== undefined) {
50+
contextMenuData = { type: "CreateNode", compatibleType: contextMenuData.CreateNode.compatibleType };
4951
}
5052
return { contextMenuCoordinates, contextMenuData };
5153
});
@@ -185,8 +187,7 @@ export type FrontendClickTargets = {
185187

186188
export type ContextMenuInformation = {
187189
contextMenuCoordinates: XY;
188-
189-
contextMenuData: "CreateNode" | { nodeId: bigint; currentlyIsNode: boolean };
190+
contextMenuData: "CreateNode" | { type: "CreateNode"; compatibleType: string } | { nodeId: bigint; currentlyIsNode: boolean };
190191
};
191192

192193
export type FrontendGraphDataType = "General" | "Raster" | "VectorData" | "Number" | "Group" | "Artboard";
@@ -337,6 +338,8 @@ export class FrontendNodeType {
337338
readonly name!: string;
338339

339340
readonly category!: string;
341+
342+
readonly inputTypes!: string[];
340343
}
341344

342345
export class NodeGraphTransform {

node-graph/gcore/src/types.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,4 +357,8 @@ impl ProtoNodeIdentifier {
357357
pub const fn new(name: &'static str) -> Self {
358358
ProtoNodeIdentifier { name: Cow::Borrowed(name) }
359359
}
360+
361+
pub const fn with_owned_string(name: String) -> Self {
362+
ProtoNodeIdentifier { name: Cow::Owned(name) }
363+
}
360364
}

0 commit comments

Comments
 (0)