Geometry Nodes: Operator to wrap a modifier's node group #104546

Merged
Hans Goudey merged 19 commits from HooglyBoogly/blender:geometry-nodes-wrapper-operator into main 2023-02-14 18:22:04 +01:00
2 changed files with 172 additions and 9 deletions

View File

@ -6,8 +6,8 @@ from bpy.types import Operator
from bpy.app.translations import pgettext_data as data_
def geometry_node_group_empty_new():
group = bpy.data.node_groups.new(data_("Geometry Nodes"), 'GeometryNodeTree')
def build_default_empty_geometry_node_group(name):
HooglyBoogly marked this conversation as resolved
Review

Better use two separate functions that call a common function instead of adding optional parameters to this one.

Better use two separate functions that call a common function instead of adding optional parameters to this one.
group = bpy.data.node_groups.new(name, 'GeometryNodeTree')
group.inputs.new('NodeSocketGeometry', data_("Geometry"))
group.outputs.new('NodeSocketGeometry', data_("Geometry"))
input_node = group.nodes.new('NodeGroupInput')
@ -20,8 +20,12 @@ def geometry_node_group_empty_new():
input_node.location.x = -200 - input_node.width
output_node.location.x = 200
group.links.new(output_node.inputs[0], input_node.outputs[0])
return group
HooglyBoogly marked this conversation as resolved
Review

Return the input and output node from the function, and move this line into geometry_node_group_empty_new.

Return the input and output node from the function, and move this line into `geometry_node_group_empty_new`.
def geometry_node_group_empty_new():
group = build_default_empty_geometry_node_group(data_("Geometry Nodes"))
group.links.new(group.nodes["Group Input"].outputs[0], group.nodes["Group Output"].inputs[0])
return group
@ -35,6 +39,158 @@ def geometry_modifier_poll(context):
return True
def get_context_modifier(context):
if context.area.type == 'PROPERTIES':
modifier = context.modifier
HooglyBoogly marked this conversation as resolved
Review

Use get_context_modifer(context) is None.

Use `get_context_modifer(context) is None`.
else:
modifier = context.object.modifiers.active
if modifier is None or modifier.type != 'NODES':
return None
return modifier
def edit_geometry_nodes_modifier_poll(context):
return get_context_modifier(context) is not None
def socket_idname_to_attribute_type(idname):
if idname.startswith("NodeSocketInt"):
return "INT"
elif idname.startswith("NodeSocketColor"):
return "FLOAT_COLOR"
elif idname.startswith("NodeSocketVector"):
return "FLOAT_VECTOR"
elif idname.startswith("NodeSocketBool"):
return "BOOLEAN"
elif idname.startswith("NodeSocketFloat"):
return "FLOAT"
raise ValueError("Unsupported socket type")
return ""
def modifier_attribute_name_get(modifier, identifier):
try:
return modifier[identifier + "_attribute_name"]
except KeyError:
return None
def modifier_input_use_attribute(modifier, identifier):
try:
return modifier[identifier + "_use_attribute"] != 0
except KeyError:
return False
def get_socket_with_identifier(sockets, identifier):
for socket in sockets:
if socket.identifier == identifier:
return socket
return None
def get_enabled_socket_with_name(sockets, name):
for socket in sockets:
if socket.name == name and socket.enabled:
return socket
return None
HooglyBoogly marked this conversation as resolved
Review

The name is still kind of ugly. Maybe something like Move to Nodes (description: Move inputs and outputs set in the modifier to a new node group) or what Simon suggested.

The name is still kind of ugly. Maybe something like `Move to Nodes` (description: `Move inputs and outputs set in the modifier to a new node group`) or what Simon suggested.
Review

Yeah, good idea!

Yeah, good idea!
class MoveModifierToNodes(Operator):
"""Move inputs and outputs from in the modifier to a new node group"""
HooglyBoogly marked this conversation as resolved
Review

The class name and idname should be updated to respect the new label.

The class name and idname should be updated to respect the new label.
bl_idname = "object.geometry_nodes_move_to_nodes"
bl_label = "Move to Nodes"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return edit_geometry_nodes_modifier_poll(context)
def execute(self, context):
modifier = get_context_modifier(context)
if not modifier:
return {'CANCELLED'}
old_group = modifier.node_group
if not old_group:
return {'CANCELLED'}
wrapper_name = old_group.name + ".wrapper"
group = build_default_empty_geometry_node_group(wrapper_name)
group_node = group.nodes.new("GeometryNodeGroup")
group_node.node_tree = old_group
group_node.update()
group_input_node = group.nodes["Group Input"]
HooglyBoogly marked this conversation as resolved
Review

Avoid using input as variable name because this is a built-in function in Python.

