Geometry Nodes: move socket items ui and operators from Python to C++

This has some benefits:
* Nodes with dynamic socket amounts can remain more self-contained
  (2 fewer files to edit with this patch).
* It's easier to reuse existing C++ code, reducing redundancy.

One new thing I'm doing here is to define operators in node files. It seems
reasonable to register operators that belong to a node together with that
node. Without this, code spreads out further than necessary without any real benefit.

This patch affects the simulation zone, repeat zone, bake node and index switch node.

The UI is slightly affected too. Since we had the UI defined in Python before,
it wasn't possible to integrate it into the node properties panel. That is possible
now and looks better anyway. The previous UI was an artifact of technical limitations.

Pull Request: https://projects.blender.org/blender/blender/pulls/121178
This commit is contained in:
Jacques Lucke
2024-04-29 19:50:11 +02:00
parent 3847cf4dc1
commit 0c585a1b8a
10 changed files with 789 additions and 615 deletions

View File

@@ -361,242 +361,9 @@ class ZoneOperator:
return True
class NodeOperator:
@classmethod
def get_node(cls, context):
node = context.active_node
if node is None:
return None
if node.bl_idname == cls.node_type:
return node
@classmethod
def poll(cls, context):
space = context.space_data
# Needs active node editor and a tree.
if not space or space.type != 'NODE_EDITOR' or not space.edit_tree or space.edit_tree.library:
return False
node = cls.get_node(context)
if node is None:
return False
return True
class SocketItemAddOperator:
items_name = None
active_index_name = None
default_socket_type = 'GEOMETRY'
def execute(self, context):
node = self.get_node(context)
items = getattr(node, self.items_name)
# Remember index to move the item.
old_active_index = getattr(node, self.active_index_name)
if 0 <= old_active_index < len(items):
old_active_item = items[old_active_index]
dst_index = old_active_index + 1
dst_type = old_active_item.socket_type
dst_name = old_active_item.name
else:
dst_index = len(items)
dst_type = self.default_socket_type
# Empty name so it is based on the type.
dst_name = ""
items.new(dst_type, dst_name)
items.move(len(items) - 1, dst_index)
setattr(node, self.active_index_name, dst_index)
return {'FINISHED'}
class SocketItemRemoveOperator:
items_name = None
active_index_name = None
def execute(self, context):
node = self.get_node(context)
items = getattr(node, self.items_name)
old_active_index = getattr(node, self.active_index_name)
if 0 <= old_active_index < len(items):
items.remove(items[old_active_index])
return {'FINISHED'}
class SocketMoveItemOperator:
items_name = None
active_index_name = None
direction: EnumProperty(
name="Direction",
items=[('UP', "Up", ""), ('DOWN', "Down", "")],
default='UP',
)
def execute(self, context):
node = self.get_node(context)
items = getattr(node, self.items_name)
old_active_index = getattr(node, self.active_index_name)
if self.direction == 'UP' and old_active_index > 0:
items.move(old_active_index, old_active_index - 1)
setattr(node, self.active_index_name, old_active_index - 1)
elif self.direction == 'DOWN' and old_active_index < len(items) - 1:
items.move(old_active_index, old_active_index + 1)
setattr(node, self.active_index_name, old_active_index + 1)
return {'FINISHED'}
class SimulationZoneOperator(ZoneOperator):
input_node_type = 'GeometryNodeSimulationInput'
output_node_type = 'GeometryNodeSimulationOutput'
items_name = "state_items"
active_index_name = "active_index"
class SimulationZoneItemAddOperator(SimulationZoneOperator, SocketItemAddOperator, Operator):
"""Add a state item to the simulation zone"""
bl_idname = "node.simulation_zone_item_add"
bl_label = "Add State Item"
bl_options = {'REGISTER', 'UNDO'}
class SimulationZoneItemRemoveOperator(SimulationZoneOperator, SocketItemRemoveOperator, Operator):
"""Remove a state item from the simulation zone"""
bl_idname = "node.simulation_zone_item_remove"
bl_label = "Remove State Item"
bl_options = {'REGISTER', 'UNDO'}
class SimulationZoneItemMoveOperator(SimulationZoneOperator, SocketMoveItemOperator, Operator):
"""Move a simulation state item up or down in the list"""
bl_idname = "node.simulation_zone_item_move"
bl_label = "Move State Item"
bl_options = {'REGISTER', 'UNDO'}
class RepeatZoneOperator(ZoneOperator):
input_node_type = 'GeometryNodeRepeatInput'
output_node_type = 'GeometryNodeRepeatOutput'
items_name = "repeat_items"
active_index_name = "active_index"
class RepeatZoneItemAddOperator(RepeatZoneOperator, SocketItemAddOperator, Operator):
"""Add a repeat item to the repeat zone"""
bl_idname = "node.repeat_zone_item_add"
bl_label = "Add Repeat Item"
bl_options = {'REGISTER', 'UNDO'}
class RepeatZoneItemRemoveOperator(RepeatZoneOperator, SocketItemRemoveOperator, Operator):
"""Remove a repeat item from the repeat zone"""
bl_idname = "node.repeat_zone_item_remove"
bl_label = "Remove Repeat Item"
bl_options = {'REGISTER', 'UNDO'}
class RepeatZoneItemMoveOperator(RepeatZoneOperator, SocketMoveItemOperator, Operator):
"""Move a repeat item up or down in the list"""
bl_idname = "node.repeat_zone_item_move"
bl_label = "Move Repeat Item"
bl_options = {'REGISTER', 'UNDO'}
class BakeNodeOperator(NodeOperator):
node_type = 'GeometryNodeBake'
items_name = "bake_items"
active_index_name = "active_index"
class BakeNodeItemAddOperator(BakeNodeOperator, SocketItemAddOperator, Operator):
"""Add a bake item to the bake node"""
bl_idname = "node.bake_node_item_add"
bl_label = "Add Bake Item"
bl_options = {'REGISTER', 'UNDO'}
class BakeNodeItemRemoveOperator(BakeNodeOperator, SocketItemRemoveOperator, Operator):
"""Remove a bake item from the bake node"""
bl_idname = "node.bake_node_item_remove"
bl_label = "Remove Bake Item"
bl_options = {'REGISTER', 'UNDO'}
class BakeNodeItemMoveOperator(BakeNodeOperator, SocketMoveItemOperator, Operator):
"""Move a bake item up or down in the list"""
bl_idname = "node.bake_node_item_move"
bl_label = "Move Bake Item"
bl_options = {'REGISTER', 'UNDO'}
def _editable_tree_with_active_node_type(context, node_type):
space = context.space_data
# Needs active node editor and a tree.
if not space or space.type != 'NODE_EDITOR' or not space.edit_tree or space.edit_tree.library:
return False
node = context.active_node
if node is None or node.bl_idname != node_type:
return False
return True
class IndexSwitchItemAddOperator(Operator):
"""Add an item to the index switch"""
bl_idname = "node.index_switch_item_add"
bl_label = "Add Item"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return _editable_tree_with_active_node_type(context, 'GeometryNodeIndexSwitch')
def execute(self, context):
node = context.active_node
node.index_switch_items.new()
return {'FINISHED'}
class IndexSwitchItemRemoveOperator(Operator):
"""Remove an item from the index switch"""
bl_idname = "node.index_switch_item_remove"
bl_label = "Remove Item"
bl_options = {'REGISTER', 'UNDO'}
index: IntProperty(
name="Index",
description="Index of item to remove",
)
@classmethod
def poll(cls, context):
return _editable_tree_with_active_node_type(context, 'GeometryNodeIndexSwitch')
def execute(self, context):
node = context.active_node
items = node.index_switch_items
items.remove(items[self.index])
return {'FINISHED'}
classes = (
NewGeometryNodesModifier,
NewGeometryNodeTreeAssign,
NewGeometryNodeGroupTool,
MoveModifierToNodes,
SimulationZoneItemAddOperator,
SimulationZoneItemRemoveOperator,
SimulationZoneItemMoveOperator,
RepeatZoneItemAddOperator,
RepeatZoneItemRemoveOperator,
RepeatZoneItemMoveOperator,
BakeNodeItemAddOperator,
BakeNodeItemRemoveOperator,
BakeNodeItemMoveOperator,
IndexSwitchItemAddOperator,
IndexSwitchItemRemoveOperator,
)

