Compare commits

..
4 changed files with 6 additions and 350 deletions

View file

@ -22,7 +22,6 @@ import webbrowser
from .api.tree import *
from .preferences import GeometryScriptPreferences
from .absolute_path import absolute_path
from .operators.convert_tree import ConvertTree
bl_info = {
"name" : "Geometry Script",
@ -75,10 +74,6 @@ def templates_menu_draw(self, context):
def editor_header_draw(self, context):
self.layout.menu(GeometryScriptMenu.bl_idname)
def node_header_draw(self, context):
if context.space_data.tree_type == 'GeometryNodeTree':
self.layout.operator(ConvertTree.bl_idname)
def auto_resolve():
if bpy.context.scene.geometry_script_settings.auto_resolve:
try:
@ -102,10 +97,8 @@ def register():
bpy.utils.register_class(GeometryScriptPreferences)
bpy.utils.register_class(OpenDocumentation)
bpy.utils.register_class(GeometryScriptMenu)
bpy.utils.register_class(ConvertTree)
bpy.types.TEXT_HT_header.append(editor_header_draw)
bpy.types.NODE_HT_header.append(node_header_draw)
bpy.types.Scene.geometry_script_settings = bpy.props.PointerProperty(type=GeometryScriptSettings)
@ -118,9 +111,7 @@ def unregister():
bpy.utils.unregister_class(GeometryScriptPreferences)
bpy.utils.unregister_class(OpenDocumentation)
bpy.utils.unregister_class(GeometryScriptMenu)
bpy.utils.unregister_class(ConvertTree)
bpy.types.TEXT_HT_header.remove(editor_header_draw)
bpy.types.NODE_HT_header.remove(node_header_draw)
try:
bpy.app.timers.unregister(auto_resolve)
except:

View file

@ -115,12 +115,8 @@ def tree(name):
node_input = node_group.interface.new_socket(socket_type=arg[1][0].socket_type, name=input_name, in_out='INPUT')
else:
node_input = node_group.inputs.new(arg[1][0].socket_type, input_name)
if arg[1][1] != inspect.Parameter.empty and arg[1][1] is not None:
try:
node_input.default_value = arg[1][1]
except TypeError:
if getattr(node_input, "bl_socket_idname", "") != "NodeSocketMenu":
raise
if arg[1][1] != inspect.Parameter.empty:
node_input.default_value = arg[1][1]
if arg[1][2] is not None:
if arg[1][2] not in builder_inputs:
builder_inputs[arg[1][2]] = signature.parameters[arg[1][2]].annotation()
@ -167,21 +163,12 @@ def tree(name):
# This lets @trees be used in other @trees via simple function calls.
def group_reference(*args, **kwargs):
if IS_BLENDER_4:
result = State.current_node_tree.nodes.new('GeometryNodeGroup')
result.node_tree = node_group
enabled_inputs = [node_input for node_input in result.inputs if node_input.enabled]
for value, node_input in zip(args, enabled_inputs):
set_or_create_link(value, node_input)
for node_input in enabled_inputs[len(args):]:
argname = node_input.name.lower().replace(' ', '_')
if argname in kwargs:
set_or_create_link(kwargs[argname], node_input)
result = geometrynodegroup(node_tree=node_group, *args, **kwargs)
else:
result = group(node_tree=node_group, *args, **kwargs)
group_outputs = []
for group_output in result.outputs:
if group_output.enabled:
group_outputs.append(Type(group_output))
for group_output in result._socket.node.outputs:
group_outputs.append(Type(group_output))
if len(group_outputs) == 1:
return group_outputs[0]
else:
@ -191,4 +178,4 @@ def tree(name):
return build_tree
else:
tree_name = name.__name__
return build_tree(name)
return build_tree(name)

View file

@ -1 +0,0 @@
"""Geometry Script Blender operators."""

View file

@ -1,321 +0,0 @@
import collections.abc
import keyword
import re
import bpy
import mathutils
_SOCKET_TYPE_NAMES = {
"BOOLEAN": "Bool",
"COLLECTION": "Collection",
"GEOMETRY": "Geometry",
"IMAGE": "Image",
"INT": "Int",
"MATERIAL": "Material",
"MENU": "Menu",
"OBJECT": "Object",
"RGBA": "Color",
"ROTATION": "Rotation",
"STRING": "String",
"TEXTURE": "Texture",
"VALUE": "Float",
"VECTOR": "Vector",
}
_PROP_DENYLIST = {
"bl_idname",
"bl_label",
"bl_static_type",
"color",
"dimensions",
"height",
"hide",
"inputs",
"internal_links",
"label",
"location",
"mute",
"name",
"outputs",
"parent",
"rna_type",
"select",
"show_options",
"show_preview",
"show_texture",
"type",
"use_custom_color",
"warning_propagation",
"width",
}
def _snake_case(value):
value = re.sub(r"[^0-9A-Za-z_]+", "_", value.strip())
value = re.sub(r"_+", "_", value).strip("_").lower()
if not value:
value = "value"
if value[0].isdigit():
value = "_" + value
if keyword.iskeyword(value):
value += "_"
return value
def _node_function_name(node):
if node.bl_idname == "GeometryNodeGroup" and getattr(node, "node_tree", None):
return _snake_case(node.node_tree.name)
return _snake_case(node.bl_rna.name)
def _socket_name(socket):
return _snake_case(socket.name or socket.identifier)
def _enabled_sockets(sockets):
return [socket for socket in sockets if socket.enabled and socket.type != "CUSTOM"]
def _convert_value(value):
if isinstance(value, mathutils.Vector):
return tuple(value)
if isinstance(value, mathutils.Euler):
return tuple(value)
if isinstance(value, mathutils.Color):
return tuple(value)
if isinstance(value, bpy.types.bpy_prop_array):
return tuple(value)
if isinstance(value, collections.abc.Iterable) and not isinstance(value, (str, bytes)):
try:
return tuple(value)
except TypeError:
return value
return value
def _literal(value):
value = _convert_value(value)
if isinstance(value, bpy.types.ID):
return "None"
return repr(value)
def _socket_type_annotation(socket):
return _SOCKET_TYPE_NAMES.get(socket.type, "Type")
class _Assignment:
def __init__(self, name, node):
self.name = name
self.node = node
self.props = {}
self.arguments = {}
self.argument_outputs = {}
self.argument_is_multi_input = {}
def flattened_arguments(self):
result = []
for value in self.arguments.values():
if isinstance(value, _Assignment):
result.append(value)
elif isinstance(value, list):
result.extend(item for item in value if isinstance(item, _Assignment))
return result
def _value_expr(self, key, value, index=None):
if isinstance(value, list):
values = [self._value_expr(key, item, index=i) for i, item in enumerate(value)]
if self.argument_is_multi_input.get(key, False):
return "[" + ", ".join(values) + "]"
if len(values) == 1:
return values[0]
return "(" + ", ".join(values) + ")"
if not isinstance(value, _Assignment):
return _literal(value)
if value.node.type == "GROUP_INPUT":
output_name = self.argument_outputs[key] if index is None else self.argument_outputs[key][index]
return output_name
output_name = self.argument_outputs[key] if index is None else self.argument_outputs[key][index]
enabled_outputs = _enabled_sockets(value.node.outputs)
if len(enabled_outputs) > 1:
return f"{value.name}.{output_name}"
return value.name
def _argument_expr(self, key, value):
return f"{key}={self._value_expr(key, value)}"
def result_targets(self):
return self.name
def to_script(self):
args = []
for key, value in self.props.items():
args.append(f"{key}={_literal(value)}")
for key, value in self.arguments.items():
args.append(self._argument_expr(key, value))
return f"{self.result_targets()} = {_node_function_name(self.node)}({', '.join(args)})"
def _collect_node_props(node):
props = {}
parent_props = set()
if getattr(node.bl_rna, "base", None) is not None:
parent_props.update(prop.identifier for prop in node.bl_rna.base.bl_rna.properties)
for prop in node.bl_rna.properties:
if prop.identifier in parent_props or prop.identifier in _PROP_DENYLIST:
continue
if prop.is_readonly and prop.type not in {"ENUM", "BOOLEAN", "INT", "FLOAT", "STRING"}:
continue
try:
value = getattr(node, prop.identifier)
except Exception:
continue
if prop.type == "POINTER":
continue
if prop.type == "COLLECTION":
continue
props[_snake_case(prop.identifier)] = _convert_value(value)
return props
def _collect_input_defaults(node):
props = {}
for socket in _enabled_sockets(node.inputs):
if not hasattr(socket, "default_value") or socket.hide_value:
continue
props[_socket_name(socket)] = _convert_value(socket.default_value)
return props
def _topological_sort(root):
seen = set()
order = []
def visit(assignment):
if assignment in seen:
return
seen.add(assignment)
for dependency in assignment.flattened_arguments():
visit(dependency)
order.append(assignment)
visit(root)
return order
def _function_arguments(group_input):
if group_input is None:
return []
arguments = []
for output in _enabled_sockets(group_input.outputs):
arguments.append(f"{_socket_name(output)}: {_socket_type_annotation(output)}")
return arguments
def _return_entries(group_output, output_assignment):
entries = []
for socket in _enabled_sockets(group_output.node.inputs):
key = _socket_name(socket)
if key not in group_output.arguments:
continue
value = group_output.arguments[key]
expr = group_output._argument_expr(key, value).split("=", 1)[1]
entries.append(f'"{socket.name}": {expr}')
if entries:
return "{ " + ", ".join(entries) + " }"
return output_assignment.name
def convert_node_tree(tree):
"""Return a best-effort Geometry Script draft for a Blender node tree."""
assignments = []
node_to_assignment = {}
node_type_counts = {}
incoming_links = {}
for node in tree.nodes:
prefix = _snake_case(node.type[:1] or "node")
count = node_type_counts.get(prefix, 0) + 1
node_type_counts[prefix] = count
assignment = _Assignment(f"{prefix}{count}", node)
assignment.props.update(_collect_node_props(node))
assignments.append(assignment)
node_to_assignment[node] = assignment
for link in tree.links:
incoming_links.setdefault(link.to_socket, []).append((node_to_assignment[link.from_node], _socket_name(link.from_socket)))
for assignment in assignments:
grouped_inputs = {}
for socket in _enabled_sockets(assignment.node.inputs):
key = _socket_name(socket)
grouped_inputs.setdefault(key, []).append(socket)
for key, sockets in grouped_inputs.items():
values = []
output_names = {}
is_multi_input = any(socket.is_multi_input for socket in sockets)
for socket in sockets:
links = incoming_links.get(socket, [])
if links:
for from_assignment, output_name in links:
output_names[len(values)] = output_name
values.append(from_assignment)
elif hasattr(socket, "default_value") and not socket.hide_value:
values.append(_convert_value(socket.default_value))
if not values:
continue
# Some Blender nodes expose the same conceptual control as both an
# RNA enum property and an input socket. Geometry Script maps that
# collapsed control to one Python keyword, so prefer the socket
# value/link and drop the duplicate property keyword.
assignment.props.pop(key, None)
if len(values) == 1:
assignment.arguments[key] = values[0]
if output_names:
assignment.argument_outputs[key] = output_names[0]
else:
assignment.arguments[key] = values
assignment.argument_outputs[key] = output_names
assignment.argument_is_multi_input[key] = is_multi_input
group_output = next((assignment for assignment in assignments if assignment.node.type == "GROUP_OUTPUT"), None)
if group_output is None:
raise ValueError(f"Node tree {tree.name!r} has no Group Output node")
group_input = next((node for node in tree.nodes if node.type == "GROUP_INPUT"), None)
sorted_assignments = [
assignment
for assignment in _topological_sort(group_output)
if assignment.node.type not in {"GROUP_INPUT", "GROUP_OUTPUT"}
]
body = "\n ".join(assignment.to_script() for assignment in sorted_assignments)
if not body:
body = "pass"
arguments = ", ".join(_function_arguments(group_input))
function_name = _snake_case(tree.name)
return_expr = _return_entries(group_output, sorted_assignments[-1] if sorted_assignments else group_output)
return (
"from geometry_script import *\n\n"
f'@tree("{tree.name}")\n'
f"def {function_name}({arguments}):\n"
f" {body}\n"
f" return {return_expr}\n"
)
class ConvertTree(bpy.types.Operator):
bl_idname = "geometry_script.convert_tree"
bl_label = "Convert to Geometry Script"
@classmethod
def poll(cls, context):
return bool(getattr(getattr(context, "space_data", None), "node_tree", None))
def execute(self, context):
script = convert_node_tree(context.space_data.node_tree)
script_datablock = bpy.data.texts.new(context.space_data.node_tree.name + ".py")
script_datablock.write(script)
return {"FINISHED"}