Avoid using `input` as variable name because this is a built-in function in Python.
group_output_node = group.nodes["Group Output"]
# Copy default values for inputs and create named attribute input nodes.
input_nodes = []
first_geometry_input = None
for input_socket in old_group.inputs:
identifier = input_socket.identifier
group_node_input = get_socket_with_identifier(group_node.inputs, identifier)
if modifier_input_use_attribute(modifier, identifier):
input_node = group.nodes.new("GeometryNodeInputNamedAttribute")
input_nodes.append(input_node)
input_node.data_type = socket_idname_to_attribute_type(input_socket.bl_socket_idname)
attribute_name = modifier_attribute_name_get(modifier, identifier)
input_node.inputs["Name"].default_value = attribute_name
output_socket = get_enabled_socket_with_name(input_node.outputs, "Attribute")
HooglyBoogly marked this conversation as resolved
Review

Make sure vscode removes trailing spaces automatically.

Make sure vscode removes trailing spaces automatically.
group.links.new(output_socket, group_node_input)
elif hasattr(input_socket, "default_value"):
group_node_input.default_value = modifier[identifier]
elif input_socket.bl_socket_idname == 'NodeSocketGeometry':
if not first_geometry_input:
first_geometry_input = group_node_input
HooglyBoogly marked this conversation as resolved
Review

Can you use .x?

Can you use `.x`?
Review

Hmm, yes!

Hmm, yes!
group.links.new(group_input_node.outputs[0], first_geometry_input)
# Adjust locations of named attribute input nodes and group input node to make some space.
if input_nodes:
for i, node in enumerate(input_nodes):
node.location.x = -175
node.location.y = i * -50
group_input_node.location.x = -350
# Connect outputs to store named attribute nodes to replace modifier attribute outputs.
store_nodes = []
first_geometry_output = None
for output_socket in old_group.outputs:
identifier = output_socket.identifier
group_node_output = get_socket_with_identifier(group_node.outputs, identifier)
attribute_name = modifier_attribute_name_get(modifier, identifier)
if attribute_name:
store_node = group.nodes.new("GeometryNodeStoreNamedAttribute")
store_nodes.append(store_node)
store_node.data_type = socket_idname_to_attribute_type(output_socket.bl_socket_idname)
store_node.domain = output_socket.attribute_domain
store_node.inputs["Name"].default_value = attribute_name
input_socket = get_enabled_socket_with_name(store_node.inputs, "Value")
group.links.new(group_node_output, input_socket)
elif output_socket.bl_socket_idname == 'NodeSocketGeometry':
if not first_geometry_output:
first_geometry_output = group_node_output
# Adjust locations of store named attribute nodes and move group output.
HooglyBoogly marked this conversation as resolved
Review

Seems like you could move the - 1 into range(...).

Seems like you could move the `- 1` into `range(...)`.
if store_nodes:
for i, node in enumerate(store_nodes):
node.location.x = (i + 1) * 175
node.location.y = 0
group_output_node.location.x = (len(store_nodes) + 1) * 175
group.links.new(first_geometry_output, store_nodes[0].inputs["Geometry"])
for i in range(len(store_nodes) - 1):
group.links.new(store_nodes[i].outputs["Geometry"], store_nodes[i + 1].inputs["Geometry"])
group.links.new(store_nodes[-1].outputs["Geometry"], group_output_node.inputs["Geometry"])
else:
group.links.new(first_geometry_output, group_output_node.inputs["Geometry"])
modifier.node_group = group
return {'FINISHED'}
class NewGeometryNodesModifier(Operator):
"""Create a new modifier with a new geometry node group"""
@ -48,7 +204,6 @@ class NewGeometryNodesModifier(Operator):
def execute(self, context):
modifier = context.object.modifiers.new(data_("GeometryNodes"), "NODES")
if not modifier:
return {'CANCELLED'}
@ -70,11 +225,7 @@ class NewGeometryNodeTreeAssign(Operator):
return geometry_modifier_poll(context)
def execute(self, context):
if context.area.type == 'PROPERTIES':
modifier = context.modifier
else:
modifier = context.object.modifiers.active
modifier = get_context_modifier(context)
if not modifier:
return {'CANCELLED'}
@ -87,4 +238,5 @@ class NewGeometryNodeTreeAssign(Operator):
classes = (
NewGeometryNodesModifier,
NewGeometryNodeTreeAssign,
MoveModifierToNodes,
)

View File

@ -274,6 +274,17 @@ static void modifier_ops_extra_draw(bContext *C, uiLayout *layout, void *md_v)
if (!md->next) {
uiLayoutSetEnabled(row, false);
}
if (md->type == eModifierType_Nodes) {
uiItemFullO(layout,
"OBJECT_OT_geometry_nodes_move_to_nodes",
NULL,
ICON_NONE,
NULL,
WM_OP_INVOKE_DEFAULT,
0,
&op_ptr);
}
}
static void modifier_panel_header(const bContext *C, Panel *panel)