View File

@@ -991,244 +991,6 @@ class NODE_PT_node_tree_properties(Panel):
col.prop(group, "is_tool")
def draw_socket_item_in_list(uilist, layout, item, icon):
if uilist.layout_type in {'DEFAULT', 'COMPACT'}:
row = layout.row(align=True)
row.template_node_socket(color=item.color)
row.prop(item, "name", text="", emboss=False, icon_value=icon)
elif uilist.layout_type == 'GRID':
layout.alignment = 'CENTER'
layout.template_node_socket(color=item.color)
class NODE_UL_simulation_zone_items(UIList):
def draw_item(self, context, layout, _data, item, icon, _active_data, _active_propname, _index):
draw_socket_item_in_list(self, layout, item, icon)
class NODE_PT_simulation_zone_items(Panel):
bl_space_type = 'NODE_EDITOR'
bl_region_type = 'UI'
bl_category = "Node"
bl_label = "Simulation State"
input_node_type = 'GeometryNodeSimulationInput'
output_node_type = 'GeometryNodeSimulationOutput'
@classmethod
def get_output_node(cls, context):
node = context.active_node
if node.bl_idname == cls.input_node_type:
return node.paired_output
if node.bl_idname == cls.output_node_type:
return node
@classmethod
def poll(cls, context):
snode = context.space_data
if snode is None:
return False
node = context.active_node
if node is None or node.bl_idname not in [cls.input_node_type, cls.output_node_type]:
return False
if cls.get_output_node(context) is None:
return False
return True
def draw(self, context):
layout = self.layout
output_node = self.get_output_node(context)
split = layout.row()
split.template_list(
"NODE_UL_simulation_zone_items",
"",
output_node,
"state_items",
output_node,
"active_index")
ops_col = split.column()
add_remove_col = ops_col.column(align=True)
add_remove_col.operator("node.simulation_zone_item_add", icon='ADD', text="")
add_remove_col.operator("node.simulation_zone_item_remove", icon='REMOVE', text="")
ops_col.separator()
up_down_col = ops_col.column(align=True)
props = up_down_col.operator("node.simulation_zone_item_move", icon='TRIA_UP', text="")
props.direction = 'UP'
props = up_down_col.operator("node.simulation_zone_item_move", icon='TRIA_DOWN', text="")
props.direction = 'DOWN'
active_item = output_node.active_item
if active_item is not None:
layout.use_property_split = True
layout.use_property_decorate = False
layout.prop(active_item, "socket_type")
if active_item.socket_type in {'VECTOR', 'INT', 'BOOLEAN', 'FLOAT', 'RGBA', 'ROTATION'}:
layout.prop(active_item, "attribute_domain")
class NODE_UL_repeat_zone_items(UIList):
def draw_item(self, _context, layout, _data, item, icon, _active_data, _active_propname, _index):
draw_socket_item_in_list(self, layout, item, icon)
class NODE_PT_repeat_zone_items(Panel):
bl_space_type = 'NODE_EDITOR'
bl_region_type = 'UI'
bl_category = "Node"
bl_label = "Repeat"
input_node_type = 'GeometryNodeRepeatInput'
output_node_type = 'GeometryNodeRepeatOutput'
@classmethod
def get_output_node(cls, context):
node = context.active_node
if node.bl_idname == cls.input_node_type:
return node.paired_output
if node.bl_idname == cls.output_node_type:
return node
return None
@classmethod
def poll(cls, context):
snode = context.space_data
if snode is None:
return False
node = context.active_node
if node is None or node.bl_idname not in {cls.input_node_type, cls.output_node_type}:
return False
if cls.get_output_node(context) is None:
return False
return True
def draw(self, context):
layout = self.layout
output_node = self.get_output_node(context)
split = layout.row()
split.template_list(
"NODE_UL_repeat_zone_items",
"",
output_node,
"repeat_items",
output_node,
"active_index")
ops_col = split.column()
add_remove_col = ops_col.column(align=True)
add_remove_col.operator("node.repeat_zone_item_add", icon='ADD', text="")
add_remove_col.operator("node.repeat_zone_item_remove", icon='REMOVE', text="")
ops_col.separator()
up_down_col = ops_col.column(align=True)
props = up_down_col.operator("node.repeat_zone_item_move", icon='TRIA_UP', text="")
props.direction = 'UP'
props = up_down_col.operator("node.repeat_zone_item_move", icon='TRIA_DOWN', text="")
props.direction = 'DOWN'
active_item = output_node.active_item
if active_item is not None:
layout.use_property_split = True
layout.use_property_decorate = False
layout.prop(active_item, "socket_type")
layout.prop(output_node, "inspection_index")
class NODE_UL_bake_node_items(UIList):
def draw_item(self, _context, layout, _data, item, icon, _active_data, _active_propname, _index):
draw_socket_item_in_list(self, layout, item, icon)
class NODE_PT_bake_node_items(bpy.types.Panel):
bl_space_type = 'NODE_EDITOR'
bl_region_type = 'UI'
bl_category = "Node"
bl_label = "Bake Items"
@classmethod
def poll(cls, context):
snode = context.space_data
if snode is None:
return False
node = context.active_node
if node is None:
return False
if node.bl_idname != "GeometryNodeBake":
return False
return True
def draw(self, context):
layout = self.layout
node = context.active_node
split = layout.row()
split.template_list(
"NODE_UL_bake_node_items",
"",
node,
"bake_items",
node,
"active_index")
ops_col = split.column()
add_remove_col = ops_col.column(align=True)
add_remove_col.operator("node.bake_node_item_add", icon='ADD', text="")
add_remove_col.operator("node.bake_node_item_remove", icon='REMOVE', text="")
ops_col.separator()
up_down_col = ops_col.column(align=True)
props = up_down_col.operator("node.bake_node_item_move", icon='TRIA_UP', text="")
props.direction = 'UP'
props = up_down_col.operator("node.bake_node_item_move", icon='TRIA_DOWN', text="")
props.direction = 'DOWN'
active_item = node.active_item
if active_item is not None:
layout.use_property_split = True
layout.use_property_decorate = False
layout.prop(active_item, "socket_type")
if active_item.socket_type in {'VECTOR', 'INT', 'BOOLEAN', 'FLOAT', 'RGBA', 'ROTATION'}:
layout.prop(active_item, "attribute_domain")
layout.prop(active_item, "is_attribute")
class NODE_PT_index_switch_node_items(Panel):
bl_space_type = 'NODE_EDITOR'
bl_region_type = 'UI'
bl_category = "Node"
bl_label = "Index Switch"
@classmethod
def poll(cls, context):
snode = context.space_data
if snode is None:
return False
node = context.active_node
if node is None or node.bl_idname != 'GeometryNodeIndexSwitch':
return False
return True
def draw(self, context):
layout = self.layout
node = context.active_node
layout.operator("node.index_switch_item_add", icon='ADD', text="Add Item")
col = layout.column()
for i, item in enumerate(node.index_switch_items):
row = col.row()
row.label(text=node.inputs[i + 1].name)
row.operator("node.index_switch_item_remove", icon='REMOVE', text="").index = i
class NODE_UL_enum_definition_items(bpy.types.UIList):
def draw_item(self, _context, layout, _data, item, icon, _active_data, _active_propname, _index):
layout.prop(item, "name", text="", emboss=False, icon_value=icon)
@@ -1349,13 +1111,6 @@ classes = (
NODE_PT_quality,
NODE_PT_annotation,
NODE_PT_overlay,
NODE_UL_simulation_zone_items,
NODE_PT_simulation_zone_items,
NODE_UL_repeat_zone_items,
NODE_UL_bake_node_items,
NODE_PT_bake_node_items,
NODE_PT_index_switch_node_items,
NODE_PT_repeat_zone_items,
NODE_UL_enum_definition_items,
NODE_PT_menu_switch_items,
NODE_PT_active_node_properties,