diff --git a/scripts/addons_core/node_wrangler/interface.py b/scripts/addons_core/node_wrangler/interface.py index 1b71bb7eb5b..aa47103327d 100644 --- a/scripts/addons_core/node_wrangler/interface.py +++ b/scripts/addons_core/node_wrangler/interface.py @@ -408,15 +408,6 @@ class NWAttributeMenu(bpy.types.Menu): l.label(text="No attributes on objects with this material") -class NWSwitchNodeTypeMenu(Menu, NWBaseMenu): - bl_idname = "NODE_MT_nw_switch_node_type_menu" - bl_label = "Switch Type to..." - - def draw(self, context): - layout = self.layout - layout.label(text="This operator is removed due to the changes of node menus.", icon='ERROR') - layout.label(text="A native implementation of the function is expected in the future.") - # # APPENDAGES TO EXISTING UI # @@ -499,7 +490,6 @@ classes = ( NWLinkUseNodeNameMenu, NWLinkUseOutputsNamesMenu, NWAttributeMenu, - NWSwitchNodeTypeMenu, ) diff --git a/scripts/addons_core/node_wrangler/preferences.py b/scripts/addons_core/node_wrangler/preferences.py index 76a01d845c2..2b877b64b53 100644 --- a/scripts/addons_core/node_wrangler/preferences.py +++ b/scripts/addons_core/node_wrangler/preferences.py @@ -357,8 +357,6 @@ kmi_defs = ( (('name', interface.NWLinkActiveToSelectedMenu.bl_idname),), "Link active to selected (menu)"), ('wm.call_menu', 'C', 'PRESS', False, True, False, (('name', interface.NWCopyToSelectedMenu.bl_idname),), "Copy to selected (menu)"), - ('wm.call_menu', 'S', 'PRESS', False, True, False, - (('name', interface.NWSwitchNodeTypeMenu.bl_idname),), "Switch node type menu"), ) classes = ( diff --git a/scripts/presets/keyconfig/keymap_data/blender_default.py b/scripts/presets/keyconfig/keymap_data/blender_default.py index 378c210854a..5ac459d1d7c 100644 --- a/scripts/presets/keyconfig/keymap_data/blender_default.py +++ b/scripts/presets/keyconfig/keymap_data/blender_default.py @@ -2231,6 +2231,7 @@ def km_node_editor(params): ("node.link_make", {"type": 'J', "value": 'PRESS', "shift": True}, {"properties": [("replace", True)]}), op_menu("NODE_MT_add", {"type": 'A', "value": 'PRESS', "shift": True}), + op_menu("NODE_MT_swap", {"type": 'S', "value": 'PRESS', "shift": True}), ("node.duplicate_move", {"type": 'D', "value": 'PRESS', "shift": True}, {"properties": [("NODE_OT_translate_attach", [("TRANSFORM_OT_translate", [("view2d_edge_pan", True)])])]}), ("node.duplicate_move_linked", {"type": 'D', "value": 'PRESS', "alt": True}, diff --git a/scripts/startup/bl_operators/node.py b/scripts/startup/bl_operators/node.py index 00197aeaac8..ee61f8cd97c 100644 --- a/scripts/startup/bl_operators/node.py +++ b/scripts/startup/bl_operators/node.py @@ -28,6 +28,84 @@ from bpy.app.translations import ( ) +math_nodes = { + "ShaderNodeMath", + "ShaderNodeVectorMath", + "FunctionNodeIntegerMath", + "FunctionNodeBooleanMath", + "FunctionNodeBitMath", +} + +switch_nodes = { + "GeometryNodeMenuSwitch", + "GeometryNodeIndexSwitch", +} + + +# A context manager for temporarily unparenting nodes from their frames +# This gets rid of issues with framed nodes using relative coordinates +class temporary_unframe: + def __init__(self, nodes): + self.parent_dict = {} + for node in nodes: + if node.parent is not None: + self.parent_dict[node] = node.parent + node.parent = None + + def __enter__(self): + return self + + def __exit__(self, _type, _value, _traceback): + for node, parent in self.parent_dict.items(): + node.parent = parent + + +def cast_value(source, target): + source_type = source.type + target_type = target.type + + value = source.default_value + + def to_bool(value): return value > 0 + def single_value_to_color(value): return Vector((value, value, value, 1.0)) + def single_value_to_vector(value): return Vector([value,] * len(target.default_value)) + def color_to_float(color): return (0.2126 * color[0]) + (0.7152 * color[1]) + (0.0722 * color[2]) + def vector_to_float(vector): return sum(vector) / len(vector) + + func_map = { + ('VALUE', 'INT'): int, + ('VALUE', 'BOOLEAN'): to_bool, + ('VALUE', 'RGBA'): single_value_to_color, + ('VALUE', 'VECTOR'): single_value_to_vector, + ('INT', 'VALUE'): float, + ('INT', 'BOOLEAN'): to_bool, + ('INT', 'RGBA'): single_value_to_color, + ('INT', 'VECTOR'): single_value_to_vector, + ('BOOLEAN', 'VALUE'): float, + ('BOOLEAN', 'INT'): int, + ('BOOLEAN', 'RGBA'): single_value_to_color, + ('BOOLEAN', 'VECTOR'): single_value_to_vector, + ('RGBA', 'VALUE'): color_to_float, + ('RGBA', 'INT'): lambda color: int(color_to_float(color)), + ('RGBA', 'BOOLEAN'): lambda color: to_bool(color_to_float(color)), + ('RGBA', 'VECTOR'): lambda color: color[:len(target.default_value)], + ('VECTOR', 'VALUE'): vector_to_float, + ('VECTOR', 'INT'): lambda vector: int(vector_to_float(vector)), + # Even negative vectors get implicitly converted to True, hence to_bool is not used + ('VECTOR', 'BOOLEAN'): lambda vector: bool(vector_to_float(vector)), + ('VECTOR', 'RGBA'): lambda vector: list(vector).extend([0.0] * (len(target.default_value) - len(vector))) + } + + if source_type == target_type: + return value + + cast_func = func_map.get((source_type, target_type)) + if cast_func is not None: + return cast_func(value) + + return None + + class NodeSetting(PropertyGroup): __slots__ = () @@ -39,14 +117,7 @@ class NodeSetting(PropertyGroup): ) -# Base class for node "Add" operators. -class NodeAddOperator: - - use_transform: BoolProperty( - name="Use Transform", - description="Start transform operator after inserting the node", - default=False, - ) +class NodeOperator: settings: CollectionProperty( name="Settings", description="Settings to be applied on the newly created node", @@ -54,6 +125,77 @@ class NodeAddOperator: options={'SKIP_SAVE'}, ) + @classmethod + def description(cls, _context, properties): + from nodeitems_builtins import node_tree_group_type + + nodetype = properties["type"] + if nodetype in node_tree_group_type.values(): + for setting in properties.settings: + if setting.name == "node_tree": + node_group = eval(setting.value) + if node_group.description: + return node_group.description + bl_rna = bpy.types.Node.bl_rna_get_subclass(nodetype) + if bl_rna is not None: + return tip_(bl_rna.description) + else: + return "" + + # Deselect all nodes in the tree. + @staticmethod + def deselect_nodes(context): + space = context.space_data + tree = space.edit_tree + for n in tree.nodes: + n.select = False + + def create_node(self, context, node_type): + space = context.space_data + tree = space.edit_tree + + try: + node = tree.nodes.new(type=node_type) + except RuntimeError as ex: + self.report({'ERROR'}, str(ex)) + return None + + node.select = True + tree.nodes.active = node + node.location = space.cursor_location + return node + + def apply_node_settings(self, node): + for setting in self.settings: + # XXX catch exceptions here? + value = eval(setting.value) + node_data = node + node_attr_name = setting.name + + # Support path to nested data. + if '.' in node_attr_name: + node_data_path, node_attr_name = node_attr_name.rsplit(".", 1) + node_data = node.path_resolve(node_data_path) + + try: + setattr(node_data, node_attr_name, value) + except AttributeError as ex: + self.report( + {'ERROR_INVALID_INPUT'}, + rpt_("Node has no attribute {:s}").format(setting.name)) + print(str(ex)) + # Continue despite invalid attribute + return node + + +# Base class for node "Add" operators. +class NodeAddOperator(NodeOperator): + use_transform: BoolProperty( + name="Use Transform", + description="Start transform operator after inserting the node", + default=False, + ) + @staticmethod def store_mouse_cursor(context, event): space = context.space_data @@ -72,49 +214,6 @@ class NodeAddOperator: else: space.cursor_location = tree.view_center - # Deselect all nodes in the tree. - @staticmethod - def deselect_nodes(context): - space = context.space_data - tree = space.edit_tree - for n in tree.nodes: - n.select = False - - def create_node(self, context, node_type): - space = context.space_data - tree = space.edit_tree - - try: - node = tree.nodes.new(type=node_type) - except RuntimeError as ex: - self.report({'ERROR'}, str(ex)) - return None - - for setting in self.settings: - # XXX catch exceptions here? - value = eval(setting.value) - node_data = node - node_attr_name = setting.name - - # Support path to nested data. - if '.' in node_attr_name: - node_data_path, node_attr_name = node_attr_name.rsplit(".", 1) - node_data = node.path_resolve(node_data_path) - - try: - setattr(node_data, node_attr_name, value) - except AttributeError as ex: - self.report( - {'ERROR_INVALID_INPUT'}, - rpt_("Node has no attribute {:s}").format(setting.name)) - print(str(ex)) - # Continue despite invalid attribute - - node.select = True - tree.nodes.active = node - node.location = space.cursor_location - return node - @classmethod def poll(cls, context): space = context.space_data @@ -135,6 +234,176 @@ class NodeAddOperator: return result +class NodeSwapOperator(NodeOperator): + properties_to_pass = ( + 'color', + 'hide', + 'label', + 'mute', + 'parent', + 'show_options', + 'show_preview', + 'show_texture', + 'use_alpha', + 'use_clamp', + 'use_custom_color', + "operation", + "domain", + "data_type", + ) + + @classmethod + def poll(cls, context): + if (context.area is None) or (context.area.type != "NODE_EDITOR"): + return False + + if len(context.selected_nodes) <= 0: + cls.poll_message_set("No nodes selected.") + return False + + return True + + def transfer_node_properties(self, old_node, new_node): + for attr in self.properties_to_pass: + if (attr in self.settings): + return + + if hasattr(old_node, attr) and hasattr(new_node, attr): + try: + setattr(new_node, attr, getattr(old_node, attr)) + except (TypeError, ValueError): + pass + + def transfer_input_values(self, old_node, new_node): + if (old_node.bl_idname in math_nodes) and (new_node.bl_idname in math_nodes): + for source_input, target_input in zip(old_node.inputs, new_node.inputs): + + new_value = cast_value(source=source_input, target=target_input) + + if new_value is not None: + target_input.default_value = new_value + + else: + for input in old_node.inputs: + try: + new_socket = new_node.inputs[input.name] + new_value = cast_value(source=input, target=new_socket) + + settings_name = f'inputs["{input.name}"].default_value' + already_defined = (settings_name in self.settings) + + if (new_value is not None) and not already_defined: + new_socket.default_value = new_value + + except (AttributeError, KeyError, TypeError): + pass + + @staticmethod + def transfer_links(tree, old_node, new_node, is_input): + both_math_nodes = (old_node.bl_idname in math_nodes) and (new_node.bl_idname in math_nodes) + + if is_input: + if both_math_nodes: + for i, input in enumerate(old_node.inputs): + for link in input.links[:]: + try: + new_socket = new_node.inputs[i] + + if new_socket.hide or not new_socket.enabled: + continue + + tree.links.new(link.from_socket, new_socket) + except IndexError: + pass + else: + for input in old_node.inputs: + links = sorted(input.links, key=lambda link: link.multi_input_sort_id) + + for link in links: + try: + new_socket = new_node.inputs[input.name] + + if new_socket.hide or not new_socket.enabled: + continue + + tree.links.new(link.from_socket, new_socket) + except KeyError: + pass + + else: + if both_math_nodes: + for i, output in enumerate(old_node.outputs): + for link in output.links[:]: + try: + new_socket = new_node.outputs[i] + + if new_socket.hide or not new_socket.enabled: + continue + + new_link = tree.links.new(new_socket, link.to_socket) + except IndexError: + pass + + else: + for output in old_node.outputs: + for link in output.links[:]: + try: + new_socket = new_node.outputs[output.name] + + if new_socket.hide or not new_socket.enabled: + continue + + new_link = tree.links.new(new_socket, link.to_socket) + + try: + if link.to_socket.is_multi_input: + new_link.swap_multi_input_sort_id(link) + except AttributeError: + pass + + except KeyError: + pass + + @staticmethod + def get_switch_items(node): + switch_type = node.bl_idname + + if switch_type == "GeometryNodeMenuSwitch": + return node.enum_definition.enum_items + elif switch_type == "GeometryNodeIndexSwitch": + return node.index_switch_items + + def transfer_switch_data(self, old_node, new_node): + old_switch_items = self.get_switch_items(old_node) + new_switch_items = self.get_switch_items(new_node) + + new_switch_items.clear() + + if new_node.bl_idname == "GeometryNodeMenuSwitch": + for i, old_item in enumerate(old_switch_items[:]): + # Change the menu item names to numerical indices + # This makes it so that later functions that match by socket name work on the switches + if hasattr(old_item, "name"): + old_item.name = str(i) + + new_switch_items.new(str(i)) + + if (old_switch_value := old_node.inputs[0].default_value) != '': + new_node.inputs[0].default_value = str(old_switch_value) + + elif new_node.bl_idname == "GeometryNodeIndexSwitch": + for i, old_item in enumerate(old_switch_items[:]): + # Change the menu item names to numerical indices + # This makes it so that later functions that match by socket name work on the switches + if hasattr(old_item, "name"): + old_item.name = str(i) + + new_switch_items.new() + + if (old_switch_value := old_node.inputs[0].default_value) != '': + new_node.inputs[0].default_value = int(old_switch_value) + + # Simple basic operator for adding a node. class NODE_OT_add_node(NodeAddOperator, Operator): """Add a node to the active tree""" @@ -158,6 +427,7 @@ class NODE_OT_add_node(NodeAddOperator, Operator): if self.properties.is_property_set("type"): self.deselect_nodes(context) if node := self.create_node(context, self.type): + self.apply_node_settings(node) if self.visible_output: for socket in node.outputs: if socket.name != self.visible_output: @@ -166,22 +436,91 @@ class NODE_OT_add_node(NodeAddOperator, Operator): else: return {'CANCELLED'} - @classmethod - def description(cls, _context, properties): - from nodeitems_builtins import node_tree_group_type - nodetype = properties["type"] - if nodetype in node_tree_group_type.values(): - for setting in properties.settings: - if setting.name == "node_tree": - node_group = eval(setting.value) - if node_group.description: - return node_group.description - bl_rna = bpy.types.Node.bl_rna_get_subclass(nodetype) - if bl_rna is not None: - return tip_(bl_rna.description) - else: - return "" +class NODE_OT_swap_node(NodeSwapOperator, Operator): + """Replace the selected nodes with the specified type""" + bl_idname = "node.swap_node" + bl_label = "Swap Node" + bl_options = {"REGISTER", "UNDO"} + + type: StringProperty( + name="Node Type", + description="Node type", + ) + + visible_output: StringProperty( + name="Output Name", + description="If provided, all outputs that are named differently will be hidden", + options={'SKIP_SAVE'}, + ) + + @staticmethod + def get_zone_pair(tree, node): + # Get paired output node + if hasattr(node, "paired_output"): + return node, node.paired_output + + # Get paired input node + for input_node in tree.nodes: + if hasattr(input_node, "paired_output"): + if input_node.paired_output == node: + return input_node, node + + return None + + def execute(self, context): + tree = context.space_data.edit_tree + + for old_node in context.selected_nodes[:]: + if tree.nodes.get(old_node.name) is None: + continue + + if old_node.bl_idname == self.type: + self.apply_node_settings(old_node) + continue + + new_node = self.create_node(context, self.type) + self.apply_node_settings(new_node) + self.transfer_node_properties(old_node, new_node) + + if self.visible_output: + for socket in new_node.outputs: + if socket.name != self.visible_output: + socket.hide = True + + with temporary_unframe((old_node,)): + new_node.location = old_node.location + new_node.select = True + + zone_pair = self.get_zone_pair(tree, old_node) + + if zone_pair is not None: + input_node, output_node = zone_pair + + if input_node.select and output_node.select: + with temporary_unframe((input_node, output_node)): + new_node.location = (input_node.location + output_node.location) / 2 + new_node.select = True + + self.transfer_input_values(input_node, new_node) + + self.transfer_links(tree, input_node, new_node, is_input=True) + self.transfer_links(tree, output_node, new_node, is_input=False) + + for node in zone_pair: + tree.nodes.remove(node) + else: + if (old_node.bl_idname in switch_nodes) and (new_node.bl_idname in switch_nodes): + self.transfer_switch_data(old_node, new_node) + + self.transfer_input_values(old_node, new_node) + + self.transfer_links(tree, old_node, new_node, is_input=True) + self.transfer_links(tree, old_node, new_node, is_input=False) + + tree.nodes.remove(old_node) + + return {'FINISHED'} class NODE_OT_add_empty_group(NodeAddOperator, bpy.types.Operator): @@ -190,12 +529,19 @@ class NODE_OT_add_empty_group(NodeAddOperator, bpy.types.Operator): bl_description = "Add a group node with an empty group" bl_options = {'REGISTER', 'UNDO'} + # Override inherited method from NodeOperator + # Return None so that bl_description is used + @classmethod + def description(cls, _context, properties): + ... + def execute(self, context): from nodeitems_builtins import node_tree_group_type tree = context.space_data.edit_tree group = self.create_empty_group(tree.bl_idname) self.deselect_nodes(context) node = self.create_node(context, node_tree_group_type[tree.bl_idname]) + self.apply_node_settings(node) node.node_tree = group return {"FINISHED"} @@ -213,7 +559,45 @@ class NODE_OT_add_empty_group(NodeAddOperator, bpy.types.Operator): return group -class NodeAddZoneOperator(NodeAddOperator): +class NODE_OT_swap_empty_group(NodeSwapOperator, bpy.types.Operator): + bl_idname = "node.swap_empty_group" + bl_label = "Swap Empty Group" + bl_description = "Replace active node with an empty group" + bl_options = {'REGISTER', 'UNDO'} + + # Override inherited method from NodeOperator + # Return None so that bl_description is used + @classmethod + def description(cls, _context, properties): + ... + + def execute(self, context): + from nodeitems_builtins import node_tree_group_type + tree = context.space_data.edit_tree + group = self.create_empty_group(tree.bl_idname) + + bpy.ops.node.swap_node('INVOKE_DEFAULT', type=node_tree_group_type[tree.bl_idname]) + + for node in context.selected_nodes: + node.node_tree = group + + return {"FINISHED"} + + @staticmethod + def create_empty_group(idname): + group = bpy.data.node_groups.new(name="NodeGroup", type=idname) + input_node = group.nodes.new('NodeGroupInput') + input_node.select = False + input_node.location.x = -200 - input_node.width + + output_node = group.nodes.new('NodeGroupOutput') + output_node.is_active_output = True + output_node.select = False + output_node.location.x = 200 + return group + + +class ZoneOperator: offset: FloatVectorProperty( name="Offset", description="Offset of nodes from the cursor when added", @@ -221,6 +605,25 @@ class NodeAddZoneOperator(NodeAddOperator): default=(150, 0), ) + zone_tooltips = { + "GeometryNodeSimulationInput": "Simulate the execution of nodes across a time span", + "GeometryNodeRepeatInput": "Execute nodes with a dynamic number of repetitions", + "GeometryNodeForeachGeometryElementInput": "Perform operations separately for each geometry element (e.g. vertices, edges, etc.)", + "NodeClosureInput": "Wrap nodes inside a closure that can be executed at a different part of the nodetree", + } + + @classmethod + def description(cls, _context, properties): + input_node_type = getattr(properties, "input_node_type", None) + + # For Add Zone operators, use class variable instead of operator property + if input_node_type is None: + input_node_type = cls.input_node_type + + return cls.zone_tooltips.get(input_node_type, None) + + +class NodeAddZoneOperator(ZoneOperator, NodeAddOperator): add_default_geometry_link = True def execute(self, context): @@ -230,6 +633,10 @@ class NodeAddZoneOperator(NodeAddOperator): self.deselect_nodes(context) input_node = self.create_node(context, self.input_node_type) output_node = self.create_node(context, self.output_node_type) + + self.apply_node_settings(input_node) + self.apply_node_settings(output_node) + if input_node is None or output_node is None: return {'CANCELLED'} @@ -249,6 +656,145 @@ class NodeAddZoneOperator(NodeAddOperator): return {'FINISHED'} +class NODE_OT_add_zone(NodeAddZoneOperator, Operator): + bl_idname = "node.add_zone" + bl_label = "Add Zone" + bl_options = {'REGISTER', 'UNDO'} + + input_node_type: StringProperty( + name="Input Node", + description="Specifies the input node used the created zone", + ) + + output_node_type: StringProperty( + name="Output Node", + description="Specifies the output node used the created zone", + ) + + add_default_geometry_link: BoolProperty( + name="Add Geometry Link", + description="When enabled, create a link between geometry sockets in this zone", + default=False, + ) + + +class NODE_OT_swap_zone(ZoneOperator, NodeSwapOperator, Operator): + bl_idname = "node.swap_zone" + bl_label = "Swap Zone" + bl_options = {"REGISTER", "UNDO"} + + input_node_type: StringProperty( + name="Input Node", + description="Specifies the input node used the created zone", + ) + + output_node_type: StringProperty( + name="Output Node", + description="Specifies the output node used the created zone", + ) + + add_default_geometry_link: BoolProperty( + name="Add Geometry Link", + description="When enabled, create a link between geometry sockets in this zone", + default=False, + ) + + @staticmethod + def get_zone_pair(tree, node): + # Get paired output node + if hasattr(node, "paired_output"): + return node, node.paired_output + + # Get paired input node + for input_node in tree.nodes: + if hasattr(input_node, "paired_output"): + if input_node.paired_output == node: + return input_node, node + + return None + + def execute(self, context): + tree = context.space_data.edit_tree + + for old_node in context.selected_nodes[:]: + if tree.nodes.get(old_node.name) is None: + continue + + zone_pair = self.get_zone_pair(tree, old_node) + + if (old_node.bl_idname in {self.input_node_type, self.output_node_type}): + if zone_pair is not None: + old_input_node, old_output_node = zone_pair + self.apply_node_settings(old_input_node) + self.apply_node_settings(old_output_node) + else: + self.apply_node_settings(old_node) + + continue + + input_node = self.create_node(context, self.input_node_type) + output_node = self.create_node(context, self.output_node_type) + + self.apply_node_settings(input_node) + self.apply_node_settings(output_node) + + if input_node is None or output_node is None: + return {'CANCELLED'} + + # Simulation input must be paired with the output. + input_node.pair_with_output(output_node) + + if zone_pair is not None: + old_input_node, old_output_node = zone_pair + + with temporary_unframe((old_input_node, old_output_node)): + input_node.location = old_input_node.location + output_node.location = old_output_node.location + + self.transfer_node_properties(old_input_node, input_node) + self.transfer_node_properties(old_output_node, output_node) + + self.transfer_input_values(old_input_node, input_node) + self.transfer_input_values(old_output_node, output_node) + + self.transfer_links(tree, old_input_node, input_node, is_input=True) + self.transfer_links(tree, old_input_node, input_node, is_input=False) + + self.transfer_links(tree, old_output_node, output_node, is_input=True) + self.transfer_links(tree, old_output_node, output_node, is_input=False) + + for node in zone_pair: + tree.nodes.remove(node) + else: + with temporary_unframe((old_node,)): + input_node.location = old_node.location + output_node.location = old_node.location + + input_node.location -= Vector(self.offset) + output_node.location += Vector(self.offset) + + self.transfer_node_properties(old_node, input_node) + self.transfer_node_properties(old_node, output_node) + + self.transfer_input_values(old_node, input_node) + + self.transfer_links(tree, old_node, input_node, is_input=True) + self.transfer_links(tree, old_node, output_node, is_input=False) + + tree.nodes.remove(old_node) + + if tree.type == "GEOMETRY" and self.add_default_geometry_link: + # Connect geometry sockets by default if available. + # Get the sockets by their types, because the name is not guaranteed due to i18n. + from_socket = next(s for s in input_node.outputs if s.type == 'GEOMETRY') + to_socket = next(s for s in output_node.inputs if s.type == 'GEOMETRY') + + if not (from_socket.is_linked or to_socket.is_linked): + tree.links.new(to_socket, from_socket) + + return {'FINISHED'} + + class NODE_OT_add_simulation_zone(NodeAddZoneOperator, Operator): """Add simulation zone input and output nodes to the active tree""" bl_idname = "node.add_simulation_zone" @@ -750,8 +1296,12 @@ classes = ( NODE_FH_image_node, - NODE_OT_add_empty_group, NODE_OT_add_node, + NODE_OT_swap_node, + NODE_OT_add_empty_group, + NODE_OT_swap_empty_group, + NODE_OT_add_zone, + NODE_OT_swap_zone, NODE_OT_add_simulation_zone, NODE_OT_add_repeat_zone, NODE_OT_add_foreach_geometry_element_zone, diff --git a/scripts/startup/bl_ui/node_add_menu.py b/scripts/startup/bl_ui/node_add_menu.py index 1420864a623..5d47847192d 100644 --- a/scripts/startup/bl_ui/node_add_menu.py +++ b/scripts/startup/bl_ui/node_add_menu.py @@ -25,112 +25,20 @@ from bpy.app.translations import ( ) +# NOTE: This is kept for compatibility's sake, as some scripts import node_add_menu.add_node_type def add_node_type(layout, node_type, *, label=None, poll=None, search_weight=0.0, translate=True): """Add a node type to a menu.""" - bl_rna = bpy.types.Node.bl_rna_get_subclass(node_type) - if not label: - label = bl_rna.name if bl_rna else iface_("Unknown") - - if poll is True or poll is None: - translation_context = bl_rna.translation_context if bl_rna else i18n_contexts.default - props = layout.operator( - "node.add_node", - text=label, - text_ctxt=translation_context, - translate=translate, - search_weight=search_weight) - props.type = node_type - props.use_transform = True - return props - - return None - - -def add_node_type_with_outputs(context, layout, node_type, subnames, *, label=None, search_weight=0.0): - bl_rna = bpy.types.Node.bl_rna_get_subclass(node_type) - if not label: - label = bl_rna.name if bl_rna else "Unknown" - - props = [] - props.append(add_node_type(layout, node_type, label=label, search_weight=search_weight)) - if getattr(context, "is_menu_search", False): - for subname in subnames: - item_props = add_node_type( - layout, - node_type, - label="{:s} \u25B8 {:s}".format(iface_(label), iface_(subname)), - search_weight=search_weight, - translate=False, - ) - item_props.visible_output = subname - props.append(item_props) - return props - - -def draw_node_group_add_menu(context, layout): - """Add items to the layout used for interacting with node groups.""" - space_node = context.space_data - node_tree = space_node.edit_tree - all_node_groups = context.blend_data.node_groups - - if node_tree in all_node_groups.values(): - layout.separator() - add_node_type(layout, "NodeGroupInput") - add_node_type(layout, "NodeGroupOutput") - - add_empty_group(layout) - - if node_tree: - from nodeitems_builtins import node_tree_group_type - - prefs = bpy.context.preferences - show_hidden = prefs.filepaths.show_hidden_files_datablocks - - groups = [ - group for group in context.blend_data.node_groups - if (group.bl_idname == node_tree.bl_idname and - not group.contains_tree(node_tree) and - (show_hidden or not group.name.startswith('.'))) - ] - if groups: - layout.separator() - for group in groups: - props = add_node_type(layout, node_tree_group_type[group.bl_idname], label=group.name) - ops = props.settings.add() - ops.name = "node_tree" - ops.value = "bpy.data.node_groups[{!r}]".format(group.name) - ops = props.settings.add() - ops.name = "width" - ops.value = repr(group.default_group_node_width) - ops = props.settings.add() - ops.name = "name" - ops.value = repr(group.name) - - -def draw_assets_for_catalog(layout, catalog_path): - layout.template_node_asset_menu_items(catalog_path=catalog_path) - - -def draw_root_assets(layout): - layout.menu_contents("NODE_MT_node_add_root_catalogs") + return AddNodeMenu.node_operator( + layout, + node_type, + label=label, + poll=poll, + search_weight=search_weight, + translate=translate) def add_node_type_with_searchable_enum(context, layout, node_idname, property_name, search_weight=0.0): - add_node_type(layout, node_idname, search_weight=search_weight) - if getattr(context, "is_menu_search", False): - node_type = getattr(bpy.types, node_idname) - translation_context = node_type.bl_rna.properties[property_name].translation_context - for item in node_type.bl_rna.properties[property_name].enum_items_static: - props = add_node_type( - layout, - node_idname, - label="{:s} \u25B8 {:s}".format(iface_(node_type.bl_rna.name), iface_(item.name, translation_context)), - translate=False, - search_weight=search_weight, - ) - prop = props.settings.add() - prop.name = property_name - prop.value = repr(item.identifier) + return AddNodeMenu.node_operator_with_searchable_enum(context, layout, node_idname, property_name, search_weight) def add_node_type_with_searchable_enum_socket( @@ -139,45 +47,32 @@ def add_node_type_with_searchable_enum_socket( node_idname, socket_identifier, enum_names, - search_weight=0.0, -): - add_node_type(layout, node_idname, search_weight=search_weight) - if getattr(context, "is_menu_search", False): - node_type = getattr(bpy.types, node_idname) - for enum_name in enum_names: - props = add_node_type( - layout, - node_idname, - label="{:s} \u25B8 {:s}".format(iface_(node_type.bl_rna.name), iface_(enum_name)), - translate=False, - search_weight=search_weight) - prop = props.settings.add() - prop.name = f'inputs["{socket_identifier}"].default_value' - prop.value = repr(enum_name) + search_weight=0.0): + return AddNodeMenu.node_operator_with_searchable_enum_socket( + context, layout, node_idname, socket_identifier, enum_names, search_weight) + + +def add_node_type_with_outputs(context, layout, node_type, subnames, *, label=None, search_weight=0.0): + return AddNodeMenu.node_operator_with_outputs( + context, + layout, + node_type, + subnames, + label=label, + search_weight=search_weight) def add_color_mix_node(context, layout): - label = iface_("Mix Color") - props = node_add_menu.add_node_type(layout, "ShaderNodeMix", label=label, translate=False) - ops = props.settings.add() - ops.name = "data_type" - ops.value = "'RGBA'" + return AddNodeMenu.color_mix_node(context, layout) - if getattr(context, "is_menu_search", False): - translation_context = bpy.types.ShaderNodeMix.bl_rna.properties["blend_type"].translation_context - for item in bpy.types.ShaderNodeMix.bl_rna.properties["blend_type"].enum_items_static: - props = node_add_menu.add_node_type( - layout, - "ShaderNodeMix", - label="{:s} \u25B8 {:s}".format(label, iface_(item.name, translation_context)), - translate=False, - ) - prop = props.settings.add() - prop.name = "data_type" - prop.value = "'RGBA'" - prop = props.settings.add() - prop.name = "blend_type" - prop.value = repr(item.identifier) + +def add_empty_group(layout): + return AddNodeMenu.new_empty_group(layout) + + +def draw_node_group_add_menu(context, layout): + """Add items to the layout used for interacting with node groups.""" + return AddNodeMenu.draw_group_menu(context, layout) def add_simulation_zone(layout, label): @@ -210,26 +105,385 @@ def add_closure_zone(layout, label): return props -def add_empty_group(layout): - props = layout.operator("node.add_empty_group", text="New Group", text_ctxt=i18n_contexts.default) - props.use_transform = True - return props +class NodeMenu(Menu): + """A baseclass defining the shared methods for AddNodeMenu and SwapNodeMenu""" + draw_assets: bool + use_transform: bool + + main_operator_id: str + zone_operator_id: str + new_empty_group_operator_id: str + + root_asset_menu: str + pathing_dict: dict[str, str] + + @classmethod + def node_operator(cls, layout, node_type, *, label=None, poll=None, search_weight=0.0, translate=True): + """The main operator defined for the node menu. + \n(e.g. 'Add Node' for AddNodeMenu, or 'Swap Node' for SwapNodeMenu)""" + + bl_rna = bpy.types.Node.bl_rna_get_subclass(node_type) + if not label: + label = bl_rna.name if bl_rna else iface_("Unknown") + + if poll is True or poll is None: + translation_context = bl_rna.translation_context if bl_rna else i18n_contexts.default + props = layout.operator( + cls.main_operator_id, + text=label, + text_ctxt=translation_context, + translate=translate, + search_weight=search_weight) + props.type = node_type + + if hasattr(props, "use_transform"): + props.use_transform = cls.use_transform + + return props + + return None + + @classmethod + def node_operator_with_searchable_enum(cls, context, layout, node_idname, property_name, search_weight=0.0): + """Similar to `node_operator`, but with extra entries based on a enum property while in search""" + operators = [] + operators.append(cls.node_operator(layout, node_idname, search_weight=search_weight)) + + if getattr(context, "is_menu_search", False): + node_type = getattr(bpy.types, node_idname) + translation_context = node_type.bl_rna.properties[property_name].translation_context + for item in node_type.bl_rna.properties[property_name].enum_items_static: + props = cls.node_operator( + layout, + node_idname, + label="{:s} \u25B8 {:s}".format( + iface_( + node_type.bl_rna.name), + iface_( + item.name, + translation_context)), + translate=False, + search_weight=search_weight) + prop = props.settings.add() + prop.name = property_name + prop.value = repr(item.identifier) + operators.append(props) + + for props in operators: + if hasattr(props, "use_transform"): + props.use_transform = cls.use_transform + + return operators + + @classmethod + def node_operator_with_searchable_enum_socket( + cls, + context, + layout, + node_idname, + socket_identifier, + enum_names, + search_weight=0.0): + """Similar to `node_operator`, but with extra entries based on a enum socket while in search""" + operators = [] + operators.append(cls.node_operator(layout, node_idname, search_weight=search_weight)) + if getattr(context, "is_menu_search", False): + node_type = getattr(bpy.types, node_idname) + for enum_name in enum_names: + props = cls.node_operator( + layout, + node_idname, + label="{:s} \u25B8 {:s}".format(iface_(node_type.bl_rna.name), iface_(enum_name)), + translate=False, + search_weight=search_weight) + prop = props.settings.add() + prop.name = f'inputs["{socket_identifier}"].default_value' + prop.value = repr(enum_name) + operators.append(props) + + for props in operators: + if hasattr(props, "use_transform"): + props.use_transform = cls.use_transform + + return operators + + @classmethod + def node_operator_with_outputs(cls, context, layout, node_type, subnames, *, label=None, search_weight=0.0): + """Similar to `node_operator`, but with extra entries based on a enum socket while in search""" + bl_rna = bpy.types.Node.bl_rna_get_subclass(node_type) + if not label: + label = bl_rna.name if bl_rna else "Unknown" + + operators = [] + operators.append(cls.node_operator(layout, node_type, label=label, search_weight=search_weight)) + + if getattr(context, "is_menu_search", False): + for subname in subnames: + item_props = cls.node_operator(layout, node_type, label="{:s} \u25B8 {:s}".format( + iface_(label), iface_(subname)), search_weight=search_weight, translate=False) + item_props.visible_output = subname + operators.append(item_props) + + for props in operators: + if hasattr(props, "use_transform"): + props.use_transform = cls.use_transform + + return operators + + @classmethod + def color_mix_node(cls, context, layout): + """The 'Mix Color' node, with its different blend modes available while in search""" + label = iface_("Mix Color") + + operators = [] + props = cls.node_operator(layout, "ShaderNodeMix", label=label, translate=False) + ops = props.settings.add() + ops.name = "data_type" + ops.value = "'RGBA'" + operators.append(props) + + if getattr(context, "is_menu_search", False): + translation_context = bpy.types.ShaderNodeMix.bl_rna.properties["blend_type"].translation_context + for item in bpy.types.ShaderNodeMix.bl_rna.properties["blend_type"].enum_items_static: + props = cls.node_operator( + layout, + "ShaderNodeMix", + label="{:s} \u25B8 {:s}".format( + label, + iface_( + item.name, + translation_context)), + translate=False) + prop = props.settings.add() + prop.name = "data_type" + prop.value = "'RGBA'" + prop = props.settings.add() + prop.name = "blend_type" + prop.value = repr(item.identifier) + operators.append(props) + + for props in operators: + if hasattr(props, "use_transform"): + props.use_transform = cls.use_transform + + return operators + + @classmethod + def new_empty_group(cls, layout): + """Group Node with a newly created empty group as its assigned nodetree""" + props = layout.operator(cls.new_empty_group_operator_id, text="New Group", text_ctxt=i18n_contexts.default) + + if hasattr(props, "use_transform"): + props.use_transform = cls.use_transform + + return props + + @classmethod + def draw_group_menu(cls, context, layout): + """Show operators used for interacting with node groups""" + space_node = context.space_data + node_tree = space_node.edit_tree + all_node_groups = context.blend_data.node_groups + + if node_tree in all_node_groups.values(): + layout.separator() + cls.node_operator(layout, "NodeGroupInput") + cls.node_operator(layout, "NodeGroupOutput") + + operators = [] + operators.append(cls.new_empty_group(layout)) + + if node_tree: + from nodeitems_builtins import node_tree_group_type + + prefs = bpy.context.preferences + show_hidden = prefs.filepaths.show_hidden_files_datablocks + + groups = [ + group for group in context.blend_data.node_groups + if (group.bl_idname == node_tree.bl_idname and + not group.contains_tree(node_tree) and + (show_hidden or not group.name.startswith('.'))) + ] + if groups: + layout.separator() + for group in groups: + props = cls.node_operator(layout, node_tree_group_type[group.bl_idname], label=group.name) + ops = props.settings.add() + ops.name = "node_tree" + ops.value = "bpy.data.node_groups[{!r}]".format(group.name) + ops = props.settings.add() + ops.name = "width" + ops.value = repr(group.default_group_node_width) + ops = props.settings.add() + ops.name = "name" + ops.value = repr(group.name) + operators.append(props) + + for props in operators: + if hasattr(props, "use_transform"): + props.use_transform = cls.use_transform + + return operators + + @classmethod + def draw_menu(cls, layout, path): + """Takes the given menu path and draws the corresponding menu. + \n Menu paths are either explicitly defined, or based on bl_label if not.""" + if cls.pathing_dict is None: + raise ValueError("`pathing_dict` was not set for {}".format(cls)) + + layout.menu(cls.pathing_dict[path]) + + @classmethod + def simulation_zone(cls, layout, label): + props = layout.operator(cls.zone_operator_id, text=label) + props.input_node_type = "GeometryNodeSimulationInput" + props.output_node_type = "GeometryNodeSimulationOutput" + props.add_default_geometry_link = True + + if hasattr(props, "use_transform"): + props.use_transform = cls.use_transform + + return props + + @classmethod + def repeat_zone(cls, layout, label): + props = layout.operator(cls.zone_operator_id, text=label) + props.input_node_type = "GeometryNodeRepeatInput" + props.output_node_type = "GeometryNodeRepeatOutput" + props.add_default_geometry_link = True + + if hasattr(props, "use_transform"): + props.use_transform = cls.use_transform + + return props + + @classmethod + def for_each_element_zone(cls, layout, label): + props = layout.operator(cls.zone_operator_id, text=label) + props.input_node_type = "GeometryNodeForeachGeometryElementInput" + props.output_node_type = "GeometryNodeForeachGeometryElementOutput" + props.add_default_geometry_link = False + + if hasattr(props, "use_transform"): + props.use_transform = cls.use_transform + + return props + + @classmethod + def closure_zone(cls, layout, label): + props = layout.operator(cls.zone_operator_id, text=label) + props.input_node_type = "NodeClosureInput" + props.output_node_type = "NodeClosureOutput" + props.add_default_geometry_link = False + + if hasattr(props, "use_transform"): + props.use_transform = cls.use_transform + + return props + + @classmethod + def draw_root_assets(cls, layout): + if cls.draw_assets: + layout.menu_contents(cls.root_asset_menu) -class NODE_MT_category_layout(Menu): - bl_idname = "NODE_MT_category_layout" +class AddNodeMenu(NodeMenu): + draw_assets = True + use_transform = True + + main_operator_id = "node.add_node" + zone_operator_id = "node.add_zone" + new_empty_group_operator_id = "node.add_empty_group" + + root_asset_menu = "NODE_MT_node_add_root_catalogs" + + @classmethod + def draw_assets_for_catalog(cls, layout, catalog_path): + if cls.draw_assets: + layout.template_node_asset_menu_items(catalog_path=catalog_path, operator='ADD') + + +class SwapNodeMenu(NodeMenu): + draw_assets = True + # NOTE: Swap operators don't have a `use_transform` property, so defining it here has no effect + + main_operator_id = "node.swap_node" + zone_operator_id = "node.swap_zone" + new_empty_group_operator_id = "node.swap_empty_group" + + root_asset_menu = "NODE_MT_node_swap_root_catalogs" + + @classmethod + def draw_assets_for_catalog(cls, layout, catalog_path): + if cls.draw_assets: + layout.template_node_asset_menu_items(catalog_path=catalog_path, operator='SWAP') + + +class NODE_MT_group_base(NodeMenu): + bl_label = "Group" + + def draw(self, context): + layout = self.layout + self.draw_group_menu(context, layout) + + self.draw_assets_for_catalog(layout, self.bl_label) + + +class NODE_MT_layout_base(NodeMenu): bl_label = "Layout" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "NodeFrame", search_weight=-1) - node_add_menu.add_node_type(layout, "NodeReroute") + self.node_operator(layout, "NodeFrame", search_weight=-1) + self.node_operator(layout, "NodeReroute") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.draw_assets_for_catalog(layout, self.bl_label) + + +add_base_pathing_dict = { + "Group": "NODE_MT_group_add", + "Layout": "NODE_MT_category_layout", +} + + +swap_base_pathing_dict = { + "Group": "NODE_MT_group_swap", + "Layout": "NODE_MT_layout_swap", +} + + +def generate_menu(bl_idname: str, template: Menu, layout_base: Menu, pathing_dict: dict = None): + return type(bl_idname, (template, layout_base), {"bl_idname": bl_idname, "pathing_dict": pathing_dict}) + + +def generate_menus(menus: dict, template: Menu, base_dict: dict): + import copy + pathing_dict = copy.copy(base_dict) + menus = tuple( + generate_menu(bl_idname, template, layout_base, pathing_dict) + for bl_idname, layout_base in menus.items() + ) + generate_pathing_dict(pathing_dict, menus) + return menus + + +def generate_pathing_dict(pathing_dict, menus): + for menu in menus: + if hasattr(menu, "menu_path"): + menu_path = menu.menu_path + else: + menu_path = menu.bl_label + + pathing_dict[menu_path] = menu.bl_idname classes = ( - NODE_MT_category_layout, + generate_menu("NODE_MT_group_add", template=AddNodeMenu, layout_base=NODE_MT_group_base), + generate_menu("NODE_MT_group_swap", template=SwapNodeMenu, layout_base=NODE_MT_group_base), + generate_menu("NODE_MT_category_layout", template=AddNodeMenu, layout_base=NODE_MT_layout_base), + generate_menu("NODE_MT_layout_swap", template=SwapNodeMenu, layout_base=NODE_MT_layout_base), ) if __name__ == "__main__": # only for live edit. diff --git a/scripts/startup/bl_ui/node_add_menu_compositor.py b/scripts/startup/bl_ui/node_add_menu_compositor.py index a3e06dda090..c384f0667e0 100644 --- a/scripts/startup/bl_ui/node_add_menu_compositor.py +++ b/scripts/startup/bl_ui/node_add_menu_compositor.py @@ -9,373 +9,395 @@ from bpy.app.translations import ( ) -class NODE_MT_category_compositor_input(Menu): - bl_idname = "NODE_MT_category_compositor_input" +class NODE_MT_compositor_node_input_base(node_add_menu.NodeMenu): bl_label = "Input" def draw(self, context): del context layout = self.layout - layout.menu("NODE_MT_category_compositor_input_constant") + self.draw_menu(layout, path="Input/Constant") layout.separator() - node_add_menu.add_node_type(layout, "NodeGroupInput") - node_add_menu.add_node_type(layout, "CompositorNodeBokehImage") - node_add_menu.add_node_type(layout, "CompositorNodeImage") - node_add_menu.add_node_type(layout, "CompositorNodeImageInfo") - node_add_menu.add_node_type(layout, "CompositorNodeImageCoordinates") - node_add_menu.add_node_type(layout, "CompositorNodeMask") - node_add_menu.add_node_type(layout, "CompositorNodeMovieClip") + self.node_operator(layout, "NodeGroupInput") + self.node_operator(layout, "CompositorNodeBokehImage") + self.node_operator(layout, "CompositorNodeImage") + self.node_operator(layout, "CompositorNodeImageInfo") + self.node_operator(layout, "CompositorNodeImageCoordinates") + self.node_operator(layout, "CompositorNodeMask") + self.node_operator(layout, "CompositorNodeMovieClip") layout.separator() - layout.menu("NODE_MT_category_compositor_input_scene") + self.draw_menu(layout, path="Input/Scene") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_category_compositor_input_constant(Menu): - bl_idname = "NODE_MT_category_compositor_input_constant" +class NODE_MT_compositor_node_input_constant_base(node_add_menu.NodeMenu): bl_label = "Constant" + menu_path = "Input/Constant" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "CompositorNodeRGB") - node_add_menu.add_node_type(layout, "ShaderNodeValue") - node_add_menu.add_node_type(layout, "CompositorNodeNormal") + self.node_operator(layout, "CompositorNodeRGB") + self.node_operator(layout, "ShaderNodeValue") + self.node_operator(layout, "CompositorNodeNormal") - node_add_menu.draw_assets_for_catalog(layout, "Input/Constant") + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_category_compositor_input_scene(Menu): - bl_idname = "NODE_MT_category_compositor_input_scene" +class NODE_MT_compositor_node_input_scene_base(node_add_menu.NodeMenu): bl_label = "Scene" + menu_path = "Input/Scene" def draw(self, context): layout = self.layout - node_add_menu.add_node_type(layout, "CompositorNodeRLayers") - node_add_menu.add_node_type_with_outputs(context, layout, "CompositorNodeSceneTime", ["Frame", "Seconds"]) - node_add_menu.add_node_type(layout, "CompositorNodeTime") + self.node_operator(layout, "CompositorNodeRLayers") + self.node_operator_with_outputs(context, layout, "CompositorNodeSceneTime", ["Frame", "Seconds"]) + self.node_operator(layout, "CompositorNodeTime") - node_add_menu.draw_assets_for_catalog(layout, "Input/Scene") + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_category_compositor_output(Menu): - bl_idname = "NODE_MT_category_compositor_output" +class NODE_MT_compositor_node_output_base(node_add_menu.NodeMenu): bl_label = "Output" def draw(self, context): del context layout = self.layout - node_add_menu.add_node_type(layout, "NodeGroupOutput") - node_add_menu.add_node_type(layout, "CompositorNodeViewer") + self.node_operator(layout, "NodeGroupOutput") + self.node_operator(layout, "CompositorNodeViewer") layout.separator() - node_add_menu.add_node_type(layout, "CompositorNodeOutputFile") + self.node_operator(layout, "CompositorNodeOutputFile") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_category_compositor_color(Menu): - bl_idname = "NODE_MT_category_compositor_color" +class NODE_MT_compositor_node_color_base(node_add_menu.NodeMenu): bl_label = "Color" def draw(self, _context): layout = self.layout - layout.menu("NODE_MT_category_compositor_color_adjust") + self.draw_menu(layout, path="Color/Adjust") layout.separator() - layout.menu("NODE_MT_category_compositor_color_mix") + self.draw_menu(layout, path="Color/Mix") layout.separator() - node_add_menu.add_node_type(layout, "CompositorNodePremulKey") - node_add_menu.add_node_type(layout, "ShaderNodeBlackbody") - node_add_menu.add_node_type(layout, "ShaderNodeValToRGB") - node_add_menu.add_node_type(layout, "CompositorNodeConvertColorSpace") - node_add_menu.add_node_type(layout, "CompositorNodeConvertToDisplay") - node_add_menu.add_node_type(layout, "CompositorNodeSetAlpha") + self.node_operator(layout, "CompositorNodePremulKey") + self.node_operator(layout, "ShaderNodeBlackbody") + self.node_operator(layout, "ShaderNodeValToRGB") + self.node_operator(layout, "CompositorNodeConvertColorSpace") + self.node_operator(layout, "CompositorNodeConvertToDisplay") + self.node_operator(layout, "CompositorNodeSetAlpha") layout.separator() - node_add_menu.add_node_type(layout, "CompositorNodeInvert") - node_add_menu.add_node_type(layout, "CompositorNodeRGBToBW") + self.node_operator(layout, "CompositorNodeInvert") + self.node_operator(layout, "CompositorNodeRGBToBW") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_category_compositor_color_adjust(Menu): - bl_idname = "NODE_MT_category_compositor_color_adjust" +class NODE_MT_compositor_node_color_adjust_base(node_add_menu.NodeMenu): bl_label = "Adjust" + menu_path = "Color/Adjust" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "CompositorNodeBrightContrast") - node_add_menu.add_node_type(layout, "CompositorNodeColorBalance") - node_add_menu.add_node_type(layout, "CompositorNodeColorCorrection") - node_add_menu.add_node_type(layout, "CompositorNodeExposure") - node_add_menu.add_node_type(layout, "ShaderNodeGamma") - node_add_menu.add_node_type(layout, "CompositorNodeHueCorrect") - node_add_menu.add_node_type(layout, "CompositorNodeHueSat") - node_add_menu.add_node_type(layout, "CompositorNodeCurveRGB") - node_add_menu.add_node_type(layout, "CompositorNodeTonemap") + self.node_operator(layout, "CompositorNodeBrightContrast") + self.node_operator(layout, "CompositorNodeColorBalance") + self.node_operator(layout, "CompositorNodeColorCorrection") + self.node_operator(layout, "CompositorNodeExposure") + self.node_operator(layout, "ShaderNodeGamma") + self.node_operator(layout, "CompositorNodeHueCorrect") + self.node_operator(layout, "CompositorNodeHueSat") + self.node_operator(layout, "CompositorNodeCurveRGB") + self.node_operator(layout, "CompositorNodeTonemap") - node_add_menu.draw_assets_for_catalog(layout, "Color/Adjust") + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_category_compositor_color_mix(Menu): - bl_idname = "NODE_MT_category_compositor_color_mix" +class NODE_MT_compositor_node_color_mix_base(node_add_menu.NodeMenu): bl_label = "Mix" + menu_path = "Color/Mix" def draw(self, context): layout = self.layout - node_add_menu.add_node_type(layout, "CompositorNodeAlphaOver") + self.node_operator(layout, "CompositorNodeAlphaOver") layout.separator() - node_add_menu.add_node_type(layout, "CompositorNodeCombineColor") - node_add_menu.add_node_type(layout, "CompositorNodeSeparateColor") + self.node_operator(layout, "CompositorNodeCombineColor") + self.node_operator(layout, "CompositorNodeSeparateColor") layout.separator() - node_add_menu.add_node_type(layout, "CompositorNodeZcombine") - node_add_menu.add_color_mix_node(context, layout) - node_add_menu.draw_assets_for_catalog(layout, "Color/Mix") + self.node_operator(layout, "CompositorNodeZcombine") + self.color_mix_node(context, layout) + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_category_compositor_filter(Menu): - bl_idname = "NODE_MT_category_compositor_filter" +class NODE_MT_compositor_node_filter_base(node_add_menu.NodeMenu): bl_label = "Filter" def draw(self, context): layout = self.layout - layout.menu("NODE_MT_category_compositor_filter_blur") + self.draw_menu(layout, path="Filter/Blur") layout.separator() - node_add_menu.add_node_type(layout, "CompositorNodeAntiAliasing") - node_add_menu.add_node_type(layout, "CompositorNodeConvolve") - node_add_menu.add_node_type(layout, "CompositorNodeDenoise") - node_add_menu.add_node_type(layout, "CompositorNodeDespeckle") + self.node_operator(layout, "CompositorNodeAntiAliasing") + self.node_operator(layout, "CompositorNodeConvolve") + self.node_operator(layout, "CompositorNodeDenoise") + self.node_operator(layout, "CompositorNodeDespeckle") layout.separator() - node_add_menu.add_node_type(layout, "CompositorNodeDilateErode") - node_add_menu.add_node_type(layout, "CompositorNodeInpaint") + self.node_operator(layout, "CompositorNodeDilateErode") + self.node_operator(layout, "CompositorNodeInpaint") layout.separator() - node_add_menu.add_node_type_with_searchable_enum_socket( + self.node_operator_with_searchable_enum_socket( context, layout, "CompositorNodeFilter", "Type", [ "Soften", "Box Sharpen", "Diamond Sharpen", "Laplace", "Sobel", "Prewitt", "Kirsch", "Shadow"]) - node_add_menu.add_node_type_with_searchable_enum_socket( + self.node_operator_with_searchable_enum_socket( context, layout, "CompositorNodeGlare", "Type", [ "Bloom", "Ghosts", "Streaks", "Fog Glow", "Simple Star", "Sun Beams", "Kernel"]) - node_add_menu.add_node_type(layout, "CompositorNodeKuwahara") - node_add_menu.add_node_type(layout, "CompositorNodePixelate") - node_add_menu.add_node_type(layout, "CompositorNodePosterize") + self.node_operator(layout, "CompositorNodeKuwahara") + self.node_operator(layout, "CompositorNodePixelate") + self.node_operator(layout, "CompositorNodePosterize") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_category_compositor_filter_blur(Menu): - bl_idname = "NODE_MT_category_compositor_filter_blur" +class NODE_MT_compositor_node_filter_blur_base(node_add_menu.NodeMenu): bl_label = "Blur" + menu_path = "Filter/Blur" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "CompositorNodeBilateralblur") - node_add_menu.add_node_type(layout, "CompositorNodeBlur") - node_add_menu.add_node_type(layout, "CompositorNodeBokehBlur") - node_add_menu.add_node_type(layout, "CompositorNodeDefocus") - node_add_menu.add_node_type(layout, "CompositorNodeDBlur") - node_add_menu.add_node_type(layout, "CompositorNodeVecBlur") + self.node_operator(layout, "CompositorNodeBilateralblur") + self.node_operator(layout, "CompositorNodeBlur") + self.node_operator(layout, "CompositorNodeBokehBlur") + self.node_operator(layout, "CompositorNodeDefocus") + self.node_operator(layout, "CompositorNodeDBlur") + self.node_operator(layout, "CompositorNodeVecBlur") - node_add_menu.draw_assets_for_catalog(layout, "Filter/Blur") + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_category_compositor_group(Menu): - bl_idname = "NODE_MT_category_compositor_group" - bl_label = "Group" - - def draw(self, context): - layout = self.layout - node_add_menu.draw_node_group_add_menu(context, layout) - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) - - -class NODE_MT_category_compositor_keying(Menu): - bl_idname = "NODE_MT_category_compositor_keying" +class NODE_MT_compositor_node_keying_base(node_add_menu.NodeMenu): bl_label = "Keying" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "CompositorNodeChannelMatte") - node_add_menu.add_node_type(layout, "CompositorNodeChromaMatte") - node_add_menu.add_node_type(layout, "CompositorNodeColorMatte") - node_add_menu.add_node_type(layout, "CompositorNodeColorSpill") - node_add_menu.add_node_type(layout, "CompositorNodeDiffMatte") - node_add_menu.add_node_type(layout, "CompositorNodeDistanceMatte") - node_add_menu.add_node_type(layout, "CompositorNodeKeying") - node_add_menu.add_node_type(layout, "CompositorNodeKeyingScreen") - node_add_menu.add_node_type(layout, "CompositorNodeLumaMatte") + self.node_operator(layout, "CompositorNodeChannelMatte") + self.node_operator(layout, "CompositorNodeChromaMatte") + self.node_operator(layout, "CompositorNodeColorMatte") + self.node_operator(layout, "CompositorNodeColorSpill") + self.node_operator(layout, "CompositorNodeDiffMatte") + self.node_operator(layout, "CompositorNodeDistanceMatte") + self.node_operator(layout, "CompositorNodeKeying") + self.node_operator(layout, "CompositorNodeKeyingScreen") + self.node_operator(layout, "CompositorNodeLumaMatte") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_category_compositor_mask(Menu): - bl_idname = "NODE_MT_category_compositor_mask" +class NODE_MT_compositor_node_mask_base(node_add_menu.NodeMenu): bl_label = "Mask" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "CompositorNodeCryptomatteV2") - node_add_menu.add_node_type(layout, "CompositorNodeCryptomatte") + self.node_operator(layout, "CompositorNodeCryptomatteV2") + self.node_operator(layout, "CompositorNodeCryptomatte") layout.separator() - node_add_menu.add_node_type(layout, "CompositorNodeBoxMask") - node_add_menu.add_node_type(layout, "CompositorNodeEllipseMask") + self.node_operator(layout, "CompositorNodeBoxMask") + self.node_operator(layout, "CompositorNodeEllipseMask") layout.separator() - node_add_menu.add_node_type(layout, "CompositorNodeDoubleEdgeMask") - node_add_menu.add_node_type(layout, "CompositorNodeIDMask") + self.node_operator(layout, "CompositorNodeDoubleEdgeMask") + self.node_operator(layout, "CompositorNodeIDMask") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_category_compositor_tracking(Menu): - bl_idname = "NODE_MT_category_compositor_tracking" +class NODE_MT_compositor_node_tracking_base(node_add_menu.NodeMenu): bl_label = "Tracking" bl_translation_context = i18n_contexts.id_movieclip def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "CompositorNodePlaneTrackDeform") - node_add_menu.add_node_type(layout, "CompositorNodeStabilize") - node_add_menu.add_node_type(layout, "CompositorNodeTrackPos") + self.node_operator(layout, "CompositorNodePlaneTrackDeform") + self.node_operator(layout, "CompositorNodeStabilize") + self.node_operator(layout, "CompositorNodeTrackPos") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_category_compositor_transform(Menu): - bl_idname = "NODE_MT_category_compositor_transform" +class NODE_MT_compositor_node_transform_base(node_add_menu.NodeMenu): bl_label = "Transform" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "CompositorNodeRotate") - node_add_menu.add_node_type(layout, "CompositorNodeScale") - node_add_menu.add_node_type(layout, "CompositorNodeTransform") - node_add_menu.add_node_type(layout, "CompositorNodeTranslate") + self.node_operator(layout, "CompositorNodeRotate") + self.node_operator(layout, "CompositorNodeScale") + self.node_operator(layout, "CompositorNodeTransform") + self.node_operator(layout, "CompositorNodeTranslate") layout.separator() - node_add_menu.add_node_type(layout, "CompositorNodeCornerPin") - node_add_menu.add_node_type(layout, "CompositorNodeCrop") + self.node_operator(layout, "CompositorNodeCornerPin") + self.node_operator(layout, "CompositorNodeCrop") layout.separator() - node_add_menu.add_node_type(layout, "CompositorNodeDisplace") - node_add_menu.add_node_type(layout, "CompositorNodeFlip") - node_add_menu.add_node_type(layout, "CompositorNodeMapUV") + self.node_operator(layout, "CompositorNodeDisplace") + self.node_operator(layout, "CompositorNodeFlip") + self.node_operator(layout, "CompositorNodeMapUV") layout.separator() - node_add_menu.add_node_type(layout, "CompositorNodeLensdist") - node_add_menu.add_node_type(layout, "CompositorNodeMovieDistortion") + self.node_operator(layout, "CompositorNodeLensdist") + self.node_operator(layout, "CompositorNodeMovieDistortion") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_category_compositor_texture(Menu): - bl_idname = "NODE_MT_category_compositor_texture" +class NODE_MT_compositor_node_texture_base(node_add_menu.NodeMenu): bl_label = "Texture" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "ShaderNodeTexBrick") - node_add_menu.add_node_type(layout, "ShaderNodeTexChecker") - node_add_menu.add_node_type(layout, "ShaderNodeTexGabor") - node_add_menu.add_node_type(layout, "ShaderNodeTexGradient") - node_add_menu.add_node_type(layout, "ShaderNodeTexMagic") - node_add_menu.add_node_type(layout, "ShaderNodeTexNoise") - node_add_menu.add_node_type(layout, "ShaderNodeTexVoronoi") - node_add_menu.add_node_type(layout, "ShaderNodeTexWave") - node_add_menu.add_node_type(layout, "ShaderNodeTexWhiteNoise") + self.node_operator(layout, "ShaderNodeTexBrick") + self.node_operator(layout, "ShaderNodeTexChecker") + self.node_operator(layout, "ShaderNodeTexGabor") + self.node_operator(layout, "ShaderNodeTexGradient") + self.node_operator(layout, "ShaderNodeTexMagic") + self.node_operator(layout, "ShaderNodeTexNoise") + self.node_operator(layout, "ShaderNodeTexVoronoi") + self.node_operator(layout, "ShaderNodeTexWave") + self.node_operator(layout, "ShaderNodeTexWhiteNoise") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_category_compositor_utilities(Menu): - bl_idname = "NODE_MT_category_compositor_utilities" +class NODE_MT_compositor_node_utilities_base(node_add_menu.NodeMenu): bl_label = "Utilities" def draw(self, context): layout = self.layout - node_add_menu.add_node_type(layout, "ShaderNodeMapRange") - node_add_menu.add_node_type_with_searchable_enum(context, layout, "ShaderNodeMath", "operation") - node_add_menu.add_node_type(layout, "ShaderNodeMix") - node_add_menu.add_node_type(layout, "ShaderNodeClamp") - node_add_menu.add_node_type(layout, "ShaderNodeFloatCurve") + self.node_operator(layout, "ShaderNodeMapRange") + self.node_operator_with_searchable_enum(context, layout, "ShaderNodeMath", "operation") + self.node_operator(layout, "ShaderNodeMix") + self.node_operator(layout, "ShaderNodeClamp") + self.node_operator(layout, "ShaderNodeFloatCurve") layout.separator() - node_add_menu.add_node_type(layout, "CompositorNodeLevels") - node_add_menu.add_node_type(layout, "CompositorNodeNormalize") + self.node_operator(layout, "CompositorNodeLevels") + self.node_operator(layout, "CompositorNodeNormalize") layout.separator() - node_add_menu.add_node_type(layout, "CompositorNodeSplit") - node_add_menu.add_node_type(layout, "CompositorNodeSwitch") - node_add_menu.add_node_type(layout, "GeometryNodeMenuSwitch") - node_add_menu.add_node_type( + self.node_operator(layout, "CompositorNodeSplit") + self.node_operator(layout, "CompositorNodeSwitch") + self.node_operator(layout, "GeometryNodeMenuSwitch") + self.node_operator( layout, "CompositorNodeSwitchView", label="Switch Stereo View") layout.separator() - node_add_menu.add_node_type(layout, "CompositorNodeRelativeToPixel") + self.node_operator(layout, "CompositorNodeRelativeToPixel") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_category_compositor_vector(Menu): - bl_idname = "NODE_MT_category_compositor_vector" +class NODE_MT_compositor_node_vector_base(node_add_menu.NodeMenu): bl_label = "Vector" def draw(self, context): layout = self.layout - node_add_menu.add_node_type(layout, "ShaderNodeCombineXYZ") - node_add_menu.add_node_type(layout, "ShaderNodeSeparateXYZ") + self.node_operator(layout, "ShaderNodeCombineXYZ") + self.node_operator(layout, "ShaderNodeSeparateXYZ") layout.separator() - props = node_add_menu.add_node_type(layout, "ShaderNodeMix", label="Mix Vector") + props = self.node_operator(layout, "ShaderNodeMix", label="Mix Vector") ops = props.settings.add() ops.name = "data_type" ops.value = "'VECTOR'" - node_add_menu.add_node_type(layout, "ShaderNodeRadialTiling") - node_add_menu.add_node_type(layout, "ShaderNodeVectorCurve") - node_add_menu.add_node_type_with_searchable_enum(context, layout, "ShaderNodeVectorMath", "operation") - node_add_menu.add_node_type(layout, "ShaderNodeVectorRotate") + self.node_operator(layout, "ShaderNodeRadialTiling") + self.node_operator(layout, "ShaderNodeVectorCurve") + self.node_operator_with_searchable_enum(context, layout, "ShaderNodeVectorMath", "operation") + self.node_operator(layout, "ShaderNodeVectorRotate") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_compositor_node_add_all(Menu): - bl_idname = "NODE_MT_compositor_node_add_all" +class NODE_MT_compositor_node_all_base(node_add_menu.NodeMenu): bl_label = "" + menu_path = "Root" + bl_translation_context = i18n_contexts.operator_default + # NOTE: Menus are looked up via their label, this is so that both the Add + # & Swap menus can share the same layout while each using their + # corresponding menus def draw(self, context): layout = self.layout - layout.menu("NODE_MT_category_compositor_input") - layout.menu("NODE_MT_category_compositor_output") + self.draw_menu(layout, "Input") + self.draw_menu(layout, "Output") layout.separator() - layout.menu("NODE_MT_category_compositor_color") - layout.menu("NODE_MT_category_compositor_filter") + self.draw_menu(layout, "Color") + self.draw_menu(layout, "Filter") layout.separator() - layout.menu("NODE_MT_category_compositor_keying") - layout.menu("NODE_MT_category_compositor_mask") + self.draw_menu(layout, "Keying") + self.draw_menu(layout, "Mask") layout.separator() - layout.menu("NODE_MT_category_compositor_tracking") + self.draw_menu(layout, "Tracking") layout.separator() - layout.menu("NODE_MT_category_compositor_texture") - layout.menu("NODE_MT_category_compositor_transform") - layout.menu("NODE_MT_category_compositor_utilities") - layout.menu("NODE_MT_category_compositor_vector") + self.draw_menu(layout, "Texture") + self.draw_menu(layout, "Transform") + self.draw_menu(layout, "Utilities") + self.draw_menu(layout, "Vector") layout.separator() - layout.menu("NODE_MT_category_compositor_group") - layout.menu("NODE_MT_category_layout") + self.draw_menu(layout, "Group") + self.draw_menu(layout, "Layout") - node_add_menu.draw_root_assets(layout) + self.draw_root_assets(layout) + + +add_menus = { + # menu bl_idname: baseclass + "NODE_MT_category_compositor_input": NODE_MT_compositor_node_input_base, + "NODE_MT_category_compositor_input_constant": NODE_MT_compositor_node_input_constant_base, + "NODE_MT_category_compositor_input_scene": NODE_MT_compositor_node_input_scene_base, + "NODE_MT_category_compositor_output": NODE_MT_compositor_node_output_base, + "NODE_MT_category_compositor_color": NODE_MT_compositor_node_color_base, + "NODE_MT_category_compositor_color_adjust": NODE_MT_compositor_node_color_adjust_base, + "NODE_MT_category_compositor_color_mix": NODE_MT_compositor_node_color_mix_base, + "NODE_MT_category_compositor_filter": NODE_MT_compositor_node_filter_base, + "NODE_MT_category_compositor_filter_blur": NODE_MT_compositor_node_filter_blur_base, + "NODE_MT_category_compositor_texture": NODE_MT_compositor_node_texture_base, + "NODE_MT_category_compositor_keying": NODE_MT_compositor_node_keying_base, + "NODE_MT_category_compositor_mask": NODE_MT_compositor_node_mask_base, + "NODE_MT_category_compositor_tracking": NODE_MT_compositor_node_tracking_base, + "NODE_MT_category_compositor_transform": NODE_MT_compositor_node_transform_base, + "NODE_MT_category_compositor_utilities": NODE_MT_compositor_node_utilities_base, + "NODE_MT_category_compositor_vector": NODE_MT_compositor_node_vector_base, + "NODE_MT_compositor_node_add_all": NODE_MT_compositor_node_all_base, +} +add_menus = node_add_menu.generate_menus( + add_menus, + template=node_add_menu.AddNodeMenu, + base_dict=node_add_menu.add_base_pathing_dict +) + + +swap_menus = { + # menu bl_idname: baseclass + "NODE_MT_compositor_node_input_swap": NODE_MT_compositor_node_input_base, + "NODE_MT_compositor_node_input_constant_swap": NODE_MT_compositor_node_input_constant_base, + "NODE_MT_compositor_node_input_scene_swap": NODE_MT_compositor_node_input_scene_base, + "NODE_MT_compositor_node_output_swap": NODE_MT_compositor_node_output_base, + "NODE_MT_compositor_node_color_swap": NODE_MT_compositor_node_color_base, + "NODE_MT_compositor_node_color_adjust_swap": NODE_MT_compositor_node_color_adjust_base, + "NODE_MT_compositor_node_color_mix_swap": NODE_MT_compositor_node_color_mix_base, + "NODE_MT_compositor_node_filter_swap": NODE_MT_compositor_node_filter_base, + "NODE_MT_compositor_node_filter_blur_swap": NODE_MT_compositor_node_filter_blur_base, + "NODE_MT_compositor_node_texture_swap": NODE_MT_compositor_node_texture_base, + "NODE_MT_compositor_node_keying_swap": NODE_MT_compositor_node_keying_base, + "NODE_MT_compositor_node_mask_swap": NODE_MT_compositor_node_mask_base, + "NODE_MT_compositor_node_tracking_swap": NODE_MT_compositor_node_tracking_base, + "NODE_MT_compositor_node_transform_swap": NODE_MT_compositor_node_transform_base, + "NODE_MT_compositor_node_utilities_swap": NODE_MT_compositor_node_utilities_base, + "NODE_MT_compositor_node_vector_swap": NODE_MT_compositor_node_vector_base, + "NODE_MT_compositor_node_swap_all": NODE_MT_compositor_node_all_base, +} +swap_menus = node_add_menu.generate_menus( + swap_menus, + template=node_add_menu.SwapNodeMenu, + base_dict=node_add_menu.swap_base_pathing_dict +) classes = ( - NODE_MT_compositor_node_add_all, - NODE_MT_category_compositor_input, - NODE_MT_category_compositor_input_constant, - NODE_MT_category_compositor_input_scene, - NODE_MT_category_compositor_output, - NODE_MT_category_compositor_color, - NODE_MT_category_compositor_color_adjust, - NODE_MT_category_compositor_color_mix, - NODE_MT_category_compositor_filter, - NODE_MT_category_compositor_filter_blur, - NODE_MT_category_compositor_texture, - NODE_MT_category_compositor_keying, - NODE_MT_category_compositor_mask, - NODE_MT_category_compositor_tracking, - NODE_MT_category_compositor_transform, - NODE_MT_category_compositor_utilities, - NODE_MT_category_compositor_vector, - NODE_MT_category_compositor_group, + *add_menus, + *swap_menus, ) if __name__ == "__main__": # only for live edit. diff --git a/scripts/startup/bl_ui/node_add_menu_geometry.py b/scripts/startup/bl_ui/node_add_menu_geometry.py index 3f54b65fc9b..d09695e0687 100644 --- a/scripts/startup/bl_ui/node_add_menu_geometry.py +++ b/scripts/startup/bl_ui/node_add_menu_geometry.py @@ -10,336 +10,352 @@ from bpy.app.translations import ( ) -class NODE_MT_geometry_node_GEO_ATTRIBUTE(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_ATTRIBUTE" +class NODE_MT_gn_attribute_base(node_add_menu.NodeMenu): bl_label = "Attribute" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeAttributeStatistic") - node_add_menu.add_node_type(layout, "GeometryNodeAttributeDomainSize") + self.node_operator(layout, "GeometryNodeAttributeStatistic") + self.node_operator(layout, "GeometryNodeAttributeDomainSize") layout.separator() - node_add_menu.add_node_type(layout, "GeometryNodeBlurAttribute") - node_add_menu.add_node_type(layout, "GeometryNodeCaptureAttribute") - node_add_menu.add_node_type(layout, "GeometryNodeRemoveAttribute") - node_add_menu.add_node_type(layout, "GeometryNodeStoreNamedAttribute", search_weight=1.0) - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.node_operator(layout, "GeometryNodeBlurAttribute") + self.node_operator(layout, "GeometryNodeCaptureAttribute") + self.node_operator(layout, "GeometryNodeRemoveAttribute") + self.node_operator(layout, "GeometryNodeStoreNamedAttribute", search_weight=1.0) + + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_geometry_node_GEO_COLOR(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_COLOR" +class NODE_MT_gn_utilities_color_base(node_add_menu.NodeMenu): bl_label = "Color" + menu_path = "Utilities/Color" def draw(self, context): layout = self.layout - node_add_menu.add_node_type(layout, "ShaderNodeBlackbody") - node_add_menu.add_node_type(layout, "ShaderNodeGamma") - node_add_menu.add_node_type(layout, "ShaderNodeValToRGB") - node_add_menu.add_node_type(layout, "ShaderNodeRGBCurve") + self.node_operator(layout, "ShaderNodeBlackbody") + self.node_operator(layout, "ShaderNodeGamma") + self.node_operator(layout, "ShaderNodeValToRGB") + self.node_operator(layout, "ShaderNodeRGBCurve") layout.separator() - node_add_menu.add_node_type(layout, "FunctionNodeCombineColor") - node_add_menu.add_color_mix_node(context, layout) - node_add_menu.add_node_type(layout, "FunctionNodeSeparateColor") - node_add_menu.draw_assets_for_catalog(layout, "Utilities/Color") + self.node_operator(layout, "FunctionNodeCombineColor") + self.color_mix_node(context, layout) + self.node_operator(layout, "FunctionNodeSeparateColor") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_geometry_node_GEO_CURVE(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_CURVE" +class NODE_MT_gn_curve_base(node_add_menu.NodeMenu): bl_label = "Curve" def draw(self, _context): layout = self.layout - layout.menu("NODE_MT_geometry_node_GEO_CURVE_READ") - layout.menu("NODE_MT_geometry_node_GEO_CURVE_SAMPLE") - layout.menu("NODE_MT_geometry_node_GEO_CURVE_WRITE") + self.draw_menu(layout, path="Curve/Read") + self.draw_menu(layout, path="Curve/Sample") + self.draw_menu(layout, path="Curve/Write") layout.separator() - layout.menu("NODE_MT_geometry_node_GEO_CURVE_OPERATIONS") - layout.menu("NODE_MT_geometry_node_GEO_PRIMITIVES_CURVE") - layout.menu("NODE_MT_geometry_node_curve_topology") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.draw_menu(layout, path="Curve/Operations") + self.draw_menu(layout, path="Curve/Primitives") + self.draw_menu(layout, path="Curve/Topology") + + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_geometry_node_GEO_CURVE_READ(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_CURVE_READ" +class NODE_MT_gn_curve_read_base(node_add_menu.NodeMenu): bl_label = "Read" + menu_path = "Curve/Read" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeInputCurveHandlePositions") - node_add_menu.add_node_type(layout, "GeometryNodeCurveLength") - node_add_menu.add_node_type(layout, "GeometryNodeInputTangent") - node_add_menu.add_node_type(layout, "GeometryNodeInputCurveTilt") - node_add_menu.add_node_type(layout, "GeometryNodeCurveEndpointSelection") - node_add_menu.add_node_type(layout, "GeometryNodeCurveHandleTypeSelection") - node_add_menu.add_node_type(layout, "GeometryNodeInputSplineCyclic") - node_add_menu.add_node_type(layout, "GeometryNodeSplineLength") - node_add_menu.add_node_type(layout, "GeometryNodeSplineParameter") - node_add_menu.add_node_type(layout, "GeometryNodeInputSplineResolution") - node_add_menu.draw_assets_for_catalog(layout, "Curve/Read") + self.node_operator(layout, "GeometryNodeInputCurveHandlePositions") + self.node_operator(layout, "GeometryNodeCurveLength") + self.node_operator(layout, "GeometryNodeInputTangent") + self.node_operator(layout, "GeometryNodeInputCurveTilt") + self.node_operator(layout, "GeometryNodeCurveEndpointSelection") + self.node_operator(layout, "GeometryNodeCurveHandleTypeSelection") + self.node_operator(layout, "GeometryNodeInputSplineCyclic") + self.node_operator(layout, "GeometryNodeSplineLength") + self.node_operator(layout, "GeometryNodeSplineParameter") + self.node_operator(layout, "GeometryNodeInputSplineResolution") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_geometry_node_GEO_CURVE_SAMPLE(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_CURVE_SAMPLE" +class NODE_MT_gn_curve_sample_base(node_add_menu.NodeMenu): bl_label = "Sample" + menu_path = "Curve/Sample" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeSampleCurve") - node_add_menu.draw_assets_for_catalog(layout, "Curve/Sample") + self.node_operator(layout, "GeometryNodeSampleCurve") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_geometry_node_GEO_CURVE_WRITE(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_CURVE_WRITE" +class NODE_MT_gn_curve_write_base(node_add_menu.NodeMenu): bl_label = "Write" + menu_path = "Curve/Write" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeSetCurveNormal") - node_add_menu.add_node_type(layout, "GeometryNodeSetCurveRadius") - node_add_menu.add_node_type(layout, "GeometryNodeSetCurveTilt") - node_add_menu.add_node_type(layout, "GeometryNodeSetCurveHandlePositions") - node_add_menu.add_node_type(layout, "GeometryNodeCurveSetHandles") - node_add_menu.add_node_type(layout, "GeometryNodeSetSplineCyclic") - node_add_menu.add_node_type(layout, "GeometryNodeSetSplineResolution") - node_add_menu.add_node_type(layout, "GeometryNodeCurveSplineType") - node_add_menu.draw_assets_for_catalog(layout, "Curve/Write") + self.node_operator(layout, "GeometryNodeSetCurveNormal") + self.node_operator(layout, "GeometryNodeSetCurveRadius") + self.node_operator(layout, "GeometryNodeSetCurveTilt") + self.node_operator(layout, "GeometryNodeSetCurveHandlePositions") + self.node_operator(layout, "GeometryNodeCurveSetHandles") + self.node_operator(layout, "GeometryNodeSetSplineCyclic") + self.node_operator(layout, "GeometryNodeSetSplineResolution") + self.node_operator(layout, "GeometryNodeCurveSplineType") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_geometry_node_GEO_CURVE_OPERATIONS(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_CURVE_OPERATIONS" +class NODE_MT_gn_curve_operations_base(node_add_menu.NodeMenu): bl_label = "Operations" + menu_path = "Curve/Operations" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeCurveToMesh") - node_add_menu.add_node_type(layout, "GeometryNodeCurveToPoints") - node_add_menu.add_node_type(layout, "GeometryNodeCurvesToGreasePencil") - node_add_menu.add_node_type(layout, "GeometryNodeDeformCurvesOnSurface") - node_add_menu.add_node_type(layout, "GeometryNodeFillCurve") - node_add_menu.add_node_type(layout, "GeometryNodeFilletCurve") - node_add_menu.add_node_type(layout, "GeometryNodeInterpolateCurves") - node_add_menu.add_node_type(layout, "GeometryNodeResampleCurve") - node_add_menu.add_node_type(layout, "GeometryNodeReverseCurve") - node_add_menu.add_node_type(layout, "GeometryNodeSubdivideCurve") - node_add_menu.add_node_type(layout, "GeometryNodeTrimCurve") - node_add_menu.draw_assets_for_catalog(layout, "Curve/Operations") + self.node_operator(layout, "GeometryNodeCurveToMesh") + self.node_operator(layout, "GeometryNodeCurveToPoints") + self.node_operator(layout, "GeometryNodeCurvesToGreasePencil") + self.node_operator(layout, "GeometryNodeDeformCurvesOnSurface") + self.node_operator(layout, "GeometryNodeFillCurve") + self.node_operator(layout, "GeometryNodeFilletCurve") + self.node_operator(layout, "GeometryNodeInterpolateCurves") + self.node_operator(layout, "GeometryNodeResampleCurve") + self.node_operator(layout, "GeometryNodeReverseCurve") + self.node_operator(layout, "GeometryNodeSubdivideCurve") + self.node_operator(layout, "GeometryNodeTrimCurve") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_geometry_node_GEO_PRIMITIVES_CURVE(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_PRIMITIVES_CURVE" +class NODE_MT_gn_curve_primitives_base(node_add_menu.NodeMenu): bl_label = "Primitives" + menu_path = "Curve/Primitives" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeCurveArc") - node_add_menu.add_node_type(layout, "GeometryNodeCurvePrimitiveBezierSegment") - node_add_menu.add_node_type(layout, "GeometryNodeCurvePrimitiveCircle") - node_add_menu.add_node_type(layout, "GeometryNodeCurvePrimitiveLine") - node_add_menu.add_node_type(layout, "GeometryNodeCurveSpiral") - node_add_menu.add_node_type(layout, "GeometryNodeCurveQuadraticBezier") - node_add_menu.add_node_type(layout, "GeometryNodeCurvePrimitiveQuadrilateral") - node_add_menu.add_node_type(layout, "GeometryNodeCurveStar") - node_add_menu.draw_assets_for_catalog(layout, "Curve/Primitives") + self.node_operator(layout, "GeometryNodeCurveArc") + self.node_operator(layout, "GeometryNodeCurvePrimitiveBezierSegment") + self.node_operator(layout, "GeometryNodeCurvePrimitiveCircle") + self.node_operator(layout, "GeometryNodeCurvePrimitiveLine") + self.node_operator(layout, "GeometryNodeCurveSpiral") + self.node_operator(layout, "GeometryNodeCurveQuadraticBezier") + self.node_operator(layout, "GeometryNodeCurvePrimitiveQuadrilateral") + self.node_operator(layout, "GeometryNodeCurveStar") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_geometry_node_curve_topology(Menu): - bl_idname = "NODE_MT_geometry_node_curve_topology" +class NODE_MT_gn_curve_topology_base(node_add_menu.NodeMenu): bl_label = "Topology" + menu_path = "Curve/Topology" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeCurveOfPoint") - node_add_menu.add_node_type(layout, "GeometryNodeOffsetPointInCurve") - node_add_menu.add_node_type(layout, "GeometryNodePointsOfCurve") - node_add_menu.draw_assets_for_catalog(layout, "Curve/Topology") + self.node_operator(layout, "GeometryNodeCurveOfPoint") + self.node_operator(layout, "GeometryNodeOffsetPointInCurve") + self.node_operator(layout, "GeometryNodePointsOfCurve") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_geometry_node_grease_pencil_read(Menu): - bl_idname = "NODE_MT_geometry_node_grease_pencil_read" +class NODE_MT_gn_grease_pencil_read_base(node_add_menu.NodeMenu): bl_label = "Read" + menu_path = "Grease Pencil/Read" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeInputNamedLayerSelection") - node_add_menu.draw_assets_for_catalog(layout, "Grease Pencil/Read") + self.node_operator(layout, "GeometryNodeInputNamedLayerSelection") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_geometry_node_grease_pencil_write(Menu): - bl_idname = "NODE_MT_geometry_node_grease_pencil_write" +class NODE_MT_gn_grease_pencil_write_base(node_add_menu.NodeMenu): bl_label = "Write" + menu_path = "Grease Pencil/Write" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeSetGreasePencilColor") - node_add_menu.add_node_type(layout, "GeometryNodeSetGreasePencilDepth") - node_add_menu.add_node_type(layout, "GeometryNodeSetGreasePencilSoftness") - node_add_menu.draw_assets_for_catalog(layout, "Grease Pencil/Write") + self.node_operator(layout, "GeometryNodeSetGreasePencilColor") + self.node_operator(layout, "GeometryNodeSetGreasePencilDepth") + self.node_operator(layout, "GeometryNodeSetGreasePencilSoftness") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_geometry_node_grease_pencil_operations(Menu): - bl_idname = "NODE_MT_geometry_node_grease_pencil_operations" +class NODE_MT_gn_grease_pencil_operations_base(node_add_menu.NodeMenu): bl_label = "Operations" + menu_path = "Grease Pencil/Operations" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeGreasePencilToCurves") - node_add_menu.add_node_type(layout, "GeometryNodeMergeLayers") - node_add_menu.draw_assets_for_catalog(layout, "Grease Pencil/Operations") + self.node_operator(layout, "GeometryNodeGreasePencilToCurves") + self.node_operator(layout, "GeometryNodeMergeLayers") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_geometry_node_grease_pencil(Menu): - bl_idname = "NODE_MT_geometry_node_grease_pencil" +class NODE_MT_gn_grease_pencil_base(node_add_menu.NodeMenu): bl_label = "Grease Pencil" def draw(self, _context): layout = self.layout - layout.menu("NODE_MT_geometry_node_grease_pencil_read") - layout.menu("NODE_MT_geometry_node_grease_pencil_write") + self.draw_menu(layout, path="Grease Pencil/Read") + self.draw_menu(layout, path="Grease Pencil/Write") layout.separator() - layout.menu("NODE_MT_geometry_node_grease_pencil_operations") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.draw_menu(layout, path="Grease Pencil/Operations") + + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_geometry_node_GEO_GEOMETRY(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_GEOMETRY" +class NODE_MT_gn_geometry_base(node_add_menu.NodeMenu): bl_label = "Geometry" def draw(self, _context): layout = self.layout - layout.menu("NODE_MT_geometry_node_GEO_GEOMETRY_READ") - layout.menu("NODE_MT_geometry_node_GEO_GEOMETRY_SAMPLE") - layout.menu("NODE_MT_geometry_node_GEO_GEOMETRY_WRITE") + self.draw_menu(layout, path="Geometry/Read") + self.draw_menu(layout, path="Geometry/Sample") + self.draw_menu(layout, path="Geometry/Write") layout.separator() - layout.menu("NODE_MT_geometry_node_GEO_GEOMETRY_OPERATIONS") + self.draw_menu(layout, path="Geometry/Operations") layout.separator() - node_add_menu.add_node_type(layout, "GeometryNodeGeometryToInstance") - node_add_menu.add_node_type(layout, "GeometryNodeJoinGeometry", search_weight=1.0) - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.node_operator(layout, "GeometryNodeGeometryToInstance") + self.node_operator(layout, "GeometryNodeJoinGeometry", search_weight=1.0) + + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_geometry_node_GEO_GEOMETRY_READ(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_GEOMETRY_READ" +class NODE_MT_gn_geometry_read_base(node_add_menu.NodeMenu): bl_label = "Read" + menu_path = "Geometry/Read" def draw(self, context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeInputID") - node_add_menu.add_node_type(layout, "GeometryNodeInputIndex") - node_add_menu.add_node_type(layout, "GeometryNodeInputNamedAttribute", search_weight=1.0) - node_add_menu.add_node_type(layout, "GeometryNodeInputNormal") - node_add_menu.add_node_type(layout, "GeometryNodeInputPosition", search_weight=1.0) - node_add_menu.add_node_type(layout, "GeometryNodeInputRadius") + self.node_operator(layout, "GeometryNodeInputID") + self.node_operator(layout, "GeometryNodeInputIndex") + self.node_operator(layout, "GeometryNodeInputNamedAttribute", search_weight=1.0) + self.node_operator(layout, "GeometryNodeInputNormal") + self.node_operator(layout, "GeometryNodeInputPosition", search_weight=1.0) + self.node_operator(layout, "GeometryNodeInputRadius") if context.space_data.node_tree_sub_type == 'TOOL': - node_add_menu.add_node_type(layout, "GeometryNodeToolSelection") - node_add_menu.add_node_type(layout, "GeometryNodeToolActiveElement") - node_add_menu.draw_assets_for_catalog(layout, "Geometry/Read") + self.node_operator(layout, "GeometryNodeToolSelection") + self.node_operator(layout, "GeometryNodeToolActiveElement") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_geometry_node_GEO_GEOMETRY_WRITE(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_GEOMETRY_WRITE" +class NODE_MT_gn_geometry_write_base(node_add_menu.NodeMenu): bl_label = "Write" + menu_path = "Geometry/Write" def draw(self, context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeSetGeometryName") - node_add_menu.add_node_type(layout, "GeometryNodeSetID") - node_add_menu.add_node_type(layout, "GeometryNodeSetPosition", search_weight=1.0) + self.node_operator(layout, "GeometryNodeSetGeometryName") + self.node_operator(layout, "GeometryNodeSetID") + self.node_operator(layout, "GeometryNodeSetPosition", search_weight=1.0) if context.space_data.node_tree_sub_type == 'TOOL': - node_add_menu.add_node_type(layout, "GeometryNodeToolSetSelection") - node_add_menu.draw_assets_for_catalog(layout, "Geometry/Write") + self.node_operator(layout, "GeometryNodeToolSetSelection") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_geometry_node_GEO_GEOMETRY_OPERATIONS(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_GEOMETRY_OPERATIONS" +class NODE_MT_gn_geometry_operations_base(node_add_menu.NodeMenu): bl_label = "Operations" + menu_path = "Geometry/Operations" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeBake") - node_add_menu.add_node_type(layout, "GeometryNodeBoundBox") - node_add_menu.add_node_type(layout, "GeometryNodeConvexHull") - node_add_menu.add_node_type(layout, "GeometryNodeDeleteGeometry") - node_add_menu.add_node_type(layout, "GeometryNodeDuplicateElements") - node_add_menu.add_node_type(layout, "GeometryNodeMergeByDistance") - node_add_menu.add_node_type(layout, "GeometryNodeSortElements") - node_add_menu.add_node_type(layout, "GeometryNodeTransform", search_weight=1.0) + self.node_operator(layout, "GeometryNodeBake") + self.node_operator(layout, "GeometryNodeBoundBox") + self.node_operator(layout, "GeometryNodeConvexHull") + self.node_operator(layout, "GeometryNodeDeleteGeometry") + self.node_operator(layout, "GeometryNodeDuplicateElements") + self.node_operator(layout, "GeometryNodeMergeByDistance") + self.node_operator(layout, "GeometryNodeSortElements") + self.node_operator(layout, "GeometryNodeTransform", search_weight=1.0) layout.separator() - node_add_menu.add_node_type(layout, "GeometryNodeSeparateComponents") - node_add_menu.add_node_type(layout, "GeometryNodeSeparateGeometry") - node_add_menu.add_node_type(layout, "GeometryNodeSplitToInstances") - node_add_menu.draw_assets_for_catalog(layout, "Geometry/Operations") + self.node_operator(layout, "GeometryNodeSeparateComponents") + self.node_operator(layout, "GeometryNodeSeparateGeometry") + self.node_operator(layout, "GeometryNodeSplitToInstances") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_geometry_node_GEO_GEOMETRY_SAMPLE(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_GEOMETRY_SAMPLE" +class NODE_MT_gn_geometry_sample_base(node_add_menu.NodeMenu): bl_label = "Sample" + menu_path = "Geometry/Sample" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeProximity") - node_add_menu.add_node_type(layout, "GeometryNodeIndexOfNearest") - node_add_menu.add_node_type(layout, "GeometryNodeRaycast") - node_add_menu.add_node_type(layout, "GeometryNodeSampleIndex") - node_add_menu.add_node_type(layout, "GeometryNodeSampleNearest") - node_add_menu.draw_assets_for_catalog(layout, "Geometry/Sample") + self.node_operator(layout, "GeometryNodeProximity") + self.node_operator(layout, "GeometryNodeIndexOfNearest") + self.node_operator(layout, "GeometryNodeRaycast") + self.node_operator(layout, "GeometryNodeSampleIndex") + self.node_operator(layout, "GeometryNodeSampleNearest") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_geometry_node_GEO_INPUT(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_INPUT" +class NODE_MT_gn_input_base(node_add_menu.NodeMenu): bl_label = "Input" def draw(self, context): layout = self.layout - layout.menu("NODE_MT_geometry_node_GEO_INPUT_CONSTANT") + self.draw_menu(layout, path="Input/Constant") if context.space_data.node_tree_sub_type != 'TOOL': - layout.menu("NODE_MT_geometry_node_GEO_INPUT_GIZMO") - layout.menu("NODE_MT_geometry_node_GEO_INPUT_GROUP") - layout.menu("NODE_MT_category_import") - layout.menu("NODE_MT_geometry_node_GEO_INPUT_SCENE") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.draw_menu(layout, path="Input/Gizmo") + self.draw_menu(layout, path="Input/Group") + self.draw_menu(layout, path="Input/Import") + self.draw_menu(layout, path="Input/Scene") + + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_geometry_node_GEO_INPUT_CONSTANT(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_INPUT_CONSTANT" +class NODE_MT_gn_input_constant_base(node_add_menu.NodeMenu): bl_label = "Constant" bl_translation_context = i18n_contexts.id_nodetree + menu_path = "Input/Constant" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "FunctionNodeInputBool") - node_add_menu.add_node_type(layout, "GeometryNodeInputCollection") - node_add_menu.add_node_type(layout, "FunctionNodeInputColor") - node_add_menu.add_node_type(layout, "GeometryNodeInputImage") - node_add_menu.add_node_type(layout, "FunctionNodeInputInt") - node_add_menu.add_node_type(layout, "GeometryNodeInputMaterial") - node_add_menu.add_node_type(layout, "GeometryNodeInputObject") - node_add_menu.add_node_type(layout, "FunctionNodeInputRotation") - node_add_menu.add_node_type(layout, "FunctionNodeInputString") - node_add_menu.add_node_type(layout, "ShaderNodeValue") - node_add_menu.add_node_type(layout, "FunctionNodeInputVector") - node_add_menu.draw_assets_for_catalog(layout, "Input/Constant") + self.node_operator(layout, "FunctionNodeInputBool") + self.node_operator(layout, "GeometryNodeInputCollection") + self.node_operator(layout, "FunctionNodeInputColor") + self.node_operator(layout, "GeometryNodeInputImage") + self.node_operator(layout, "FunctionNodeInputInt") + self.node_operator(layout, "GeometryNodeInputMaterial") + self.node_operator(layout, "GeometryNodeInputObject") + self.node_operator(layout, "FunctionNodeInputRotation") + self.node_operator(layout, "FunctionNodeInputString") + self.node_operator(layout, "ShaderNodeValue") + self.node_operator(layout, "FunctionNodeInputVector") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_geometry_node_GEO_INPUT_GROUP(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_INPUT_GROUP" +class NODE_MT_gn_input_group_base(node_add_menu.NodeMenu): bl_label = "Group" + menu_path = "Input/Group" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "NodeGroupInput") - node_add_menu.draw_assets_for_catalog(layout, "Input/Group") + self.node_operator(layout, "NodeGroupInput") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_geometry_node_GEO_INPUT_SCENE(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_INPUT_SCENE" +class NODE_MT_gn_input_scene_base(node_add_menu.NodeMenu): bl_label = "Scene" + menu_path = "Input/Scene" def draw(self, context): layout = self.layout if context.space_data.node_tree_sub_type == 'TOOL': - node_add_menu.add_node_type(layout, "GeometryNodeTool3DCursor") - node_add_menu.add_node_type(layout, "GeometryNodeInputActiveCamera") - node_add_menu.add_node_type_with_outputs( + self.node_operator(layout, "GeometryNodeTool3DCursor") + self.node_operator(layout, "GeometryNodeInputActiveCamera") + self.node_operator_with_outputs( context, layout, "GeometryNodeCameraInfo", @@ -355,655 +371,752 @@ class NODE_MT_geometry_node_GEO_INPUT_SCENE(Menu): "Orthographic Scale", ], ) - node_add_menu.add_node_type(layout, "GeometryNodeCollectionInfo") - node_add_menu.add_node_type(layout, "GeometryNodeImageInfo") - node_add_menu.add_node_type(layout, "GeometryNodeIsViewport") + self.node_operator(layout, "GeometryNodeCollectionInfo") + self.node_operator(layout, "GeometryNodeImageInfo") + self.node_operator(layout, "GeometryNodeIsViewport") if context.space_data.node_tree_sub_type == 'TOOL': - node_add_menu.add_node_type_with_outputs( + self.node_operator_with_outputs( context, layout, "GeometryNodeToolMousePosition", ["Mouse X", "Mouse Y", "Region Width", "Region Height"], ) - node_add_menu.add_node_type(layout, "GeometryNodeObjectInfo") - node_add_menu.add_node_type_with_outputs(context, layout, "GeometryNodeInputSceneTime", ["Frame", "Seconds"]) - node_add_menu.add_node_type(layout, "GeometryNodeSelfObject") + self.node_operator(layout, "GeometryNodeObjectInfo") + self.node_operator_with_outputs(context, layout, "GeometryNodeInputSceneTime", ["Frame", "Seconds"]) + self.node_operator(layout, "GeometryNodeSelfObject") if context.space_data.node_tree_sub_type == 'TOOL': - node_add_menu.add_node_type_with_outputs( + self.node_operator_with_outputs( context, layout, "GeometryNodeViewportTransform", ["Projection", "View", "Is Orthographic"], ) - node_add_menu.draw_assets_for_catalog(layout, "Input/Scene") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_geometry_node_GEO_INPUT_GIZMO(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_INPUT_GIZMO" +class NODE_MT_gn_input_gizmo_base(node_add_menu.NodeMenu): bl_label = "Gizmo" + menu_path = "Input/Gizmo" def draw(self, context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeGizmoDial") - node_add_menu.add_node_type(layout, "GeometryNodeGizmoLinear") - node_add_menu.add_node_type(layout, "GeometryNodeGizmoTransform") - node_add_menu.draw_assets_for_catalog(layout, "Input/Gizmo") + self.node_operator(layout, "GeometryNodeGizmoDial") + self.node_operator(layout, "GeometryNodeGizmoLinear") + self.node_operator(layout, "GeometryNodeGizmoTransform") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_geometry_node_GEO_INSTANCE(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_INSTANCE" +class NODE_MT_gn_instance_base(node_add_menu.NodeMenu): bl_label = "Instances" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeInstanceOnPoints", search_weight=2.0) - node_add_menu.add_node_type(layout, "GeometryNodeInstancesToPoints") + self.node_operator(layout, "GeometryNodeInstanceOnPoints", search_weight=2.0) + self.node_operator(layout, "GeometryNodeInstancesToPoints") layout.separator() - node_add_menu.add_node_type(layout, "GeometryNodeRealizeInstances", search_weight=1.0) - node_add_menu.add_node_type(layout, "GeometryNodeRotateInstances") - node_add_menu.add_node_type(layout, "GeometryNodeScaleInstances") - node_add_menu.add_node_type(layout, "GeometryNodeTranslateInstances") - node_add_menu.add_node_type(layout, "GeometryNodeSetInstanceTransform") + self.node_operator(layout, "GeometryNodeRealizeInstances", search_weight=1.0) + self.node_operator(layout, "GeometryNodeRotateInstances") + self.node_operator(layout, "GeometryNodeScaleInstances") + self.node_operator(layout, "GeometryNodeTranslateInstances") + self.node_operator(layout, "GeometryNodeSetInstanceTransform") layout.separator() - node_add_menu.add_node_type(layout, "GeometryNodeInputInstanceBounds") - node_add_menu.add_node_type(layout, "GeometryNodeInstanceTransform") - node_add_menu.add_node_type(layout, "GeometryNodeInputInstanceRotation") - node_add_menu.add_node_type(layout, "GeometryNodeInputInstanceScale") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.node_operator(layout, "GeometryNodeInputInstanceBounds") + self.node_operator(layout, "GeometryNodeInstanceTransform") + self.node_operator(layout, "GeometryNodeInputInstanceRotation") + self.node_operator(layout, "GeometryNodeInputInstanceScale") + + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_geometry_node_GEO_MATERIAL(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_MATERIAL" +class NODE_MT_gn_material_base(node_add_menu.NodeMenu): bl_label = "Material" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeReplaceMaterial") + self.node_operator(layout, "GeometryNodeReplaceMaterial") layout.separator() - node_add_menu.add_node_type(layout, "GeometryNodeInputMaterialIndex") - node_add_menu.add_node_type(layout, "GeometryNodeMaterialSelection") + self.node_operator(layout, "GeometryNodeInputMaterialIndex") + self.node_operator(layout, "GeometryNodeMaterialSelection") layout.separator() - node_add_menu.add_node_type(layout, "GeometryNodeSetMaterial", search_weight=1.0) - node_add_menu.add_node_type(layout, "GeometryNodeSetMaterialIndex") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.node_operator(layout, "GeometryNodeSetMaterial", search_weight=1.0) + self.node_operator(layout, "GeometryNodeSetMaterialIndex") + + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_geometry_node_GEO_MESH(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_MESH" +class NODE_MT_gn_mesh_base(node_add_menu.NodeMenu): bl_label = "Mesh" def draw(self, _context): layout = self.layout - layout.menu("NODE_MT_geometry_node_GEO_MESH_READ") - layout.menu("NODE_MT_geometry_node_GEO_MESH_SAMPLE") - layout.menu("NODE_MT_geometry_node_GEO_MESH_WRITE") + self.draw_menu(layout, path="Mesh/Read") + self.draw_menu(layout, path="Mesh/Sample") + self.draw_menu(layout, path="Mesh/Write") layout.separator() - layout.menu("NODE_MT_geometry_node_GEO_MESH_OPERATIONS") - layout.menu("NODE_MT_category_PRIMITIVES_MESH") - layout.menu("NODE_MT_geometry_node_mesh_topology") - layout.menu("NODE_MT_category_GEO_UV") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.draw_menu(layout, path="Mesh/Operations") + self.draw_menu(layout, path="Mesh/Primitives") + self.draw_menu(layout, path="Mesh/Topology") + self.draw_menu(layout, path="Mesh/UV") + + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_geometry_node_GEO_MESH_READ(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_MESH_READ" +class NODE_MT_gn_mesh_read_base(node_add_menu.NodeMenu): bl_label = "Read" + menu_path = "Mesh/Read" def draw(self, context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeInputMeshEdgeAngle") - node_add_menu.add_node_type(layout, "GeometryNodeInputMeshEdgeNeighbors") - node_add_menu.add_node_type(layout, "GeometryNodeInputMeshEdgeVertices") - node_add_menu.add_node_type(layout, "GeometryNodeEdgesToFaceGroups") - node_add_menu.add_node_type(layout, "GeometryNodeInputMeshFaceArea") - node_add_menu.add_node_type(layout, "GeometryNodeMeshFaceSetBoundaries") - node_add_menu.add_node_type(layout, "GeometryNodeInputMeshFaceNeighbors") + self.node_operator(layout, "GeometryNodeInputMeshEdgeAngle") + self.node_operator(layout, "GeometryNodeInputMeshEdgeNeighbors") + self.node_operator(layout, "GeometryNodeInputMeshEdgeVertices") + self.node_operator(layout, "GeometryNodeEdgesToFaceGroups") + self.node_operator(layout, "GeometryNodeInputMeshFaceArea") + self.node_operator(layout, "GeometryNodeMeshFaceSetBoundaries") + self.node_operator(layout, "GeometryNodeInputMeshFaceNeighbors") if context.space_data.node_tree_sub_type == 'TOOL': - node_add_menu.add_node_type(layout, "GeometryNodeToolFaceSet") - node_add_menu.add_node_type(layout, "GeometryNodeInputMeshFaceIsPlanar") - node_add_menu.add_node_type(layout, "GeometryNodeInputShadeSmooth") - node_add_menu.add_node_type(layout, "GeometryNodeInputEdgeSmooth") - node_add_menu.add_node_type(layout, "GeometryNodeInputMeshIsland") - node_add_menu.add_node_type(layout, "GeometryNodeInputShortestEdgePaths") - node_add_menu.add_node_type(layout, "GeometryNodeInputMeshVertexNeighbors") - node_add_menu.draw_assets_for_catalog(layout, "Mesh/Read") + self.node_operator(layout, "GeometryNodeToolFaceSet") + self.node_operator(layout, "GeometryNodeInputMeshFaceIsPlanar") + self.node_operator(layout, "GeometryNodeInputShadeSmooth") + self.node_operator(layout, "GeometryNodeInputEdgeSmooth") + self.node_operator(layout, "GeometryNodeInputMeshIsland") + self.node_operator(layout, "GeometryNodeInputShortestEdgePaths") + self.node_operator(layout, "GeometryNodeInputMeshVertexNeighbors") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_geometry_node_GEO_MESH_SAMPLE(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_MESH_SAMPLE" +class NODE_MT_gn_mesh_sample_base(node_add_menu.NodeMenu): bl_label = "Sample" + menu_path = "Mesh/Sample" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeSampleNearestSurface") - node_add_menu.add_node_type(layout, "GeometryNodeSampleUVSurface") - node_add_menu.draw_assets_for_catalog(layout, "Mesh/Sample") + self.node_operator(layout, "GeometryNodeSampleNearestSurface") + self.node_operator(layout, "GeometryNodeSampleUVSurface") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_geometry_node_GEO_MESH_WRITE(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_MESH_WRITE" +class NODE_MT_gn_mesh_write_base(node_add_menu.NodeMenu): bl_label = "Write" + menu_path = "Mesh/Write" def draw(self, context): layout = self.layout if context.space_data.node_tree_sub_type == 'TOOL': - node_add_menu.add_node_type(layout, "GeometryNodeToolSetFaceSet") - node_add_menu.add_node_type(layout, "GeometryNodeSetMeshNormal") - node_add_menu.add_node_type(layout, "GeometryNodeSetShadeSmooth") - node_add_menu.draw_assets_for_catalog(layout, "Mesh/Write") + self.node_operator(layout, "GeometryNodeToolSetFaceSet") + self.node_operator(layout, "GeometryNodeSetMeshNormal") + self.node_operator(layout, "GeometryNodeSetShadeSmooth") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_geometry_node_GEO_MESH_OPERATIONS(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_MESH_OPERATIONS" +class NODE_MT_gn_mesh_operations_base(node_add_menu.NodeMenu): bl_label = "Operations" + menu_path = "Mesh/Operations" def draw(self, context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeDualMesh") - node_add_menu.add_node_type(layout, "GeometryNodeEdgePathsToCurves") - node_add_menu.add_node_type(layout, "GeometryNodeEdgePathsToSelection") - node_add_menu.add_node_type(layout, "GeometryNodeExtrudeMesh") - node_add_menu.add_node_type(layout, "GeometryNodeFlipFaces") - node_add_menu.add_node_type(layout, "GeometryNodeMeshBoolean") - node_add_menu.add_node_type(layout, "GeometryNodeMeshToCurve") + self.node_operator(layout, "GeometryNodeDualMesh") + self.node_operator(layout, "GeometryNodeEdgePathsToCurves") + self.node_operator(layout, "GeometryNodeEdgePathsToSelection") + self.node_operator(layout, "GeometryNodeExtrudeMesh") + self.node_operator(layout, "GeometryNodeFlipFaces") + self.node_operator(layout, "GeometryNodeMeshBoolean") + self.node_operator(layout, "GeometryNodeMeshToCurve") if context.preferences.experimental.use_new_volume_nodes: - node_add_menu.add_node_type(layout, "GeometryNodeMeshToDensityGrid") - node_add_menu.add_node_type(layout, "GeometryNodeMeshToPoints") + self.node_operator(layout, "GeometryNodeMeshToDensityGrid") + self.node_operator(layout, "GeometryNodeMeshToPoints") if context.preferences.experimental.use_new_volume_nodes: - node_add_menu.add_node_type(layout, "GeometryNodeMeshToSDFGrid") - node_add_menu.add_node_type(layout, "GeometryNodeMeshToVolume") - node_add_menu.add_node_type(layout, "GeometryNodeScaleElements") - node_add_menu.add_node_type(layout, "GeometryNodeSplitEdges") - node_add_menu.add_node_type(layout, "GeometryNodeSubdivideMesh") - node_add_menu.add_node_type(layout, "GeometryNodeSubdivisionSurface") - node_add_menu.add_node_type(layout, "GeometryNodeTriangulate") - node_add_menu.draw_assets_for_catalog(layout, "Mesh/Operations") + self.node_operator(layout, "GeometryNodeMeshToSDFGrid") + self.node_operator(layout, "GeometryNodeMeshToVolume") + self.node_operator(layout, "GeometryNodeScaleElements") + self.node_operator(layout, "GeometryNodeSplitEdges") + self.node_operator(layout, "GeometryNodeSubdivideMesh") + self.node_operator(layout, "GeometryNodeSubdivisionSurface") + self.node_operator(layout, "GeometryNodeTriangulate") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_category_PRIMITIVES_MESH(Menu): - bl_idname = "NODE_MT_category_PRIMITIVES_MESH" +class NODE_MT_gn_mesh_primitives_base(node_add_menu.NodeMenu): bl_label = "Primitives" + menu_path = "Mesh/Primitives" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeMeshCone") - node_add_menu.add_node_type(layout, "GeometryNodeMeshCube") - node_add_menu.add_node_type(layout, "GeometryNodeMeshCylinder") - node_add_menu.add_node_type(layout, "GeometryNodeMeshGrid") - node_add_menu.add_node_type(layout, "GeometryNodeMeshIcoSphere") - node_add_menu.add_node_type(layout, "GeometryNodeMeshCircle") - node_add_menu.add_node_type(layout, "GeometryNodeMeshLine") - node_add_menu.add_node_type(layout, "GeometryNodeMeshUVSphere") - node_add_menu.draw_assets_for_catalog(layout, "Mesh/Primitives") + self.node_operator(layout, "GeometryNodeMeshCone") + self.node_operator(layout, "GeometryNodeMeshCube") + self.node_operator(layout, "GeometryNodeMeshCylinder") + self.node_operator(layout, "GeometryNodeMeshGrid") + self.node_operator(layout, "GeometryNodeMeshIcoSphere") + self.node_operator(layout, "GeometryNodeMeshCircle") + self.node_operator(layout, "GeometryNodeMeshLine") + self.node_operator(layout, "GeometryNodeMeshUVSphere") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_category_import(Menu): - bl_idname = "NODE_MT_category_import" +class NODE_MT_gn_input_import_base(node_add_menu.NodeMenu): bl_label = "Import" + menu_path = "Input/Import" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeImportCSV", label="CSV (.csv)") - node_add_menu.add_node_type(layout, "GeometryNodeImportOBJ", label="Wavefront (.obj)") - node_add_menu.add_node_type(layout, "GeometryNodeImportPLY", label="Stanford PLY (.ply)") - node_add_menu.add_node_type(layout, "GeometryNodeImportSTL", label="STL (.stl)") - node_add_menu.add_node_type(layout, "GeometryNodeImportText", label="Text (.txt)") - node_add_menu.add_node_type(layout, "GeometryNodeImportVDB", label="OpenVDB (.vdb)") - node_add_menu.draw_assets_for_catalog(layout, "Input/Import") + self.node_operator(layout, "GeometryNodeImportCSV", label="CSV (.csv)") + self.node_operator(layout, "GeometryNodeImportOBJ", label="Wavefront (.obj)") + self.node_operator(layout, "GeometryNodeImportPLY", label="Stanford PLY (.ply)") + self.node_operator(layout, "GeometryNodeImportSTL", label="STL (.stl)") + self.node_operator(layout, "GeometryNodeImportText", label="Text (.txt)") + self.node_operator(layout, "GeometryNodeImportVDB", label="OpenVDB (.vdb)") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_geometry_node_mesh_topology(Menu): - bl_idname = "NODE_MT_geometry_node_mesh_topology" +class NODE_MT_gn_mesh_topology_base(node_add_menu.NodeMenu): bl_label = "Topology" + menu_path = "Mesh/Topology" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeCornersOfEdge") - node_add_menu.add_node_type(layout, "GeometryNodeCornersOfFace") - node_add_menu.add_node_type(layout, "GeometryNodeCornersOfVertex") - node_add_menu.add_node_type(layout, "GeometryNodeEdgesOfCorner") - node_add_menu.add_node_type(layout, "GeometryNodeEdgesOfVertex") - node_add_menu.add_node_type(layout, "GeometryNodeFaceOfCorner") - node_add_menu.add_node_type(layout, "GeometryNodeOffsetCornerInFace") - node_add_menu.add_node_type(layout, "GeometryNodeVertexOfCorner") - node_add_menu.draw_assets_for_catalog(layout, "Mesh/Topology") + self.node_operator(layout, "GeometryNodeCornersOfEdge") + self.node_operator(layout, "GeometryNodeCornersOfFace") + self.node_operator(layout, "GeometryNodeCornersOfVertex") + self.node_operator(layout, "GeometryNodeEdgesOfCorner") + self.node_operator(layout, "GeometryNodeEdgesOfVertex") + self.node_operator(layout, "GeometryNodeFaceOfCorner") + self.node_operator(layout, "GeometryNodeOffsetCornerInFace") + self.node_operator(layout, "GeometryNodeVertexOfCorner") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_category_GEO_OUTPUT(Menu): - bl_idname = "NODE_MT_category_GEO_OUTPUT" +class NODE_MT_gn_output_base(node_add_menu.NodeMenu): bl_label = "Output" def draw(self, context): layout = self.layout - node_add_menu.add_node_type(layout, "NodeGroupOutput") - node_add_menu.add_node_type(layout, "GeometryNodeViewer") - node_add_menu.add_node_type_with_searchable_enum(context, layout, "GeometryNodeWarning", "warning_type") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.node_operator(layout, "NodeGroupOutput") + self.node_operator(layout, "GeometryNodeViewer") + self.node_operator_with_searchable_enum(context, layout, "GeometryNodeWarning", "warning_type") + + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_category_GEO_POINT(Menu): - bl_idname = "NODE_MT_category_GEO_POINT" +class NODE_MT_gn_point_base(node_add_menu.NodeMenu): bl_label = "Point" def draw(self, context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeDistributePointsInVolume") + self.node_operator(layout, "GeometryNodeDistributePointsInVolume") if context.preferences.experimental.use_new_volume_nodes: - node_add_menu.add_node_type(layout, "GeometryNodeDistributePointsInGrid") - node_add_menu.add_node_type(layout, "GeometryNodeDistributePointsOnFaces") + self.node_operator(layout, "GeometryNodeDistributePointsInGrid") + self.node_operator(layout, "GeometryNodeDistributePointsOnFaces") layout.separator() - node_add_menu.add_node_type(layout, "GeometryNodePoints") - node_add_menu.add_node_type(layout, "GeometryNodePointsToCurves") + self.node_operator(layout, "GeometryNodePoints") + self.node_operator(layout, "GeometryNodePointsToCurves") if context.preferences.experimental.use_new_volume_nodes: - node_add_menu.add_node_type(layout, "GeometryNodePointsToSDFGrid") - node_add_menu.add_node_type(layout, "GeometryNodePointsToVertices") - node_add_menu.add_node_type(layout, "GeometryNodePointsToVolume") + self.node_operator(layout, "GeometryNodePointsToSDFGrid") + self.node_operator(layout, "GeometryNodePointsToVertices") + self.node_operator(layout, "GeometryNodePointsToVolume") layout.separator() - node_add_menu.add_node_type(layout, "GeometryNodeSetPointRadius") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.node_operator(layout, "GeometryNodeSetPointRadius") + + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_category_simulation(Menu): - bl_idname = "NODE_MT_category_simulation" +class NODE_MT_gn_simulation_base(node_add_menu.NodeMenu): bl_label = "Simulation" def draw(self, _context): layout = self.layout - node_add_menu.add_simulation_zone(layout, label="Simulation") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.simulation_zone(layout, label="Simulation") + + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_category_GEO_TEXT(Menu): - bl_idname = "NODE_MT_category_GEO_TEXT" +class NODE_MT_gn_utilities_text_base(node_add_menu.NodeMenu): bl_label = "Text" + menu_path = "Utilities/Text" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "FunctionNodeFormatString") - node_add_menu.add_node_type(layout, "GeometryNodeStringJoin") - node_add_menu.add_node_type(layout, "FunctionNodeMatchString") - node_add_menu.add_node_type(layout, "FunctionNodeReplaceString") - node_add_menu.add_node_type(layout, "FunctionNodeSliceString") + self.node_operator(layout, "FunctionNodeFormatString") + self.node_operator(layout, "GeometryNodeStringJoin") + self.node_operator(layout, "FunctionNodeMatchString") + self.node_operator(layout, "FunctionNodeReplaceString") + self.node_operator(layout, "FunctionNodeSliceString") layout.separator() - node_add_menu.add_node_type(layout, "FunctionNodeFindInString") - node_add_menu.add_node_type(layout, "FunctionNodeStringLength") - node_add_menu.add_node_type(layout, "GeometryNodeStringToCurves") - node_add_menu.add_node_type(layout, "FunctionNodeStringToValue") - node_add_menu.add_node_type(layout, "FunctionNodeValueToString") + self.node_operator(layout, "FunctionNodeFindInString") + self.node_operator(layout, "FunctionNodeStringLength") + self.node_operator(layout, "GeometryNodeStringToCurves") + self.node_operator(layout, "FunctionNodeStringToValue") + self.node_operator(layout, "FunctionNodeValueToString") layout.separator() - node_add_menu.add_node_type(layout, "FunctionNodeInputSpecialCharacters") - node_add_menu.draw_assets_for_catalog(layout, "Utilities/Text") + self.node_operator(layout, "FunctionNodeInputSpecialCharacters") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_category_GEO_TEXTURE(Menu): - bl_idname = "NODE_MT_category_GEO_TEXTURE" +class NODE_MT_gn_texture_base(node_add_menu.NodeMenu): bl_label = "Texture" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "ShaderNodeTexBrick") - node_add_menu.add_node_type(layout, "ShaderNodeTexChecker") - node_add_menu.add_node_type(layout, "ShaderNodeTexGabor") - node_add_menu.add_node_type(layout, "ShaderNodeTexGradient") - node_add_menu.add_node_type(layout, "GeometryNodeImageTexture") - node_add_menu.add_node_type(layout, "ShaderNodeTexMagic") - node_add_menu.add_node_type(layout, "ShaderNodeTexNoise") - node_add_menu.add_node_type(layout, "ShaderNodeTexVoronoi") - node_add_menu.add_node_type(layout, "ShaderNodeTexWave") - node_add_menu.add_node_type(layout, "ShaderNodeTexWhiteNoise") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.node_operator(layout, "ShaderNodeTexBrick") + self.node_operator(layout, "ShaderNodeTexChecker") + self.node_operator(layout, "ShaderNodeTexGabor") + self.node_operator(layout, "ShaderNodeTexGradient") + self.node_operator(layout, "GeometryNodeImageTexture") + self.node_operator(layout, "ShaderNodeTexMagic") + self.node_operator(layout, "ShaderNodeTexNoise") + self.node_operator(layout, "ShaderNodeTexVoronoi") + self.node_operator(layout, "ShaderNodeTexWave") + self.node_operator(layout, "ShaderNodeTexWhiteNoise") + + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_category_GEO_UTILITIES(Menu): - bl_idname = "NODE_MT_category_GEO_UTILITIES" +class NODE_MT_gn_utilities_base(node_add_menu.NodeMenu): bl_label = "Utilities" def draw(self, context): layout = self.layout - layout.menu("NODE_MT_geometry_node_GEO_COLOR") - layout.menu("NODE_MT_category_GEO_TEXT") - layout.menu("NODE_MT_category_GEO_VECTOR") + self.draw_menu(layout, path="Utilities/Color") + self.draw_menu(layout, path="Utilities/Text") + self.draw_menu(layout, path="Utilities/Vector") layout.separator() - layout.menu("NODE_MT_category_utilities_bundle") - layout.menu("NODE_MT_category_utilities_closure") - layout.menu("NODE_MT_category_GEO_UTILITIES_FIELD") + self.draw_menu(layout, path="Utilities/Bundle") + self.draw_menu(layout, path="Utilities/Closure") + self.draw_menu(layout, path="Utilities/Field") + self.draw_menu(layout, path="Utilities/Math") if context.preferences.experimental.use_geometry_nodes_lists: - layout.menu("NODE_MT_category_utilities_list") - layout.menu("NODE_MT_category_GEO_UTILITIES_MATH") - layout.menu("NODE_MT_category_utilities_matrix") - layout.menu("NODE_MT_category_GEO_UTILITIES_ROTATION") - layout.menu("NODE_MT_category_GEO_UTILITIES_DEPRECATED") + self.draw_menu(layout, path="Utilities/List") + self.draw_menu(layout, path="Utilities/Matrix") + self.draw_menu(layout, path="Utilities/Rotation") + self.draw_menu(layout, path="Utilities/Deprecated") layout.separator() - node_add_menu.add_foreach_geometry_element_zone(layout, label="For Each Element") - node_add_menu.add_node_type(layout, "GeometryNodeIndexSwitch") - node_add_menu.add_node_type(layout, "GeometryNodeMenuSwitch") - node_add_menu.add_node_type(layout, "FunctionNodeRandomValue") - node_add_menu.add_repeat_zone(layout, label="Repeat") - node_add_menu.add_node_type(layout, "GeometryNodeSwitch") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.for_each_element_zone(layout, label="For Each Element") + self.node_operator(layout, "GeometryNodeIndexSwitch") + self.node_operator(layout, "GeometryNodeMenuSwitch") + self.node_operator(layout, "FunctionNodeRandomValue") + self.repeat_zone(layout, label="Repeat") + self.node_operator(layout, "GeometryNodeSwitch") + + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_category_GEO_UTILITIES_DEPRECATED(Menu): - bl_idname = "NODE_MT_category_GEO_UTILITIES_DEPRECATED" +class NODE_MT_gn_utilities_deprecated_base(node_add_menu.NodeMenu): bl_label = "Deprecated" + menu_path = "Utilities/Deprecated" def draw(self, context): layout = self.layout - node_add_menu.add_node_type(layout, "FunctionNodeAlignEulerToVector") - node_add_menu.add_node_type(layout, "FunctionNodeRotateEuler") - node_add_menu.draw_assets_for_catalog(layout, "Utilities/Deprecated") + self.node_operator(layout, "FunctionNodeAlignEulerToVector") + self.node_operator(layout, "FunctionNodeRotateEuler") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_category_GEO_UTILITIES_FIELD(Menu): - bl_idname = "NODE_MT_category_GEO_UTILITIES_FIELD" +class NODE_MT_gn_utilities_field_base(node_add_menu.NodeMenu): bl_label = "Field" + menu_path = "Utilities/Field" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeAccumulateField") - node_add_menu.add_node_type(layout, "GeometryNodeFieldAtIndex") - node_add_menu.add_node_type(layout, "GeometryNodeFieldOnDomain") - node_add_menu.add_node_type(layout, "GeometryNodeFieldAverage") - node_add_menu.add_node_type(layout, "GeometryNodeFieldMinAndMax") - node_add_menu.add_node_type(layout, "GeometryNodeFieldVariance") - node_add_menu.draw_assets_for_catalog(layout, "Utilities/Field") + self.node_operator(layout, "GeometryNodeAccumulateField") + self.node_operator(layout, "GeometryNodeFieldAtIndex") + self.node_operator(layout, "GeometryNodeFieldOnDomain") + self.node_operator(layout, "GeometryNodeFieldAverage") + self.node_operator(layout, "GeometryNodeFieldMinAndMax") + self.node_operator(layout, "GeometryNodeFieldVariance") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_category_GEO_UTILITIES_ROTATION(Menu): - bl_idname = "NODE_MT_category_GEO_UTILITIES_ROTATION" +class NODE_MT_gn_utilities_rotation_base(node_add_menu.NodeMenu): bl_label = "Rotation" + menu_path = "Utilities/Rotation" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "FunctionNodeAlignRotationToVector") - node_add_menu.add_node_type(layout, "FunctionNodeAxesToRotation") - node_add_menu.add_node_type(layout, "FunctionNodeAxisAngleToRotation") - node_add_menu.add_node_type(layout, "FunctionNodeEulerToRotation") - node_add_menu.add_node_type(layout, "FunctionNodeInvertRotation") - node_add_menu.add_node_type(layout, "FunctionNodeRotateRotation") - node_add_menu.add_node_type(layout, "FunctionNodeRotateVector") - node_add_menu.add_node_type(layout, "FunctionNodeRotationToAxisAngle") - node_add_menu.add_node_type(layout, "FunctionNodeRotationToEuler") - node_add_menu.add_node_type(layout, "FunctionNodeRotationToQuaternion") - node_add_menu.add_node_type(layout, "FunctionNodeQuaternionToRotation") - node_add_menu.draw_assets_for_catalog(layout, "Utilities/Rotation") + self.node_operator(layout, "FunctionNodeAlignRotationToVector") + self.node_operator(layout, "FunctionNodeAxesToRotation") + self.node_operator(layout, "FunctionNodeAxisAngleToRotation") + self.node_operator(layout, "FunctionNodeEulerToRotation") + self.node_operator(layout, "FunctionNodeInvertRotation") + self.node_operator(layout, "FunctionNodeRotateRotation") + self.node_operator(layout, "FunctionNodeRotateVector") + self.node_operator(layout, "FunctionNodeRotationToAxisAngle") + self.node_operator(layout, "FunctionNodeRotationToEuler") + self.node_operator(layout, "FunctionNodeRotationToQuaternion") + self.node_operator(layout, "FunctionNodeQuaternionToRotation") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_category_utilities_matrix(Menu): - bl_idname = "NODE_MT_category_utilities_matrix" +class NODE_MT_gn_utilities_matrix_base(node_add_menu.NodeMenu): bl_label = "Matrix" + menu_path = "Utilities/Matrix" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "FunctionNodeCombineMatrix") - node_add_menu.add_node_type(layout, "FunctionNodeCombineTransform") - node_add_menu.add_node_type(layout, "FunctionNodeMatrixDeterminant", label="Determinant") - node_add_menu.add_node_type(layout, "FunctionNodeInvertMatrix") - node_add_menu.add_node_type(layout, "FunctionNodeMatrixMultiply") - node_add_menu.add_node_type(layout, "FunctionNodeProjectPoint") - node_add_menu.add_node_type(layout, "FunctionNodeSeparateMatrix") - node_add_menu.add_node_type(layout, "FunctionNodeSeparateTransform") - node_add_menu.add_node_type(layout, "FunctionNodeTransformDirection") - node_add_menu.add_node_type(layout, "FunctionNodeTransformPoint") - node_add_menu.add_node_type(layout, "FunctionNodeTransposeMatrix") - node_add_menu.draw_assets_for_catalog(layout, "Utilities/Matrix") + self.node_operator(layout, "FunctionNodeCombineMatrix") + self.node_operator(layout, "FunctionNodeCombineTransform") + self.node_operator(layout, "FunctionNodeMatrixDeterminant", label="Determinant") + self.node_operator(layout, "FunctionNodeInvertMatrix") + self.node_operator(layout, "FunctionNodeMatrixMultiply") + self.node_operator(layout, "FunctionNodeProjectPoint") + self.node_operator(layout, "FunctionNodeSeparateMatrix") + self.node_operator(layout, "FunctionNodeSeparateTransform") + self.node_operator(layout, "FunctionNodeTransformDirection") + self.node_operator(layout, "FunctionNodeTransformPoint") + self.node_operator(layout, "FunctionNodeTransposeMatrix") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_category_utilities_bundle(Menu): - bl_idname = "NODE_MT_category_utilities_bundle" +class NODE_MT_category_utilities_bundle_base(node_add_menu.NodeMenu): bl_label = "Bundle" + menu_path = "Utilities/Bundle" def draw(self, context): layout = self.layout - node_add_menu.add_node_type(layout, "NodeCombineBundle") - node_add_menu.add_node_type(layout, "NodeSeparateBundle") - node_add_menu.draw_assets_for_catalog(layout, "Utilities/Bundle") + self.node_operator(layout, "NodeCombineBundle") + self.node_operator(layout, "NodeSeparateBundle") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_category_utilities_closure(Menu): - bl_idname = "NODE_MT_category_utilities_closure" +class NODE_MT_category_utilities_closure_base(node_add_menu.NodeMenu): bl_label = "Closure" + menu_path = "Utilities/Closure" def draw(self, context): layout = self.layout - node_add_menu.add_closure_zone(layout, label="Closure") - node_add_menu.add_node_type(layout, "NodeEvaluateClosure") - node_add_menu.draw_assets_for_catalog(layout, "Utilities/Closure") + self.closure_zone(layout, label="Closure") + self.node_operator(layout, "NodeEvaluateClosure") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_category_utilities_list(Menu): - bl_idname = "NODE_MT_category_utilities_list" +class NODE_MT_gn_utilities_list_base(node_add_menu.NodeMenu): bl_label = "List" + menu_path = "Utilities/List" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeList") - node_add_menu.add_node_type(layout, "GeometryNodeListGetItem") - node_add_menu.add_node_type(layout, "GeometryNodeListLength") - node_add_menu.draw_assets_for_catalog(layout, "Utilities/List") + self.node_operator(layout, "GeometryNodeList") + self.node_operator(layout, "GeometryNodeListGetItem") + self.node_operator(layout, "GeometryNodeListLength") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_category_GEO_UTILITIES_MATH(Menu): - bl_idname = "NODE_MT_category_GEO_UTILITIES_MATH" +class NODE_MT_gn_utilities_math_base(node_add_menu.NodeMenu): bl_label = "Math" + menu_path = "Utilities/Math" def draw(self, context): layout = self.layout - node_add_menu.add_node_type_with_searchable_enum( + self.node_operator_with_searchable_enum( context, layout, "FunctionNodeBitMath", "operation", search_weight=-1.0, ) - node_add_menu.add_node_type_with_searchable_enum(context, layout, "FunctionNodeBooleanMath", "operation") - node_add_menu.add_node_type_with_searchable_enum(context, layout, "FunctionNodeIntegerMath", "operation") - node_add_menu.add_node_type(layout, "ShaderNodeClamp") - node_add_menu.add_node_type(layout, "FunctionNodeCompare") - node_add_menu.add_node_type(layout, "ShaderNodeFloatCurve") - node_add_menu.add_node_type(layout, "FunctionNodeFloatToInt") - node_add_menu.add_node_type(layout, "FunctionNodeHashValue") - node_add_menu.add_node_type(layout, "ShaderNodeMapRange") - node_add_menu.add_node_type_with_searchable_enum(context, layout, "ShaderNodeMath", "operation") - node_add_menu.add_node_type(layout, "ShaderNodeMix") - node_add_menu.draw_assets_for_catalog(layout, "Utilities/Math") + self.node_operator_with_searchable_enum(context, layout, "FunctionNodeBooleanMath", "operation") + self.node_operator_with_searchable_enum(context, layout, "FunctionNodeIntegerMath", "operation") + self.node_operator(layout, "ShaderNodeClamp") + self.node_operator(layout, "FunctionNodeCompare") + self.node_operator(layout, "ShaderNodeFloatCurve") + self.node_operator(layout, "FunctionNodeFloatToInt") + self.node_operator(layout, "FunctionNodeHashValue") + self.node_operator(layout, "ShaderNodeMapRange") + self.node_operator_with_searchable_enum(context, layout, "ShaderNodeMath", "operation") + self.node_operator(layout, "ShaderNodeMix") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_category_GEO_UV(Menu): - bl_idname = "NODE_MT_category_GEO_UV" +class NODE_MT_gn_mesh_uv_base(node_add_menu.NodeMenu): bl_label = "UV" + menu_path = "Mesh/UV" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeUVPackIslands") - node_add_menu.add_node_type(layout, "GeometryNodeUVUnwrap") - node_add_menu.draw_assets_for_catalog(layout, "Mesh/UV") + self.node_operator(layout, "GeometryNodeUVPackIslands") + self.node_operator(layout, "GeometryNodeUVUnwrap") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_category_GEO_VECTOR(Menu): - bl_idname = "NODE_MT_category_GEO_VECTOR" +class NODE_MT_gn_utilities_vector_base(node_add_menu.NodeMenu): bl_label = "Vector" + menu_path = "Utilities/Vector" def draw(self, context): layout = self.layout - node_add_menu.add_node_type(layout, "ShaderNodeRadialTiling") - node_add_menu.add_node_type(layout, "ShaderNodeVectorCurve") - node_add_menu.add_node_type_with_searchable_enum(context, layout, "ShaderNodeVectorMath", "operation") - node_add_menu.add_node_type(layout, "ShaderNodeVectorRotate") + self.node_operator(layout, "ShaderNodeRadialTiling") + self.node_operator(layout, "ShaderNodeVectorCurve") + self.node_operator_with_searchable_enum(context, layout, "ShaderNodeVectorMath", "operation") + self.node_operator(layout, "ShaderNodeVectorRotate") layout.separator() - node_add_menu.add_node_type(layout, "ShaderNodeCombineXYZ") - props = node_add_menu.add_node_type(layout, "ShaderNodeMix", label="Mix Vector") + self.node_operator(layout, "ShaderNodeCombineXYZ") + props = self.node_operator(layout, "ShaderNodeMix", label="Mix Vector") ops = props.settings.add() ops.name = "data_type" ops.value = "'VECTOR'" - node_add_menu.add_node_type(layout, "ShaderNodeSeparateXYZ") - node_add_menu.draw_assets_for_catalog(layout, "Utilities/Vector") + self.node_operator(layout, "ShaderNodeSeparateXYZ") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_category_GEO_VOLUME(Menu): - bl_idname = "NODE_MT_category_GEO_VOLUME" +class NODE_MT_gn_volume_base(node_add_menu.NodeMenu): bl_label = "Volume" bl_translation_context = i18n_contexts.id_id def draw(self, context): layout = self.layout if context.preferences.experimental.use_new_volume_nodes: - layout.menu("NODE_MT_geometry_node_GEO_VOLUME_READ") - layout.menu("NODE_MT_geometry_node_volume_sample") - layout.menu("NODE_MT_geometry_node_GEO_VOLUME_WRITE") + self.draw_menu(layout, path="Volume/Read") + self.draw_menu(layout, path="Volume/Sample") + self.draw_menu(layout, path="Volume/Write") layout.separator() - layout.menu("NODE_MT_geometry_node_GEO_VOLUME_OPERATIONS") - layout.menu("NODE_MT_geometry_node_GEO_VOLUME_PRIMITIVES") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.draw_menu(layout, path="Volume/Operations") + self.draw_menu(layout, path="Volume/Primitives") + + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_geometry_node_GEO_VOLUME_READ(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_VOLUME_READ" +class NODE_MT_gn_volume_read_base(node_add_menu.NodeMenu): bl_label = "Read" + menu_path = "Volume/Read" def draw(self, context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeGetNamedGrid") - node_add_menu.add_node_type(layout, "GeometryNodeGridInfo") - node_add_menu.draw_assets_for_catalog(layout, "Volume/Read") + self.node_operator(layout, "GeometryNodeGetNamedGrid") + self.node_operator(layout, "GeometryNodeGridInfo") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_geometry_node_GEO_VOLUME_WRITE(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_VOLUME_WRITE" +class NODE_MT_gn_volume_write_base(node_add_menu.NodeMenu): bl_label = "Write" + menu_path = "Volume/Write" def draw(self, context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeStoreNamedGrid") - node_add_menu.draw_assets_for_catalog(layout, "Volume/Write") + self.node_operator(layout, "GeometryNodeStoreNamedGrid") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_geometry_node_volume_sample(Menu): - bl_idname = "NODE_MT_geometry_node_volume_sample" +class NODE_MT_gn_volume_sample_base(node_add_menu.NodeMenu): bl_label = "Sample" + menu_path = "Volume/Sample" def draw(self, context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeSampleGrid") - node_add_menu.add_node_type(layout, "GeometryNodeSampleGridIndex") - node_add_menu.draw_assets_for_catalog(layout, "Volume/Sample") + self.node_operator(layout, "GeometryNodeSampleGrid") + self.node_operator(layout, "GeometryNodeSampleGridIndex") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_geometry_node_GEO_VOLUME_OPERATIONS(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_VOLUME_OPERATIONS" +class NODE_MT_gn_volume_operations_base(node_add_menu.NodeMenu): bl_label = "Operations" + menu_path = "Volume/Operations" def draw(self, context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeVolumeToMesh") + self.node_operator(layout, "GeometryNodeVolumeToMesh") if context.preferences.experimental.use_new_volume_nodes: - node_add_menu.add_node_type(layout, "GeometryNodeGridToMesh") - node_add_menu.add_node_type(layout, "GeometryNodeSDFGridBoolean") - node_add_menu.draw_assets_for_catalog(layout, "Volume/Operations") + self.node_operator(layout, "GeometryNodeGridToMesh") + self.node_operator(layout, "GeometryNodeSDFGridBoolean") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_geometry_node_GEO_VOLUME_PRIMITIVES(Menu): - bl_idname = "NODE_MT_geometry_node_GEO_VOLUME_PRIMITIVES" +class NODE_MT_gn_volume_primitives_base(node_add_menu.NodeMenu): bl_label = "Primitives" + menu_path = "Volume/Primitives" def draw(self, context): layout = self.layout - node_add_menu.add_node_type(layout, "GeometryNodeVolumeCube") - node_add_menu.draw_assets_for_catalog(layout, "Volume/Primitives") + self.node_operator(layout, "GeometryNodeVolumeCube") + + self.draw_assets_for_catalog(layout, self.menu_path) -class NODE_MT_category_GEO_GROUP(Menu): - bl_idname = "NODE_MT_category_GEO_GROUP" - bl_label = "Group" - - def draw(self, context): - layout = self.layout - node_add_menu.draw_node_group_add_menu(context, layout) - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) - - -class NODE_MT_geometry_node_add_all(Menu): - bl_idname = "NODE_MT_geometry_node_add_all" +class NODE_MT_gn_all_base(node_add_menu.NodeMenu): bl_label = "" + menu_path = "Root" + # NOTE: Menus are looked up via their label, this is so that both the Add + # & Swap menus can share the same layout while each using their + # corresponding menus def draw(self, context): layout = self.layout - layout.menu("NODE_MT_geometry_node_GEO_ATTRIBUTE") - layout.menu("NODE_MT_geometry_node_GEO_INPUT") - layout.menu("NODE_MT_category_GEO_OUTPUT") + self.draw_menu(layout, "Attribute") + self.draw_menu(layout, "Input") + self.draw_menu(layout, "Output") layout.separator() - layout.menu("NODE_MT_geometry_node_GEO_GEOMETRY") + self.draw_menu(layout, "Geometry") layout.separator() - layout.menu("NODE_MT_geometry_node_GEO_CURVE") - layout.menu("NODE_MT_geometry_node_grease_pencil") - layout.menu("NODE_MT_geometry_node_GEO_INSTANCE") - layout.menu("NODE_MT_geometry_node_GEO_MESH") - layout.menu("NODE_MT_category_GEO_POINT") - layout.menu("NODE_MT_category_GEO_VOLUME") + self.draw_menu(layout, "Curve") + self.draw_menu(layout, "Grease Pencil") + self.draw_menu(layout, "Instances") + self.draw_menu(layout, "Mesh") + self.draw_menu(layout, "Point") + self.draw_menu(layout, "Volume") layout.separator() - layout.menu("NODE_MT_category_simulation") + self.draw_menu(layout, "Simulation") layout.separator() - layout.menu("NODE_MT_geometry_node_GEO_MATERIAL") - layout.menu("NODE_MT_category_GEO_TEXTURE") - layout.menu("NODE_MT_category_GEO_UTILITIES") + self.draw_menu(layout, "Material") + self.draw_menu(layout, "Texture") + self.draw_menu(layout, "Utilities") layout.separator() - layout.menu("NODE_MT_category_GEO_GROUP") - layout.menu("NODE_MT_category_layout") - node_add_menu.draw_root_assets(layout) + self.draw_menu(layout, "Group") + self.draw_menu(layout, "Layout") + + self.draw_root_assets(layout) + + +add_menus = { + # menu bl_idname: baseclass + "NODE_MT_geometry_node_GEO_ATTRIBUTE": NODE_MT_gn_attribute_base, + "NODE_MT_geometry_node_GEO_INPUT": NODE_MT_gn_input_base, + "NODE_MT_geometry_node_GEO_INPUT_CONSTANT": NODE_MT_gn_input_constant_base, + "NODE_MT_geometry_node_GEO_INPUT_GIZMO": NODE_MT_gn_input_gizmo_base, + "NODE_MT_geometry_node_GEO_INPUT_GROUP": NODE_MT_gn_input_group_base, + "NODE_MT_category_import": NODE_MT_gn_input_import_base, + "NODE_MT_geometry_node_GEO_INPUT_SCENE": NODE_MT_gn_input_scene_base, + "NODE_MT_category_GEO_OUTPUT": NODE_MT_gn_output_base, + "NODE_MT_geometry_node_GEO_CURVE": NODE_MT_gn_curve_base, + "NODE_MT_geometry_node_GEO_CURVE_READ": NODE_MT_gn_curve_read_base, + "NODE_MT_geometry_node_GEO_CURVE_SAMPLE": NODE_MT_gn_curve_sample_base, + "NODE_MT_geometry_node_GEO_CURVE_WRITE": NODE_MT_gn_curve_write_base, + "NODE_MT_geometry_node_GEO_CURVE_OPERATIONS": NODE_MT_gn_curve_operations_base, + "NODE_MT_geometry_node_GEO_PRIMITIVES_CURVE": NODE_MT_gn_curve_primitives_base, + "NODE_MT_geometry_node_curve_topology": NODE_MT_gn_curve_topology_base, + "NODE_MT_geometry_node_grease_pencil": NODE_MT_gn_grease_pencil_base, + "NODE_MT_geometry_node_grease_pencil_read": NODE_MT_gn_grease_pencil_read_base, + "NODE_MT_geometry_node_grease_pencil_write": NODE_MT_gn_grease_pencil_write_base, + "NODE_MT_geometry_node_grease_pencil_operations": NODE_MT_gn_grease_pencil_operations_base, + "NODE_MT_geometry_node_GEO_GEOMETRY": NODE_MT_gn_geometry_base, + "NODE_MT_geometry_node_GEO_GEOMETRY_READ": NODE_MT_gn_geometry_read_base, + "NODE_MT_geometry_node_GEO_GEOMETRY_WRITE": NODE_MT_gn_geometry_write_base, + "NODE_MT_geometry_node_GEO_GEOMETRY_OPERATIONS": NODE_MT_gn_geometry_operations_base, + "NODE_MT_geometry_node_GEO_GEOMETRY_SAMPLE": NODE_MT_gn_geometry_sample_base, + "NODE_MT_geometry_node_GEO_INSTANCE": NODE_MT_gn_instance_base, + "NODE_MT_geometry_node_GEO_MESH": NODE_MT_gn_mesh_base, + "NODE_MT_geometry_node_GEO_MESH_READ": NODE_MT_gn_mesh_read_base, + "NODE_MT_geometry_node_GEO_MESH_SAMPLE": NODE_MT_gn_mesh_sample_base, + "NODE_MT_geometry_node_GEO_MESH_WRITE": NODE_MT_gn_mesh_write_base, + "NODE_MT_geometry_node_GEO_MESH_OPERATIONS": NODE_MT_gn_mesh_operations_base, + "NODE_MT_category_PRIMITIVES_MESH": NODE_MT_gn_mesh_uv_base, + "NODE_MT_geometry_node_mesh_topology": NODE_MT_gn_mesh_topology_base, + "NODE_MT_category_GEO_UV": NODE_MT_gn_mesh_primitives_base, + "NODE_MT_category_GEO_POINT": NODE_MT_gn_point_base, + "NODE_MT_category_simulation": NODE_MT_gn_simulation_base, + "NODE_MT_category_GEO_VOLUME": NODE_MT_gn_volume_base, + "NODE_MT_geometry_node_GEO_VOLUME_READ": NODE_MT_gn_volume_read_base, + "NODE_MT_geometry_node_volume_sample": NODE_MT_gn_volume_sample_base, + "NODE_MT_geometry_node_GEO_VOLUME_WRITE": NODE_MT_gn_volume_write_base, + "NODE_MT_geometry_node_GEO_VOLUME_OPERATIONS": NODE_MT_gn_volume_operations_base, + "NODE_MT_geometry_node_GEO_VOLUME_PRIMITIVES": NODE_MT_gn_volume_primitives_base, + "NODE_MT_geometry_node_GEO_MATERIAL": NODE_MT_gn_material_base, + "NODE_MT_category_GEO_TEXTURE": NODE_MT_gn_texture_base, + "NODE_MT_category_GEO_UTILITIES": NODE_MT_gn_utilities_base, + "NODE_MT_geometry_node_GEO_COLOR": NODE_MT_gn_utilities_color_base, + "NODE_MT_category_GEO_TEXT": NODE_MT_gn_utilities_text_base, + "NODE_MT_category_GEO_VECTOR": NODE_MT_gn_utilities_vector_base, + "NODE_MT_category_utilities_bundle": NODE_MT_category_utilities_bundle_base, + "NODE_MT_category_utilities_closure": NODE_MT_category_utilities_closure_base, + "NODE_MT_category_GEO_UTILITIES_FIELD": NODE_MT_gn_utilities_field_base, + "NODE_MT_category_GEO_UTILITIES_MATH": NODE_MT_gn_utilities_math_base, + "NODE_MT_category_GEO_UTILITIES_ROTATION": NODE_MT_gn_utilities_rotation_base, + "NODE_MT_category_utilities_list": NODE_MT_gn_utilities_list_base, + "NODE_MT_category_utilities_matrix": NODE_MT_gn_utilities_matrix_base, + "NODE_MT_category_GEO_UTILITIES_DEPRECATED": NODE_MT_gn_utilities_deprecated_base, + "NODE_MT_geometry_node_add_all": NODE_MT_gn_all_base, +} +add_menus = node_add_menu.generate_menus( + add_menus, + template=node_add_menu.AddNodeMenu, + base_dict=node_add_menu.add_base_pathing_dict +) + + +swap_menus = { + # menu bl_idname: baseclass + "NODE_MT_gn_attribute_swap": NODE_MT_gn_attribute_base, + "NODE_MT_gn_input_swap": NODE_MT_gn_input_base, + "NODE_MT_gn_input_constant_swap": NODE_MT_gn_input_constant_base, + "NODE_MT_gn_input_gizmo_swap": NODE_MT_gn_input_gizmo_base, + "NODE_MT_gn_input_group_swap": NODE_MT_gn_input_group_base, + "NODE_MT_gn_input_import_swap": NODE_MT_gn_input_import_base, + "NODE_MT_gn_input_scene_swap": NODE_MT_gn_input_scene_base, + "NODE_MT_gn_output_swap": NODE_MT_gn_output_base, + "NODE_MT_gn_curve_swap": NODE_MT_gn_curve_base, + "NODE_MT_gn_curve_read_swap": NODE_MT_gn_curve_read_base, + "NODE_MT_gn_curve_sample_swap": NODE_MT_gn_curve_sample_base, + "NODE_MT_gn_curve_write_swap": NODE_MT_gn_curve_write_base, + "NODE_MT_gn_curve_operations_swap": NODE_MT_gn_curve_operations_base, + "NODE_MT_gn_curve_primitives_swap": NODE_MT_gn_curve_primitives_base, + "NODE_MT_gn_curve_topology_swap": NODE_MT_gn_curve_topology_base, + "NODE_MT_gn_grease_pencil_swap": NODE_MT_gn_grease_pencil_base, + "NODE_MT_gn_grease_pencil_read_swap": NODE_MT_gn_grease_pencil_read_base, + "NODE_MT_gn_grease_pencil_write_swap": NODE_MT_gn_grease_pencil_write_base, + "NODE_MT_gn_grease_pencil_operations_swap": NODE_MT_gn_grease_pencil_operations_base, + "NODE_MT_gn_geometry_swap": NODE_MT_gn_geometry_base, + "NODE_MT_gn_geometry_read_swap": NODE_MT_gn_geometry_read_base, + "NODE_MT_gn_geometry_write_swap": NODE_MT_gn_geometry_write_base, + "NODE_MT_gn_geometry_operations_swap": NODE_MT_gn_geometry_operations_base, + "NODE_MT_gn_geometry_sample_swap": NODE_MT_gn_geometry_sample_base, + "NODE_MT_gn_instance_swap": NODE_MT_gn_instance_base, + "NODE_MT_gn_mesh_swap": NODE_MT_gn_mesh_base, + "NODE_MT_gn_mesh_read_swap": NODE_MT_gn_mesh_read_base, + "NODE_MT_gn_mesh_sample_swap": NODE_MT_gn_mesh_sample_base, + "NODE_MT_gn_mesh_write_swap": NODE_MT_gn_mesh_write_base, + "NODE_MT_gn_mesh_operations_swap": NODE_MT_gn_mesh_operations_base, + "NODE_MT_gn_mesh_uv_swap": NODE_MT_gn_mesh_uv_base, + "NODE_MT_gn_mesh_topology_swap": NODE_MT_gn_mesh_topology_base, + "NODE_MT_gn_mesh_primitives_swap": NODE_MT_gn_mesh_primitives_base, + "NODE_MT_gn_point_swap": NODE_MT_gn_point_base, + "NODE_MT_gn_simulation_swap": NODE_MT_gn_simulation_base, + "NODE_MT_gn_volume_swap": NODE_MT_gn_volume_base, + "NODE_MT_gn_volume_read_swap": NODE_MT_gn_volume_read_base, + "NODE_MT_gn_volume_sample_swap": NODE_MT_gn_volume_sample_base, + "NODE_MT_gn_volume_write_swap": NODE_MT_gn_volume_write_base, + "NODE_MT_gn_volume_operations_swap": NODE_MT_gn_volume_operations_base, + "NODE_MT_gn_volume_primitives_swap": NODE_MT_gn_volume_primitives_base, + "NODE_MT_gn_material_swap": NODE_MT_gn_material_base, + "NODE_MT_gn_texture_swap": NODE_MT_gn_texture_base, + "NODE_MT_gn_utilities_swap": NODE_MT_gn_utilities_base, + "NODE_MT_gn_utilities_color_swap": NODE_MT_gn_utilities_color_base, + "NODE_MT_gn_utilities_text_swap": NODE_MT_gn_utilities_text_base, + "NODE_MT_gn_utilities_vector_swap": NODE_MT_gn_utilities_vector_base, + "NODE_MT_gn_utilities_bundle_swap": NODE_MT_category_utilities_bundle_base, + "NODE_MT_gn_utilities_closure_swap": NODE_MT_category_utilities_closure_base, + "NODE_MT_gn_utilities_field_swap": NODE_MT_gn_utilities_field_base, + "NODE_MT_gn_utilities_math_swap": NODE_MT_gn_utilities_math_base, + "NODE_MT_gn_utilities_rotation_swap": NODE_MT_gn_utilities_rotation_base, + "NODE_MT_gn_utilities_list_swap": NODE_MT_gn_utilities_list_base, + "NODE_MT_gn_utilities_matrix_swap": NODE_MT_gn_utilities_matrix_base, + "NODE_MT_gn_utilities_deprecated_swap": NODE_MT_gn_utilities_deprecated_base, + "NODE_MT_geometry_node_swap_all": NODE_MT_gn_all_base, +} +swap_menus = node_add_menu.generate_menus( + swap_menus, + template=node_add_menu.SwapNodeMenu, + base_dict=node_add_menu.swap_base_pathing_dict +) classes = ( - NODE_MT_geometry_node_add_all, - NODE_MT_geometry_node_GEO_ATTRIBUTE, - NODE_MT_geometry_node_GEO_INPUT, - NODE_MT_geometry_node_GEO_INPUT_CONSTANT, - NODE_MT_geometry_node_GEO_INPUT_GROUP, - NODE_MT_geometry_node_GEO_INPUT_SCENE, - NODE_MT_category_GEO_OUTPUT, - NODE_MT_geometry_node_GEO_CURVE, - NODE_MT_geometry_node_GEO_CURVE_READ, - NODE_MT_geometry_node_GEO_CURVE_SAMPLE, - NODE_MT_geometry_node_GEO_CURVE_WRITE, - NODE_MT_geometry_node_GEO_CURVE_OPERATIONS, - NODE_MT_geometry_node_GEO_PRIMITIVES_CURVE, - NODE_MT_geometry_node_curve_topology, - NODE_MT_geometry_node_grease_pencil, - NODE_MT_geometry_node_grease_pencil_read, - NODE_MT_geometry_node_grease_pencil_write, - NODE_MT_geometry_node_grease_pencil_operations, - NODE_MT_geometry_node_GEO_GEOMETRY, - NODE_MT_geometry_node_GEO_GEOMETRY_READ, - NODE_MT_geometry_node_GEO_GEOMETRY_WRITE, - NODE_MT_geometry_node_GEO_GEOMETRY_OPERATIONS, - NODE_MT_geometry_node_GEO_GEOMETRY_SAMPLE, - NODE_MT_geometry_node_GEO_INSTANCE, - NODE_MT_geometry_node_GEO_MESH, - NODE_MT_geometry_node_GEO_MESH_READ, - NODE_MT_geometry_node_GEO_MESH_SAMPLE, - NODE_MT_geometry_node_GEO_MESH_WRITE, - NODE_MT_geometry_node_GEO_MESH_OPERATIONS, - NODE_MT_category_GEO_UV, - NODE_MT_category_PRIMITIVES_MESH, - NODE_MT_category_import, - NODE_MT_geometry_node_mesh_topology, - NODE_MT_category_GEO_POINT, - NODE_MT_category_simulation, - NODE_MT_category_GEO_VOLUME, - NODE_MT_geometry_node_GEO_VOLUME_READ, - NODE_MT_geometry_node_volume_sample, - NODE_MT_geometry_node_GEO_VOLUME_WRITE, - NODE_MT_geometry_node_GEO_VOLUME_OPERATIONS, - NODE_MT_geometry_node_GEO_VOLUME_PRIMITIVES, - NODE_MT_geometry_node_GEO_MATERIAL, - NODE_MT_category_GEO_TEXTURE, - NODE_MT_category_GEO_UTILITIES, - NODE_MT_geometry_node_GEO_COLOR, - NODE_MT_category_GEO_TEXT, - NODE_MT_category_GEO_VECTOR, - NODE_MT_category_GEO_UTILITIES_FIELD, - NODE_MT_category_GEO_UTILITIES_MATH, - NODE_MT_category_GEO_UTILITIES_ROTATION, - NODE_MT_geometry_node_GEO_INPUT_GIZMO, - NODE_MT_category_utilities_list, - NODE_MT_category_utilities_matrix, - NODE_MT_category_utilities_bundle, - NODE_MT_category_utilities_closure, - NODE_MT_category_GEO_UTILITIES_DEPRECATED, - NODE_MT_category_GEO_GROUP, + *add_menus, + *swap_menus, ) + if __name__ == "__main__": # only for live edit. from bpy.utils import register_class for cls in classes: diff --git a/scripts/startup/bl_ui/node_add_menu_shader.py b/scripts/startup/bl_ui/node_add_menu_shader.py index 3dc8171b091..6eea092485f 100644 --- a/scripts/startup/bl_ui/node_add_menu_shader.py +++ b/scripts/startup/bl_ui/node_add_menu_shader.py @@ -48,27 +48,26 @@ def object_eevee_shader_nodes_poll(context): eevee_shader_nodes_poll(context)) -class NODE_MT_category_shader_input(Menu): - bl_idname = "NODE_MT_category_shader_input" +class NODE_MT_shader_node_input_base(node_add_menu.NodeMenu): bl_label = "Input" def draw(self, context): layout = self.layout - node_add_menu.add_node_type(layout, "ShaderNodeAmbientOcclusion") - node_add_menu.add_node_type(layout, "ShaderNodeAttribute") - node_add_menu.add_node_type(layout, "ShaderNodeBevel") - node_add_menu.add_node_type_with_outputs( + self.node_operator(layout, "ShaderNodeAmbientOcclusion") + self.node_operator(layout, "ShaderNodeAttribute") + self.node_operator(layout, "ShaderNodeBevel") + self.node_operator_with_outputs( context, layout, "ShaderNodeCameraData", ["View Vector", "View Z Depth", "View Distance"], ) - node_add_menu.add_node_type(layout, "ShaderNodeVertexColor") - node_add_menu.add_node_type_with_outputs( + self.node_operator(layout, "ShaderNodeVertexColor") + self.node_operator_with_outputs( context, layout, "ShaderNodeHairInfo", ["Is Strand", "Intercept", "Length", "Thickness", "Tangent Normal", "Random"], ) - node_add_menu.add_node_type(layout, "ShaderNodeFresnel") - node_add_menu.add_node_type_with_outputs( + self.node_operator(layout, "ShaderNodeFresnel") + self.node_operator_with_outputs( context, layout, "ShaderNodeNewGeometry", @@ -84,8 +83,8 @@ class NODE_MT_category_shader_input(Menu): "Random Per Island", ], ) - node_add_menu.add_node_type(layout, "ShaderNodeLayerWeight") - node_add_menu.add_node_type_with_outputs( + self.node_operator(layout, "ShaderNodeLayerWeight") + self.node_operator_with_outputs( context, layout, "ShaderNodeLightPath", @@ -107,358 +106,374 @@ class NODE_MT_category_shader_input(Menu): "Portal Depth" ], ) - node_add_menu.add_node_type_with_outputs( + self.node_operator_with_outputs( context, layout, "ShaderNodeObjectInfo", ["Location", "Color", "Alpha", "Object Index", "Material Index", "Random"], ) - node_add_menu.add_node_type_with_outputs( + self.node_operator_with_outputs( context, layout, "ShaderNodeParticleInfo", ["Index", "Random", "Age", "Lifetime", "Location", "Size", "Velocity", "Angular Velocity"], ) - node_add_menu.add_node_type_with_outputs( + self.node_operator_with_outputs( context, layout, "ShaderNodePointInfo", ["Position", "Radius", "Random"], ) - node_add_menu.add_node_type(layout, "ShaderNodeRGB") - node_add_menu.add_node_type(layout, "ShaderNodeTangent") - node_add_menu.add_node_type_with_outputs( + self.node_operator(layout, "ShaderNodeRGB") + self.node_operator(layout, "ShaderNodeTangent") + self.node_operator_with_outputs( context, layout, "ShaderNodeTexCoord", ["Normal", "UV", "Object", "Camera", "Window", "Reflection"], ) - node_add_menu.add_node_type(layout, "ShaderNodeUVAlongStroke", poll=line_style_shader_nodes_poll(context)) - node_add_menu.add_node_type(layout, "ShaderNodeUVMap") - node_add_menu.add_node_type(layout, "ShaderNodeValue") - node_add_menu.add_node_type_with_outputs( + self.node_operator(layout, "ShaderNodeUVAlongStroke", poll=line_style_shader_nodes_poll(context)) + self.node_operator(layout, "ShaderNodeUVMap") + self.node_operator(layout, "ShaderNodeValue") + self.node_operator_with_outputs( context, layout, "ShaderNodeVolumeInfo", ["Color", "Density", "Flame", "Temperature"], ) - node_add_menu.add_node_type(layout, "ShaderNodeWireframe") + self.node_operator(layout, "ShaderNodeWireframe") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_category_shader_output(Menu): - bl_idname = "NODE_MT_category_shader_output" +class NODE_MT_shader_node_output_base(node_add_menu.NodeMenu): bl_label = "Output" def draw(self, context): layout = self.layout - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeOutputAOV", ) - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeOutputLight", poll=object_not_eevee_shader_nodes_poll(context), ) - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeOutputLineStyle", poll=line_style_shader_nodes_poll(context), ) - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeOutputMaterial", poll=object_shader_nodes_poll(context), ) - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeOutputWorld", poll=world_shader_nodes_poll(context), ) - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_category_shader_shader(Menu): - bl_idname = "NODE_MT_category_shader_shader" +class NODE_MT_shader_node_shader_base(node_add_menu.NodeMenu): bl_label = "Shader" def draw(self, context): layout = self.layout - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeAddShader", ) - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeBackground", poll=world_shader_nodes_poll(context), ) - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeBsdfDiffuse", poll=object_shader_nodes_poll(context), ) - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeEmission", ) - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeBsdfGlass", poll=object_shader_nodes_poll(context), ) - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeBsdfGlossy", poll=object_shader_nodes_poll(context), ) - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeBsdfHair", poll=object_not_eevee_shader_nodes_poll(context), ) - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeHoldout", poll=object_shader_nodes_poll(context), ) - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeBsdfMetallic", poll=object_shader_nodes_poll(context), ) - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeMixShader", ) - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeBsdfPrincipled", poll=object_shader_nodes_poll(context), ) - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeBsdfHairPrincipled", poll=object_not_eevee_shader_nodes_poll(context), ) - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeVolumePrincipled" ) - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeBsdfRayPortal", poll=object_not_eevee_shader_nodes_poll(context), ) - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeBsdfRefraction", poll=object_shader_nodes_poll(context), ) - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeBsdfSheen", poll=object_not_eevee_shader_nodes_poll(context), ) - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeEeveeSpecular", poll=object_eevee_shader_nodes_poll(context), ) - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeSubsurfaceScattering", poll=object_shader_nodes_poll(context), ) - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeBsdfToon", poll=object_not_eevee_shader_nodes_poll(context), ) - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeBsdfTranslucent", poll=object_shader_nodes_poll(context), ) - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeBsdfTransparent", poll=object_shader_nodes_poll(context), ) - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeVolumeAbsorption", ) - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeVolumeScatter", ) - node_add_menu.add_node_type( + self.node_operator( layout, "ShaderNodeVolumeCoefficients", ) - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_category_shader_color(Menu): - bl_idname = "NODE_MT_category_shader_color" +class NODE_MT_shader_node_color_base(node_add_menu.NodeMenu): bl_label = "Color" def draw(self, context): layout = self.layout - node_add_menu.add_node_type(layout, "ShaderNodeBrightContrast") - node_add_menu.add_node_type(layout, "ShaderNodeGamma") - node_add_menu.add_node_type(layout, "ShaderNodeHueSaturation") - node_add_menu.add_node_type(layout, "ShaderNodeInvert") - node_add_menu.add_node_type(layout, "ShaderNodeLightFalloff") - node_add_menu.add_color_mix_node(context, layout) - node_add_menu.add_node_type(layout, "ShaderNodeRGBCurve") + self.node_operator(layout, "ShaderNodeBrightContrast") + self.node_operator(layout, "ShaderNodeGamma") + self.node_operator(layout, "ShaderNodeHueSaturation") + self.node_operator(layout, "ShaderNodeInvert") + self.node_operator(layout, "ShaderNodeLightFalloff") + self.color_mix_node(context, layout) + self.node_operator(layout, "ShaderNodeRGBCurve") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_category_shader_converter(Menu): - bl_idname = "NODE_MT_category_shader_converter" +class NODE_MT_shader_node_converter_base(node_add_menu.NodeMenu): bl_label = "Converter" def draw(self, context): layout = self.layout - node_add_menu.add_node_type(layout, "ShaderNodeBlackbody") - node_add_menu.add_node_type(layout, "ShaderNodeClamp") - node_add_menu.add_node_type(layout, "ShaderNodeValToRGB") - node_add_menu.add_node_type(layout, "ShaderNodeCombineColor") - node_add_menu.add_node_type(layout, "ShaderNodeCombineXYZ") - node_add_menu.add_node_type(layout, "ShaderNodeFloatCurve") - node_add_menu.add_node_type(layout, "ShaderNodeMapRange") - node_add_menu.add_node_type_with_searchable_enum(context, layout, "ShaderNodeMath", "operation") - node_add_menu.add_node_type(layout, "ShaderNodeMix") - node_add_menu.add_node_type(layout, "ShaderNodeRGBToBW") - node_add_menu.add_node_type(layout, "ShaderNodeSeparateColor") - node_add_menu.add_node_type(layout, "ShaderNodeSeparateXYZ") - node_add_menu.add_node_type(layout, "ShaderNodeShaderToRGB", poll=object_eevee_shader_nodes_poll(context)) - node_add_menu.add_node_type_with_searchable_enum(context, layout, "ShaderNodeVectorMath", "operation") - node_add_menu.add_node_type(layout, "ShaderNodeWavelength") + self.node_operator(layout, "ShaderNodeBlackbody") + self.node_operator(layout, "ShaderNodeClamp") + self.node_operator(layout, "ShaderNodeValToRGB") + self.node_operator(layout, "ShaderNodeCombineColor") + self.node_operator(layout, "ShaderNodeCombineXYZ") + self.node_operator(layout, "ShaderNodeFloatCurve") + self.node_operator(layout, "ShaderNodeMapRange") + self.node_operator_with_searchable_enum(context, layout, "ShaderNodeMath", "operation") + self.node_operator(layout, "ShaderNodeMix") + self.node_operator(layout, "ShaderNodeRGBToBW") + self.node_operator(layout, "ShaderNodeSeparateColor") + self.node_operator(layout, "ShaderNodeSeparateXYZ") + self.node_operator(layout, "ShaderNodeShaderToRGB", poll=object_eevee_shader_nodes_poll(context)) + self.node_operator_with_searchable_enum(context, layout, "ShaderNodeVectorMath", "operation") + self.node_operator(layout, "ShaderNodeWavelength") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_category_shader_texture(Menu): - bl_idname = "NODE_MT_category_shader_texture" +class NODE_MT_shader_node_texture_base(node_add_menu.NodeMenu): bl_label = "Texture" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "ShaderNodeTexBrick") - node_add_menu.add_node_type(layout, "ShaderNodeTexChecker") - node_add_menu.add_node_type(layout, "ShaderNodeTexEnvironment") - node_add_menu.add_node_type(layout, "ShaderNodeTexGabor") - node_add_menu.add_node_type(layout, "ShaderNodeTexGradient") - node_add_menu.add_node_type(layout, "ShaderNodeTexIES") - node_add_menu.add_node_type(layout, "ShaderNodeTexImage") - node_add_menu.add_node_type(layout, "ShaderNodeTexMagic") - node_add_menu.add_node_type(layout, "ShaderNodeTexNoise") - node_add_menu.add_node_type(layout, "ShaderNodeTexSky") - node_add_menu.add_node_type(layout, "ShaderNodeTexVoronoi") - node_add_menu.add_node_type(layout, "ShaderNodeTexWave") - node_add_menu.add_node_type(layout, "ShaderNodeTexWhiteNoise") + self.node_operator(layout, "ShaderNodeTexBrick") + self.node_operator(layout, "ShaderNodeTexChecker") + self.node_operator(layout, "ShaderNodeTexEnvironment") + self.node_operator(layout, "ShaderNodeTexGabor") + self.node_operator(layout, "ShaderNodeTexGradient") + self.node_operator(layout, "ShaderNodeTexIES") + self.node_operator(layout, "ShaderNodeTexImage") + self.node_operator(layout, "ShaderNodeTexMagic") + self.node_operator(layout, "ShaderNodeTexNoise") + self.node_operator(layout, "ShaderNodeTexSky") + self.node_operator(layout, "ShaderNodeTexVoronoi") + self.node_operator(layout, "ShaderNodeTexWave") + self.node_operator(layout, "ShaderNodeTexWhiteNoise") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_category_shader_vector(Menu): - bl_idname = "NODE_MT_category_shader_vector" +class NODE_MT_shader_node_vector_base(node_add_menu.NodeMenu): bl_label = "Vector" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "ShaderNodeBump") - node_add_menu.add_node_type(layout, "ShaderNodeDisplacement") - node_add_menu.add_node_type(layout, "ShaderNodeMapping") - node_add_menu.add_node_type(layout, "ShaderNodeNormal") - node_add_menu.add_node_type(layout, "ShaderNodeNormalMap") - node_add_menu.add_node_type(layout, "ShaderNodeRadialTiling") - node_add_menu.add_node_type(layout, "ShaderNodeVectorCurve") - node_add_menu.add_node_type(layout, "ShaderNodeVectorDisplacement") - node_add_menu.add_node_type(layout, "ShaderNodeVectorRotate") - node_add_menu.add_node_type(layout, "ShaderNodeVectorTransform") + self.node_operator(layout, "ShaderNodeBump") + self.node_operator(layout, "ShaderNodeDisplacement") + self.node_operator(layout, "ShaderNodeMapping") + self.node_operator(layout, "ShaderNodeNormal") + self.node_operator(layout, "ShaderNodeNormalMap") + self.node_operator(layout, "ShaderNodeRadialTiling") + self.node_operator(layout, "ShaderNodeVectorCurve") + self.node_operator(layout, "ShaderNodeVectorDisplacement") + self.node_operator(layout, "ShaderNodeVectorRotate") + self.node_operator(layout, "ShaderNodeVectorTransform") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_category_shader_script(Menu): - bl_idname = "NODE_MT_category_shader_script" +class NODE_MT_shader_node_script_base(node_add_menu.NodeMenu): bl_label = "Script" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "ShaderNodeScript") + self.node_operator(layout, "ShaderNodeScript") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) + self.draw_assets_for_catalog(layout, self.bl_label) -class NODE_MT_category_shader_group(Menu): - bl_idname = "NODE_MT_category_shader_group" - bl_label = "Group" - - def draw(self, context): - layout = self.layout - node_add_menu.draw_node_group_add_menu(context, layout) - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) - - -class NODE_MT_category_shader_utilities(Menu): - bl_idname = "NODE_MT_category_shader_utilities" +class NODE_MT_shader_node_utilities_base(node_add_menu.NodeMenu): bl_label = "Utilities" def draw(self, context): layout = self.layout - node_add_menu.add_repeat_zone(layout, label="Repeat") + self.repeat_zone(layout, label="Repeat") layout.separator() - node_add_menu.add_closure_zone(layout, label="Closure") - node_add_menu.add_node_type(layout, "NodeEvaluateClosure") - node_add_menu.add_node_type(layout, "NodeCombineBundle") - node_add_menu.add_node_type(layout, "NodeSeparateBundle") + self.closure_zone(layout, label="Closure") + self.node_operator(layout, "NodeEvaluateClosure") + self.node_operator(layout, "NodeCombineBundle") + self.node_operator(layout, "NodeSeparateBundle") -class NODE_MT_shader_node_add_all(Menu): - bl_idname = "NODE_MT_shader_node_add_all" - bl_label = "Add" +class NODE_MT_shader_node_all_base(node_add_menu.NodeMenu): + bl_label = "" + menu_path = "Root" bl_translation_context = i18n_contexts.operator_default - def draw(self, _context): + # NOTE: Menus are looked up via their label, this is so that both the Add + # & Swap menus can share the same layout while each using their + # corresponding menus + def draw(self, context): layout = self.layout - layout.menu("NODE_MT_category_shader_input") - layout.menu("NODE_MT_category_shader_output") + self.draw_menu(layout, "Input") + self.draw_menu(layout, "Output") layout.separator() - layout.menu("NODE_MT_category_shader_color") - layout.menu("NODE_MT_category_shader_converter") - layout.menu("NODE_MT_category_shader_shader") - layout.menu("NODE_MT_category_shader_texture") - layout.menu("NODE_MT_category_shader_vector") - layout.menu("NODE_MT_category_shader_utilities") + self.draw_menu(layout, "Color") + self.draw_menu(layout, "Converter") + self.draw_menu(layout, "Shader") + self.draw_menu(layout, "Texture") + self.draw_menu(layout, "Vector") + self.draw_menu(layout, "Utilities") layout.separator() - layout.menu("NODE_MT_category_shader_script") + self.draw_menu(layout, "Script") layout.separator() - layout.menu("NODE_MT_category_shader_group") - layout.menu("NODE_MT_category_layout") + self.draw_menu(layout, "Group") + self.draw_menu(layout, "Layout") - node_add_menu.draw_root_assets(layout) + self.draw_root_assets(layout) + + +add_menus = { + # menu bl_idname: baseclass + "NODE_MT_category_shader_input": NODE_MT_shader_node_input_base, + "NODE_MT_category_shader_output": NODE_MT_shader_node_output_base, + "NODE_MT_category_shader_color": NODE_MT_shader_node_color_base, + "NODE_MT_category_shader_converter": NODE_MT_shader_node_converter_base, + "NODE_MT_category_shader_shader": NODE_MT_shader_node_shader_base, + "NODE_MT_category_shader_texture": NODE_MT_shader_node_texture_base, + "NODE_MT_category_shader_vector": NODE_MT_shader_node_vector_base, + "NODE_MT_category_shader_script": NODE_MT_shader_node_script_base, + "NODE_MT_category_shader_utilities": NODE_MT_shader_node_utilities_base, + "NODE_MT_shader_node_add_all": NODE_MT_shader_node_all_base, +} +add_menus = node_add_menu.generate_menus( + add_menus, + template=node_add_menu.AddNodeMenu, + base_dict=node_add_menu.add_base_pathing_dict +) + + +swap_menus = { + # menu bl_idname: baseclass + "NODE_MT_shader_node_input_swap": NODE_MT_shader_node_input_base, + "NODE_MT_shader_node_output_swap": NODE_MT_shader_node_output_base, + "NODE_MT_shader_node_color_swap": NODE_MT_shader_node_color_base, + "NODE_MT_shader_node_converter_swap": NODE_MT_shader_node_converter_base, + "NODE_MT_shader_node_shader_swap": NODE_MT_shader_node_shader_base, + "NODE_MT_shader_node_texture_swap": NODE_MT_shader_node_texture_base, + "NODE_MT_shader_node_vector_swap": NODE_MT_shader_node_vector_base, + "NODE_MT_shader_node_script_swap": NODE_MT_shader_node_script_base, + "NODE_MT_shader_node_utilities_swap": NODE_MT_shader_node_utilities_base, + "NODE_MT_shader_node_swap_all": NODE_MT_shader_node_all_base, +} +swap_menus = node_add_menu.generate_menus( + swap_menus, + template=node_add_menu.SwapNodeMenu, + base_dict=node_add_menu.swap_base_pathing_dict +) classes = ( - NODE_MT_shader_node_add_all, - NODE_MT_category_shader_input, - NODE_MT_category_shader_output, - NODE_MT_category_shader_color, - NODE_MT_category_shader_converter, - NODE_MT_category_shader_shader, - NODE_MT_category_shader_texture, - NODE_MT_category_shader_vector, - NODE_MT_category_shader_script, - NODE_MT_category_shader_group, - NODE_MT_category_shader_utilities, + *add_menus, + *swap_menus, ) diff --git a/scripts/startup/bl_ui/node_add_menu_texture.py b/scripts/startup/bl_ui/node_add_menu_texture.py index 2417cd2f5fb..5846e8e06c9 100644 --- a/scripts/startup/bl_ui/node_add_menu_texture.py +++ b/scripts/startup/bl_ui/node_add_menu_texture.py @@ -9,136 +9,154 @@ from bpy.app.translations import ( from bl_ui import node_add_menu -class NODE_MT_category_texture_input(Menu): - bl_idname = "NODE_MT_category_texture_input" +class NODE_MT_texture_node_input_base(Menu): bl_label = "Input" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "TextureNodeCoordinates") - node_add_menu.add_node_type(layout, "TextureNodeCurveTime") - node_add_menu.add_node_type(layout, "TextureNodeImage") - node_add_menu.add_node_type(layout, "TextureNodeTexture") + self.node_operator(layout, "TextureNodeCoordinates") + self.node_operator(layout, "TextureNodeCurveTime") + self.node_operator(layout, "TextureNodeImage") + self.node_operator(layout, "TextureNodeTexture") -class NODE_MT_category_texture_output(Menu): - bl_idname = "NODE_MT_category_texture_output" +class NODE_MT_texture_node_output_base(Menu): bl_label = "Output" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "TextureNodeOutput") - node_add_menu.add_node_type(layout, "TextureNodeViewer") + self.node_operator(layout, "TextureNodeOutput") + self.node_operator(layout, "TextureNodeViewer") -class NODE_MT_category_texture_color(Menu): - bl_idname = "NODE_MT_category_texture_color" +class NODE_MT_texture_node_color_base(Menu): bl_label = "Color" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "TextureNodeHueSaturation") - node_add_menu.add_node_type(layout, "TextureNodeInvert") - node_add_menu.add_node_type(layout, "TextureNodeMixRGB") - node_add_menu.add_node_type(layout, "TextureNodeCurveRGB") + self.node_operator(layout, "TextureNodeHueSaturation") + self.node_operator(layout, "TextureNodeInvert") + self.node_operator(layout, "TextureNodeMixRGB") + self.node_operator(layout, "TextureNodeCurveRGB") layout.separator() - node_add_menu.add_node_type(layout, "TextureNodeCombineColor") - node_add_menu.add_node_type(layout, "TextureNodeSeparateColor") + self.node_operator(layout, "TextureNodeCombineColor") + self.node_operator(layout, "TextureNodeSeparateColor") -class NODE_MT_category_texture_converter(Menu): - bl_idname = "NODE_MT_category_texture_converter" +class NODE_MT_texture_node_converter_base(Menu): bl_label = "Converter" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "TextureNodeValToRGB") - node_add_menu.add_node_type(layout, "TextureNodeDistance") - node_add_menu.add_node_type(layout, "TextureNodeMath") - node_add_menu.add_node_type(layout, "TextureNodeRGBToBW") - node_add_menu.add_node_type(layout, "TextureNodeValToNor") + self.node_operator(layout, "TextureNodeValToRGB") + self.node_operator(layout, "TextureNodeDistance") + self.node_operator(layout, "TextureNodeMath") + self.node_operator(layout, "TextureNodeRGBToBW") + self.node_operator(layout, "TextureNodeValToNor") -class NODE_MT_category_texture_distort(Menu): - bl_idname = "NODE_MT_category_texture_distort" +class NODE_MT_texture_node_distort_base(Menu): bl_label = "Distort" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "TextureNodeAt") - node_add_menu.add_node_type(layout, "TextureNodeRotate") - node_add_menu.add_node_type(layout, "TextureNodeScale") - node_add_menu.add_node_type(layout, "TextureNodeTranslate") + self.node_operator(layout, "TextureNodeAt") + self.node_operator(layout, "TextureNodeRotate") + self.node_operator(layout, "TextureNodeScale") + self.node_operator(layout, "TextureNodeTranslate") -class NODE_MT_category_texture_pattern(Menu): - bl_idname = "NODE_MT_category_texture_pattern" +class NODE_MT_texture_node_pattern_base(Menu): bl_label = "Pattern" bl_translation_context = i18n_contexts.id_texture def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "TextureNodeBricks") - node_add_menu.add_node_type(layout, "TextureNodeChecker") + self.node_operator(layout, "TextureNodeBricks") + self.node_operator(layout, "TextureNodeChecker") -class NODE_MT_category_texture_texture(Menu): - bl_idname = "NODE_MT_category_texture_texture" +class NODE_MT_texture_node_texture_base(Menu): bl_label = "Texture" def draw(self, _context): layout = self.layout - node_add_menu.add_node_type(layout, "TextureNodeTexBlend") - node_add_menu.add_node_type(layout, "TextureNodeTexClouds") - node_add_menu.add_node_type(layout, "TextureNodeTexDistNoise") - node_add_menu.add_node_type(layout, "TextureNodeTexMagic") - node_add_menu.add_node_type(layout, "TextureNodeTexMarble") - node_add_menu.add_node_type(layout, "TextureNodeTexMusgrave") - node_add_menu.add_node_type(layout, "TextureNodeTexNoise") - node_add_menu.add_node_type(layout, "TextureNodeTexStucci") - node_add_menu.add_node_type(layout, "TextureNodeTexVoronoi") - node_add_menu.add_node_type(layout, "TextureNodeTexWood") + self.node_operator(layout, "TextureNodeTexBlend") + self.node_operator(layout, "TextureNodeTexClouds") + self.node_operator(layout, "TextureNodeTexDistNoise") + self.node_operator(layout, "TextureNodeTexMagic") + self.node_operator(layout, "TextureNodeTexMarble") + self.node_operator(layout, "TextureNodeTexMusgrave") + self.node_operator(layout, "TextureNodeTexNoise") + self.node_operator(layout, "TextureNodeTexStucci") + self.node_operator(layout, "TextureNodeTexVoronoi") + self.node_operator(layout, "TextureNodeTexWood") -class NODE_MT_category_texture_group(Menu): - bl_idname = "NODE_MT_category_texture_group" - bl_label = "Group" - - def draw(self, context): - layout = self.layout - node_add_menu.draw_node_group_add_menu(context, layout) - - -class NODE_MT_texture_node_add_all(Menu): - bl_idname = "NODE_MT_texture_node_add_all" - bl_label = "Add" +class NODE_MT_texture_node_all_base(node_add_menu.NodeMenu): + bl_label = "" + menu_path = "Root" bl_translation_context = i18n_contexts.operator_default - def draw(self, _context): + # NOTE: Menus are looked up via their label, this is so that both the Add + # & Swap menus can share the same layout while each using their + # corresponding menus + def draw(self, context): layout = self.layout - layout.menu("NODE_MT_category_texture_input") - layout.menu("NODE_MT_category_texture_output") + self.draw_menu(layout, "Input") + self.draw_menu(layout, "Output") layout.separator() - layout.menu("NODE_MT_category_texture_color") - layout.menu("NODE_MT_category_texture_converter") - layout.menu("NODE_MT_category_texture_distort") - layout.menu("NODE_MT_category_texture_pattern") - layout.menu("NODE_MT_category_texture_texture") + self.draw_menu(layout, "Color") + self.draw_menu(layout, "Converter") + self.draw_menu(layout, "Distort") + self.draw_menu(layout, "Pattern") + self.draw_menu(layout, "Texture") layout.separator() - layout.menu("NODE_MT_category_texture_group") - layout.menu("NODE_MT_category_layout") + self.draw_menu(layout, "Group") + self.draw_menu(layout, "Layout") + + self.draw_root_assets(layout) + + +add_menus = { + # menu bl_idname: baseclass + "NODE_MT_category_texture_input": NODE_MT_texture_node_input_base, + "NODE_MT_category_texture_output": NODE_MT_texture_node_output_base, + "NODE_MT_category_texture_color": NODE_MT_texture_node_color_base, + "NODE_MT_category_texture_converter": NODE_MT_texture_node_converter_base, + "NODE_MT_category_texture_distort": NODE_MT_texture_node_distort_base, + "NODE_MT_category_texture_pattern": NODE_MT_texture_node_pattern_base, + "NODE_MT_category_texture_texture": NODE_MT_texture_node_texture_base, + "NODE_MT_texture_node_add_all": NODE_MT_texture_node_all_base, +} +add_menus = node_add_menu.generate_menus( + add_menus, + template=node_add_menu.AddNodeMenu, + base_dict=node_add_menu.add_base_pathing_dict +) + + +swap_menus = { + # menu bl_idname: baseclass + "NODE_MT_texture_node_input_swap": NODE_MT_texture_node_input_base, + "NODE_MT_texture_node_output_swap": NODE_MT_texture_node_output_base, + "NODE_MT_texture_node_color_swap": NODE_MT_texture_node_color_base, + "NODE_MT_texture_node_converter_swap": NODE_MT_texture_node_converter_base, + "NODE_MT_texture_node_distort_swap": NODE_MT_texture_node_distort_base, + "NODE_MT_texture_node_pattern_swap": NODE_MT_texture_node_pattern_base, + "NODE_MT_texture_node_texture_swap": NODE_MT_texture_node_texture_base, + "NODE_MT_texture_node_swap_all": NODE_MT_texture_node_all_base, +} +swap_menus = node_add_menu.generate_menus( + swap_menus, + template=node_add_menu.SwapNodeMenu, + base_dict=node_add_menu.swap_base_pathing_dict +) classes = ( - NODE_MT_texture_node_add_all, - NODE_MT_category_texture_input, - NODE_MT_category_texture_output, - NODE_MT_category_texture_color, - NODE_MT_category_texture_converter, - NODE_MT_category_texture_distort, - NODE_MT_category_texture_pattern, - NODE_MT_category_texture_texture, - NODE_MT_category_texture_group, + *add_menus, + *swap_menus, ) diff --git a/scripts/startup/bl_ui/space_node.py b/scripts/startup/bl_ui/space_node.py index f5cca87c3bd..98453ce6780 100644 --- a/scripts/startup/bl_ui/space_node.py +++ b/scripts/startup/bl_ui/space_node.py @@ -14,7 +14,7 @@ from bpy.app.translations import ( pgettext_iface as iface_, contexts as i18n_contexts, ) -from bl_ui import anim +from bl_ui import anim, node_add_menu from bl_ui.utils import PresetPanel from bl_ui.properties_grease_pencil_common import ( AnnotationDataPanel, @@ -263,10 +263,11 @@ class NODE_MT_editor_menus(Menu): layout.menu("NODE_MT_view") layout.menu("NODE_MT_select") layout.menu("NODE_MT_add") + layout.menu("NODE_MT_swap") layout.menu("NODE_MT_node") -class NODE_MT_add(Menu): +class NODE_MT_add(node_add_menu.AddNodeMenu): bl_space_type = 'NODE_EDITOR' bl_label = "Add" bl_translation_context = i18n_contexts.operator_default @@ -298,6 +299,33 @@ class NODE_MT_add(Menu): nodeitems_utils.draw_node_categories_menu(self, context) +class NODE_MT_swap(node_add_menu.SwapNodeMenu): + bl_space_type = 'NODE_EDITOR' + bl_label = "Swap" + bl_translation_context = i18n_contexts.operator_default + bl_options = {'SEARCH_ON_KEY_PRESS'} + + def draw(self, context): + layout = self.layout + + if layout.operator_context == 'EXEC_REGION_WIN': + layout.operator_context = 'INVOKE_REGION_WIN' + layout.operator("WM_OT_search_single_menu", text="Search...", icon='VIEWZOOM').menu_idname = "NODE_MT_swap" + layout.separator() + + layout.operator_context = 'INVOKE_REGION_WIN' + + snode = context.space_data + if snode.tree_type == 'GeometryNodeTree': + layout.menu_contents("NODE_MT_geometry_node_swap_all") + elif snode.tree_type == 'CompositorNodeTree': + layout.menu_contents("NODE_MT_compositor_node_swap_all") + elif snode.tree_type == 'ShaderNodeTree': + layout.menu_contents("NODE_MT_shader_node_swap_all") + elif snode.tree_type == 'TextureNodeTree': + layout.menu_contents("NODE_MT_texture_node_swap_all") + + class NODE_MT_view(Menu): bl_label = "View" @@ -1216,13 +1244,14 @@ classes = ( NODE_HT_header, NODE_MT_editor_menus, NODE_MT_add, - NODE_MT_view, + NODE_MT_swap, NODE_MT_select, NODE_MT_node, NODE_MT_node_color_context_menu, NODE_MT_context_menu_show_hide_menu, NODE_MT_context_menu_select_menu, NODE_MT_context_menu, + NODE_MT_view, NODE_MT_view_pie, NODE_PT_material_slots, NODE_PT_geometry_node_tool_object_types, diff --git a/scripts/templates_py/custom_nodes.py b/scripts/templates_py/custom_nodes.py index bb6dcf0114e..63c7e3c3076 100644 --- a/scripts/templates_py/custom_nodes.py +++ b/scripts/templates_py/custom_nodes.py @@ -132,14 +132,14 @@ class MyCustomNode(MyCustomTreeNode, Node): return "I am a custom node" -# Add custom nodes to the Add menu. +# Add custom nodes to the Add & Swap menu. def draw_add_menu(self, context): layout = self.layout if context.space_data.tree_type != MyCustomTree.bl_idname: # Avoid adding nodes to built-in node tree return # Add nodes to the layout. Can use submenus, separators, etc. as in any other menu. - node_add_menu.add_node_type(layout, "CustomNodeType") + self.node_operator(layout, "CustomNodeType") classes = ( @@ -156,10 +156,12 @@ def register(): register_class(cls) bpy.types.NODE_MT_add.append(draw_add_menu) + bpy.types.NODE_MT_swap.append(draw_add_menu) def unregister(): bpy.types.NODE_MT_add.remove(draw_add_menu) + bpy.types.NODE_MT_swap.remove(draw_add_menu) from bpy.utils import unregister_class for cls in reversed(classes): diff --git a/source/blender/editors/asset/intern/asset_menu_utils.cc b/source/blender/editors/asset/intern/asset_menu_utils.cc index b3894b23189..590cbe4e3ef 100644 --- a/source/blender/editors/asset/intern/asset_menu_utils.cc +++ b/source/blender/editors/asset/intern/asset_menu_utils.cc @@ -152,4 +152,15 @@ void draw_menu_for_catalog(const asset_system::AssetCatalogTreeItem &item, col->menu(menu_name, IFACE_(item.get_name()), ICON_NONE); } +void draw_node_menu_for_catalog(const asset_system::AssetCatalogTreeItem &item, + const StringRefNull operator_id, + const StringRefNull menu_name, + uiLayout &layout) +{ + uiLayout *col = &layout.column(false); + col->context_string_set("asset_catalog_path", item.catalog_path().c_str()); + col->context_string_set("operator_id", operator_id); + col->menu(menu_name, IFACE_(item.get_name()), ICON_NONE); +} + } // namespace blender::ed::asset diff --git a/source/blender/editors/include/ED_asset_menu_utils.hh b/source/blender/editors/include/ED_asset_menu_utils.hh index ff7d7287a57..63aa79a46a6 100644 --- a/source/blender/editors/include/ED_asset_menu_utils.hh +++ b/source/blender/editors/include/ED_asset_menu_utils.hh @@ -29,6 +29,11 @@ void draw_menu_for_catalog(const asset_system::AssetCatalogTreeItem &item, StringRefNull menu_name, uiLayout &layout); +void draw_node_menu_for_catalog(const asset_system::AssetCatalogTreeItem &item, + StringRefNull operator_id, + StringRefNull menu_name, + uiLayout &layout); + void operator_asset_reference_props_set(const asset_system::AssetRepresentation &asset, PointerRNA &ptr); bool operator_asset_reference_props_is_set(PointerRNA &ptr); diff --git a/source/blender/editors/include/ED_node.hh b/source/blender/editors/include/ED_node.hh index abe390b79e9..f586a3db52e 100644 --- a/source/blender/editors/include/ED_node.hh +++ b/source/blender/editors/include/ED_node.hh @@ -20,6 +20,8 @@ #include "ED_node_c.hh" +#include "UI_interface_layout.hh" + struct SpaceNode; struct ARegion; struct Main; @@ -136,7 +138,8 @@ bool node_editor_is_for_geometry_nodes_modifier(const SpaceNode &snode, void ui_template_node_asset_menu_items(uiLayout &layout, const bContext &C, - StringRef catalog_path); + StringRef catalog_path, + const NodeAssetMenuOperatorType operator_type); /** See #SpaceNode_Runtime::node_can_sync_states. */ Map &node_can_sync_cache_get(SpaceNode &snode); diff --git a/source/blender/editors/include/UI_interface_layout.hh b/source/blender/editors/include/UI_interface_layout.hh index 369b823fc28..2de95d3de08 100644 --- a/source/blender/editors/include/UI_interface_layout.hh +++ b/source/blender/editors/include/UI_interface_layout.hh @@ -89,6 +89,11 @@ enum class LayoutSeparatorType : int8_t { Line, }; +enum class NodeAssetMenuOperatorType : int8_t { + Add, + Swap, +}; + /** * NOTE: `uiLayout` properties should be considered private outside `interface_layout.cc`, * incoming refactors would remove public access and add public read/write function methods. diff --git a/source/blender/editors/space_node/add_menu_assets.cc b/source/blender/editors/space_node/add_menu_assets.cc index 74b1784dc94..17b7e2ab3c5 100644 --- a/source/blender/editors/space_node/add_menu_assets.cc +++ b/source/blender/editors/space_node/add_menu_assets.cc @@ -153,7 +153,7 @@ static Set get_builtin_menus(const int tree_type) return {}; } -static void node_add_catalog_assets_draw(const bContext *C, Menu *menu) +static void node_catalog_assets_draw(const bContext *C, Menu *menu) { SpaceNode &snode = *CTX_wm_space_node(C); const bNodeTree *edit_tree = snode.edittree; @@ -172,6 +172,12 @@ static void node_add_catalog_assets_draw(const bContext *C, Menu *menu) if (!menu_path) { return; } + + const std::optional operator_id = CTX_data_string_get(C, "operator_id"); + if (!operator_id) { + return; + } + const Span assets = tree.assets_per_path.lookup( menu_path->c_str()); const asset_system::AssetCatalogTreeItem *catalog_item = tree.catalogs.find_item( @@ -190,7 +196,7 @@ static void node_add_catalog_assets_draw(const bContext *C, Menu *menu) layout->separator(); add_separator = false; } - PointerRNA op_ptr = layout->op("NODE_OT_add_group_asset", + PointerRNA op_ptr = layout->op(*operator_id, IFACE_(asset->get_name()), ICON_NONE, wm::OpCallContext::InvokeRegionWin, @@ -208,17 +214,23 @@ static void node_add_catalog_assets_draw(const bContext *C, Menu *menu) layout->separator(); add_separator = false; } - asset::draw_menu_for_catalog(item, "NODE_MT_node_add_catalog_assets", *layout); + asset::draw_node_menu_for_catalog(item, *operator_id, "NODE_MT_node_catalog_assets", *layout); }); } -static void node_add_unassigned_assets_draw(const bContext *C, Menu *menu) +static void node_unassigned_assets_draw(const bContext *C, Menu *menu) { SpaceNode &snode = *CTX_wm_space_node(C); const bNodeTree *edit_tree = snode.edittree; if (!edit_tree) { return; } + + const std::optional operator_id = CTX_data_string_get(C, "operator_id"); + if (!operator_id) { + return; + } + if (!snode.runtime->assets_for_menu) { snode.runtime->assets_for_menu = std::make_shared( build_catalog_tree(*C, *edit_tree)); @@ -226,7 +238,7 @@ static void node_add_unassigned_assets_draw(const bContext *C, Menu *menu) } asset::AssetItemTree &tree = *snode.runtime->assets_for_menu; for (const asset_system::AssetRepresentation *asset : tree.unassigned_assets) { - PointerRNA op_ptr = menu->layout->op("NODE_OT_add_group_asset", + PointerRNA op_ptr = menu->layout->op(*operator_id, IFACE_(asset->get_name()), ICON_NONE, wm::OpCallContext::InvokeRegionWin, @@ -235,7 +247,7 @@ static void node_add_unassigned_assets_draw(const bContext *C, Menu *menu) } } -static void add_root_catalogs_draw(const bContext *C, Menu *menu) +static void root_catalogs_draw(const bContext *C, Menu *menu, const StringRefNull operator_id) { SpaceNode &snode = *CTX_wm_space_node(C); uiLayout *layout = menu->layout; @@ -264,33 +276,49 @@ static void add_root_catalogs_draw(const bContext *C, Menu *menu) tree.catalogs.foreach_root_item([&](const asset_system::AssetCatalogTreeItem &item) { if (!all_builtin_menus.contains_as(item.catalog_path().str())) { - asset::draw_menu_for_catalog(item, "NODE_MT_node_add_catalog_assets", *layout); + asset::draw_node_menu_for_catalog(item, operator_id, "NODE_MT_node_catalog_assets", *layout); } }); if (!tree.unassigned_assets.is_empty()) { layout->separator(); - layout->menu("NODE_MT_node_add_unassigned_assets", IFACE_("Unassigned"), ICON_FILE_HIDDEN); + layout->menu("NODE_MT_node_unassigned_assets", IFACE_("Unassigned"), ICON_FILE_HIDDEN); } } -MenuType add_catalog_assets_menu_type() +static void add_root_catalogs_draw(const bContext *C, Menu *menu) +{ + const StringRefNull operator_id = "NODE_OT_add_group_asset"; + + menu->layout->context_string_set("operator_id", operator_id); + root_catalogs_draw(C, menu, operator_id); +} + +static void swap_root_catalogs_draw(const bContext *C, Menu *menu) +{ + const StringRefNull operator_id = "NODE_OT_swap_group_asset"; + + menu->layout->context_string_set("operator_id", operator_id); + root_catalogs_draw(C, menu, operator_id); +} + +MenuType catalog_assets_menu_type() { MenuType type{}; - STRNCPY_UTF8(type.idname, "NODE_MT_node_add_catalog_assets"); + STRNCPY_UTF8(type.idname, "NODE_MT_node_catalog_assets"); type.poll = node_add_menu_poll; - type.draw = node_add_catalog_assets_draw; + type.draw = node_catalog_assets_draw; type.listener = asset::list::asset_reading_region_listen_fn; type.flag = MenuTypeFlag::ContextDependent; return type; } -MenuType add_unassigned_assets_menu_type() +MenuType unassigned_assets_menu_type() { MenuType type{}; - STRNCPY_UTF8(type.idname, "NODE_MT_node_add_unassigned_assets"); + STRNCPY_UTF8(type.idname, "NODE_MT_node_unassigned_assets"); type.poll = node_add_menu_poll; - type.draw = node_add_unassigned_assets_draw; + type.draw = node_unassigned_assets_draw; type.listener = asset::list::asset_reading_region_listen_fn; type.flag = MenuTypeFlag::ContextDependent; type.description = N_( @@ -309,9 +337,20 @@ MenuType add_root_catalogs_menu_type() return type; } +MenuType swap_root_catalogs_menu_type() +{ + MenuType type{}; + STRNCPY_UTF8(type.idname, "NODE_MT_node_swap_root_catalogs"); + type.poll = node_add_menu_poll; + type.draw = swap_root_catalogs_draw; + type.listener = asset::list::asset_reading_region_listen_fn; + return type; +} + void ui_template_node_asset_menu_items(uiLayout &layout, const bContext &C, - const StringRef catalog_path) + const StringRef catalog_path, + const NodeAssetMenuOperatorType operator_type) { SpaceNode &snode = *CTX_wm_space_node(&C); if (snode.runtime->assets_for_menu == nullptr) { @@ -322,9 +361,21 @@ void ui_template_node_asset_menu_items(uiLayout &layout, if (!item) { return; } + + StringRef operator_id; + + switch (operator_type) { + case NodeAssetMenuOperatorType::Swap: + operator_id = "NODE_OT_swap_group_asset"; + break; + default: + operator_id = "NODE_OT_add_group_asset"; + } + uiLayout *col = &layout.column(false); col->context_string_set("asset_catalog_path", item->catalog_path().str()); - col->menu_contents("NODE_MT_node_add_catalog_assets"); + col->context_string_set("operator_id", operator_id); + col->menu_contents("NODE_MT_node_catalog_assets"); } } // namespace blender::ed::space_node diff --git a/source/blender/editors/space_node/node_add.cc b/source/blender/editors/space_node/node_add.cc index bd06ecedd96..fca972e7e95 100644 --- a/source/blender/editors/space_node/node_add.cc +++ b/source/blender/editors/space_node/node_add.cc @@ -361,6 +361,27 @@ static bool node_add_group_poll(bContext *C) return true; } +static bool node_swap_group_poll(bContext *C) +{ + if (!ED_operator_node_editable(C)) { + return false; + } + const SpaceNode *snode = CTX_wm_space_node(C); + if (snode->edittree->type == NTREE_CUSTOM) { + CTX_wm_operator_poll_msg_set( + C, "Adding node groups isn't supported for custom (Python defined) node trees"); + return false; + } + Vector selected_nodes; + selected_nodes = CTX_data_collection_get(C, "selected_nodes"); + + if (selected_nodes.size() <= 0) { + CTX_wm_operator_poll_msg_set(C, "No nodes selected."); + return false; + } + return true; +} + static wmOperatorStatus node_add_group_invoke(bContext *C, wmOperator *op, const wmEvent *event) { ARegion *region = CTX_wm_region(C); @@ -490,6 +511,78 @@ static wmOperatorStatus node_add_group_asset_invoke(bContext *C, return OPERATOR_FINISHED; } +static wmOperatorStatus node_swap_group_asset_invoke(bContext *C, + wmOperator *op, + const wmEvent *event) +{ + ARegion ®ion = *CTX_wm_region(C); + Main &bmain = *CTX_data_main(C); + SpaceNode &snode = *CTX_wm_space_node(C); + bNodeTree &ntree = *snode.edittree; + + const asset_system::AssetRepresentation *asset = + asset::operator_asset_reference_props_get_asset_from_all_library(*C, *op->ptr, op->reports); + if (!asset) { + return OPERATOR_CANCELLED; + } + bNodeTree *node_group = reinterpret_cast( + asset::asset_local_id_ensure_imported(bmain, *asset)); + + /* Convert mouse coordinates to v2d space. */ + UI_view2d_region_to_view(®ion.v2d, + event->mval[0], + event->mval[1], + &snode.runtime->cursor[0], + &snode.runtime->cursor[1]); + + snode.runtime->cursor /= UI_SCALE_FAC; + + const StringRef node_idname = node_group_idname(C); + if (node_idname[0] == '\0') { + BKE_report(op->reports, RPT_WARNING, "Could not determine type of group node"); + return OPERATOR_CANCELLED; + } + wmOperatorType *ot = WM_operatortype_find("NODE_OT_swap_node", true); + BLI_assert(ot); + PointerRNA ptr; + PointerRNA itemptr; + WM_operator_properties_create_ptr(&ptr, ot); + RNA_string_set(&ptr, "type", node_idname.data()); + + /* Assign node group via operator.settings. This needs to be done here so that NODE_OT_swap_node + * can preserve matching links */ + /* Assigning it in the for-loop along with the other node group properties causes the links to + * not be preserved*/ + RNA_collection_add(&ptr, "settings", &itemptr); + RNA_string_set(&itemptr, "name", "node_tree"); + + std::string setting_value = "bpy.data.node_groups[\"" + + std::string(BKE_id_name(node_group->id)) + "\"]"; + RNA_string_set(&itemptr, "value", setting_value.c_str()); + + WM_operator_name_call_ptr(C, ot, wm::OpCallContext::InvokeDefault, &ptr, nullptr); + WM_operator_properties_free(&ptr); + + for (bNode *group_node : get_selected_nodes(ntree)) { + STRNCPY_UTF8(group_node->name, BKE_id_name(node_group->id)); + bke::node_unique_name(*snode.edittree, *group_node); + + /* By default, don't show the data-block selector since it's not usually necessary for assets. + */ + group_node->flag &= ~NODE_OPTIONS; + group_node->width = node_group->default_group_node_width; + + id_us_plus(group_node->id); + BKE_ntree_update_tag_node_property(&ntree, group_node); + } + + BKE_main_ensure_invariants(bmain); + WM_event_add_notifier(C, NC_NODE | NA_ADDED, nullptr); + DEG_relations_tag_update(&bmain); + + return OPERATOR_FINISHED; +} + static std::string node_add_group_asset_get_description(bContext *C, wmOperatorType * /*ot*/, PointerRNA *ptr) @@ -521,6 +614,21 @@ void NODE_OT_add_group_asset(wmOperatorType *ot) asset::operator_asset_reference_props_register(*ot->srna); } +void NODE_OT_swap_group_asset(wmOperatorType *ot) +{ + ot->name = "Swap Node Group Asset"; + ot->description = "Swap selected nodes with the specified node group asset"; + ot->idname = "NODE_OT_swap_group_asset"; + + ot->invoke = node_swap_group_asset_invoke; + ot->poll = node_swap_group_poll; + ot->get_description = node_add_group_asset_get_description; + + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO | OPTYPE_INTERNAL; + + asset::operator_asset_reference_props_register(*ot->srna); +} + /** \} */ /* -------------------------------------------------------------------- */ diff --git a/source/blender/editors/space_node/node_intern.hh b/source/blender/editors/space_node/node_intern.hh index 584e823a38f..07e26ec4d4e 100644 --- a/source/blender/editors/space_node/node_intern.hh +++ b/source/blender/editors/space_node/node_intern.hh @@ -307,6 +307,7 @@ void NODE_OT_add_mask(wmOperatorType *ot); void NODE_OT_add_material(wmOperatorType *ot); void NODE_OT_add_color(wmOperatorType *ot); void NODE_OT_add_import_node(wmOperatorType *ot); +void NODE_OT_swap_group_asset(wmOperatorType *ot); void NODE_OT_new_node_tree(wmOperatorType *ot); void NODE_OT_new_compositing_node_group(wmOperatorType *ot); void NODE_OT_add_group_input_node(wmOperatorType *ot); @@ -444,10 +445,12 @@ void invoke_node_link_drag_add_menu(bContext &C, /* `add_menu_assets.cc` */ -MenuType add_catalog_assets_menu_type(); -MenuType add_unassigned_assets_menu_type(); +MenuType catalog_assets_menu_type(); +MenuType unassigned_assets_menu_type(); MenuType add_root_catalogs_menu_type(); +MenuType swap_root_catalogs_menu_type(); + /* `node_sync_sockets.cc` */ void NODE_OT_sockets_sync(wmOperatorType *ot); diff --git a/source/blender/editors/space_node/node_ops.cc b/source/blender/editors/space_node/node_ops.cc index a386a03a700..94e5fad6607 100644 --- a/source/blender/editors/space_node/node_ops.cc +++ b/source/blender/editors/space_node/node_ops.cc @@ -91,6 +91,8 @@ void node_operatortypes() WM_operatortype_append(NODE_OT_add_import_node); WM_operatortype_append(NODE_OT_add_group_input_node); + WM_operatortype_append(NODE_OT_swap_group_asset); + WM_operatortype_append(NODE_OT_new_node_tree); WM_operatortype_append(NODE_OT_new_compositing_node_group); diff --git a/source/blender/editors/space_node/space_node.cc b/source/blender/editors/space_node/space_node.cc index 7f7092d1d7c..27e45151811 100644 --- a/source/blender/editors/space_node/space_node.cc +++ b/source/blender/editors/space_node/space_node.cc @@ -1884,9 +1884,10 @@ void ED_spacetype_node() art->draw = node_toolbar_region_draw; BLI_addhead(&st->regiontypes, art); - WM_menutype_add(MEM_dupallocN(__func__, add_catalog_assets_menu_type())); - WM_menutype_add(MEM_dupallocN(__func__, add_unassigned_assets_menu_type())); + WM_menutype_add(MEM_dupallocN(__func__, catalog_assets_menu_type())); + WM_menutype_add(MEM_dupallocN(__func__, unassigned_assets_menu_type())); WM_menutype_add(MEM_dupallocN(__func__, add_root_catalogs_menu_type())); + WM_menutype_add(MEM_dupallocN(__func__, swap_root_catalogs_menu_type())); BKE_spacetype_register(std::move(st)); } diff --git a/source/blender/makesrna/intern/rna_ui_api.cc b/source/blender/makesrna/intern/rna_ui_api.cc index 5f09fcebefe..da1afdc5ff7 100644 --- a/source/blender/makesrna/intern/rna_ui_api.cc +++ b/source/blender/makesrna/intern/rna_ui_api.cc @@ -898,10 +898,12 @@ void rna_uiLayoutPanel(uiLayout *layout, static void rna_uiLayout_template_node_asset_menu_items(uiLayout *layout, bContext *C, - const char *catalog_path) + const char *catalog_path, + const int operator_type) { using namespace blender; - ed::space_node::ui_template_node_asset_menu_items(*layout, *C, StringRef(catalog_path)); + ed::space_node::ui_template_node_asset_menu_items( + *layout, *C, StringRef(catalog_path), NodeAssetMenuOperatorType(operator_type)); } static void rna_uiLayout_template_node_operator_asset_menu_items(uiLayout *layout, @@ -1201,6 +1203,20 @@ void RNA_api_ui_layout(StructRNA *srna) {0, nullptr, 0, nullptr, nullptr}, }; + static const EnumPropertyItem rna_enum_template_node_operator_type[] = { + {int(NodeAssetMenuOperatorType::Add), + "ADD", + 0, + "Add Node", + "Add a node to the active tree."}, + {int(NodeAssetMenuOperatorType::Swap), + "SWAP", + 0, + "Swap Node", + "Replace the selected nodes with the specified type."}, + {0, nullptr, 0, nullptr, nullptr}, + }; + static const float node_socket_color_default[] = {0.0f, 0.0f, 0.0f, 1.0f}; /* simple layout specifiers */ @@ -2123,6 +2139,12 @@ void RNA_api_ui_layout(StructRNA *srna) srna, "template_node_asset_menu_items", "rna_uiLayout_template_node_asset_menu_items"); RNA_def_function_flag(func, FUNC_USE_CONTEXT); parm = RNA_def_string(func, "catalog_path", nullptr, 0, "", ""); + parm = RNA_def_enum(func, + "operator", + rna_enum_template_node_operator_type, + int(NodeAssetMenuOperatorType::Add), + "Operator", + "The operator the asset menu will use"); func = RNA_def_function(srna, "template_modifier_asset_menu_items",