Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Make the node catalog, originating from a wire dropped in the graph, filter for valid types #2423

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
.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))
.is_some_and(|context_menu| matches!(context_menu.context_menu_data, super::node_graph::utility_types::ContextMenuData::CreateNode { compatible_type: None }))
{
// Close the context menu
self.node_graph_handler.context_menu = None;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3409,7 +3409,23 @@ pub fn collect_node_types() -> Vec<FrontendNodeType> {
DOCUMENT_NODE_TYPES
.iter()
.filter(|definition| !definition.category.is_empty())
.map(|definition| FrontendNodeType::new(definition.identifier, definition.category))
.map(|definition| {
let input_types = definition
.node_template
.document_node
.inputs
.iter()
.filter_map(|node_input| {
if let Some(node_value) = node_input.as_value() {
Some(node_value.ty().nested_type().to_string())
} else {
None
}
})
.collect::<Vec<String>>();

FrontendNodeType::with_input_types(definition.identifier, definition.category, input_types)
})
.collect()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,30 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
return;
}

let Some(network_metadata) = network_interface.network_metadata(selection_network_path) else {
log::error!("Could not get network metadata in EnterNestedNetwork");
return;
};

let click = ipp.mouse.position;
let node_graph_point = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.inverse().transform_point2(click);

// Check if clicked on empty area (no node, no input/output connector)
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);

if clicked_id.is_none() && clicked_input.is_none() && clicked_output.is_none() && self.context_menu.is_none() {
// Create a context menu with node creation options
self.context_menu = Some(ContextMenuInformation {
context_menu_coordinates: (node_graph_point.x as i32, node_graph_point.y as i32),
context_menu_data: ContextMenuData::CreateNode { compatible_type: None },
});

responses.add(FrontendMessage::UpdateContextMenuInformation {
context_menu_information: self.context_menu.clone(),
});
}
let Some(node_id) = network_interface.node_from_click(ipp.mouse.position, selection_network_path) else {
return;
};
Expand Down Expand Up @@ -613,11 +637,11 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
currently_is_node: !network_interface.is_layer(&node_id, selection_network_path),
}
} else {
ContextMenuData::CreateNode
ContextMenuData::CreateNode { compatible_type: None }
};

// TODO: Create function
let node_graph_shift = if matches!(context_menu_data, ContextMenuData::CreateNode) {
let node_graph_shift = if matches!(context_menu_data, ContextMenuData::CreateNode { compatible_type: None }) {
let appear_right_of_mouse = if click.x > ipp.viewport_bounds.size().x - 180. { -180. } else { 0. };
let appear_above_mouse = if click.y > ipp.viewport_bounds.size().y - 200. { -200. } else { 0. };
DVec2::new(appear_right_of_mouse, appear_above_mouse) / network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.matrix2.x_axis.x
Expand Down Expand Up @@ -1001,14 +1025,31 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
warn!("No network_metadata");
return;
};

// Get the compatible type from the output connector
let compatible_type = if let Some(output_connector) = &output_connector {
if let Some(node_id) = output_connector.node_id() {
let output_index = output_connector.index();
// Get the output types from the network interface
let output_types = network_interface.output_types(&node_id, selection_network_path);

// Extract the type if available
output_types.get(output_index).and_then(|type_option| type_option.as_ref()).map(|(output_type, _)| {
// Create a search term based on the type
format!("type:{}", output_type.clone().nested_type())
})
} else {
None
}
} else {
None
};
let appear_right_of_mouse = if ipp.mouse.position.x > ipp.viewport_bounds.size().x - 173. { -173. } else { 0. };
let appear_above_mouse = if ipp.mouse.position.y > ipp.viewport_bounds.size().y - 34. { -34. } else { 0. };
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;

self.context_menu = Some(ContextMenuInformation {
context_menu_coordinates: ((point.x + node_graph_shift.x) as i32, (point.y + node_graph_shift.y) as i32),
context_menu_data: ContextMenuData::CreateNode,
context_menu_data: ContextMenuData::CreateNode { compatible_type },
});

responses.add(FrontendMessage::UpdateContextMenuInformation {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,24 @@ pub struct FrontendNodeWire {
pub struct FrontendNodeType {
pub name: String,
pub category: String,
#[serde(rename = "inputTypes")]
pub input_types: Option<Vec<String>>,
}

impl FrontendNodeType {
pub fn new(name: &'static str, category: &'static str) -> Self {
Self {
name: name.to_string(),
category: category.to_string(),
input_types: None,
}
}

pub fn with_input_types(name: &'static str, category: &'static str, input_types: Vec<String>) -> Self {
Self {
name: name.to_string(),
category: category.to_string(),
input_types: Some(input_types),
}
}
}
Expand Down Expand Up @@ -163,7 +174,11 @@ pub enum ContextMenuData {
#[serde(rename = "currentlyIsNode")]
currently_is_node: bool,
},
CreateNode,
CreateNode {
#[serde(rename = "compatibleType")]
#[serde(default)]
compatible_type: Option<String>,
},
}

#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
Expand Down
37 changes: 27 additions & 10 deletions frontend/src/components/floating-menus/NodeCatalog.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
const nodeGraph = getContext<NodeGraphState>("nodeGraph");

export let disabled = false;
// Add prop for initial search term from compatible type
export let initialSearchTerm = "";

let nodeSearchInput: TextInput | undefined = undefined;
let searchTerm = "";
let searchTerm = initialSearchTerm;

$: nodeCategories = buildNodeCategories($nodeGraph.nodeTypes, searchTerm);

Expand All @@ -26,26 +28,41 @@
function buildNodeCategories(nodeTypes: FrontendNodeType[], searchTerm: string): [string, NodeCategoryDetails][] {
const categories = new Map<string, NodeCategoryDetails>();

nodeTypes.forEach((node) => {
let nameIncludesSearchTerm = node.name.toLowerCase().includes(searchTerm.toLowerCase());
// Check if search term is prefixed with "type:"
const isTypeSearch = searchTerm.toLowerCase().startsWith("type:");
const typeSearchTerm = isTypeSearch ? searchTerm.substring(5).trim().toLowerCase() : "";

// Quick and dirty hack to alias "Layer" to "Merge" in the search
if (node.name === "Merge") {
nameIncludesSearchTerm = nameIncludesSearchTerm || "Layer".toLowerCase().includes(searchTerm.toLowerCase());
nodeTypes.forEach((node) => {
let includesSearchTerm = false;

if (isTypeSearch) {
// Check if the type search term is present in any of the node's inputTypes
includesSearchTerm = node.inputTypes?.some((inputType) => inputType.toLowerCase().includes(typeSearchTerm)) || false;
} else {
// Regular name search
includesSearchTerm = node.name.toLowerCase().includes(searchTerm.toLowerCase());

// Quick and dirty hack to alias "Layer" to "Merge" in the search
if (node.name === "Merge") {
includesSearchTerm = includesSearchTerm || "Layer".toLowerCase().includes(searchTerm.toLowerCase());
}
}

if (searchTerm.length > 0 && !nameIncludesSearchTerm && !node.category.toLowerCase().includes(searchTerm.toLowerCase())) {
// Also check category if not a type search
const categoryIncludesSearchTerm = !isTypeSearch && searchTerm.length > 0 && node.category.toLowerCase().includes(searchTerm.toLowerCase());

if (searchTerm.length > 0 && !includesSearchTerm && !categoryIncludesSearchTerm) {
return;
}

const category = categories.get(node.category);
let open = nameIncludesSearchTerm;
let open = includesSearchTerm || categoryIncludesSearchTerm;
if (searchTerm.length === 0) {
open = false;
}

if (category) {
category.open = open;
category.open = category.open || open;
category.nodes.push(node);
} else
categories.set(node.category, {
Expand Down Expand Up @@ -82,7 +99,7 @@
</script>

<div class="node-catalog">
<TextInput placeholder="Search Nodes..." value={searchTerm} on:value={({ detail }) => (searchTerm = detail)} bind:this={nodeSearchInput} />
<TextInput placeholder="Search Nodes... (or type:InputType)" value={searchTerm} on:value={({ detail }) => (searchTerm = detail)} bind:this={nodeSearchInput} />
Copy link
Member

Choose a reason for hiding this comment

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

I'm not quite sure of the UX of having the type as the search term. This prevents the user from further filtering down the options.

For example vector data is used by many nodes (and more will be added over time), so the user may wish to further filter them with a keyword.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi, @0HyperCube pushed code changes that adds support for simultaneous type, category and node name search.

<div class="list-results" on:wheel|passive|stopPropagation>
{#each nodeCategories as nodeCategory}
<details open={nodeCategory[1].open}>
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/views/Graph.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -653,8 +653,10 @@
top: `${$nodeGraph.contextMenuInformation.contextMenuCoordinates.y * $nodeGraph.transform.scale + $nodeGraph.transform.y}px`,
}}
>
{#if $nodeGraph.contextMenuInformation.contextMenuData === "CreateNode"}
{#if typeof $nodeGraph.contextMenuInformation.contextMenuData === "string" && $nodeGraph.contextMenuInformation.contextMenuData === "CreateNode"}
<NodeCatalog on:selectNodeType={(e) => createNode(e.detail)} />
{:else if $nodeGraph.contextMenuInformation.contextMenuData && "compatibleType" in $nodeGraph.contextMenuInformation.contextMenuData}
<NodeCatalog initialSearchTerm={$nodeGraph.contextMenuInformation.contextMenuData.compatibleType || ""} on:selectNodeType={(e) => createNode(e.detail)} />
{:else}
{@const contextMenuData = $nodeGraph.contextMenuInformation.contextMenuData}
<LayoutRow class="toggle-layer-or-node">
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ const ContextTupleToVec2 = Transform((data) => {
let contextMenuData = data.obj.contextMenuInformation.contextMenuData;
if (contextMenuData.ToggleLayer !== undefined) {
contextMenuData = { nodeId: contextMenuData.ToggleLayer.nodeId, currentlyIsNode: contextMenuData.ToggleLayer.currentlyIsNode };
} else if (contextMenuData.CreateNode !== undefined) {
contextMenuData = { type: "CreateNode", compatibleType: contextMenuData.CreateNode.compatibleType };
}
return { contextMenuCoordinates, contextMenuData };
});
Expand Down Expand Up @@ -185,8 +187,7 @@ export type FrontendClickTargets = {

export type ContextMenuInformation = {
contextMenuCoordinates: XY;

contextMenuData: "CreateNode" | { nodeId: bigint; currentlyIsNode: boolean };
contextMenuData: "CreateNode" | { type: "CreateNode"; compatibleType: string } | { nodeId: bigint; currentlyIsNode: boolean };
};

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

readonly category!: string;

readonly inputTypes!: string[];
}

export class NodeGraphTransform {
Expand Down
Loading