This repository has been archived on 2026-10-29. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
geometry-script/api/node_mapper.py
Adam P f42e0ba456 Fix some cases of node name collisions
Some compositor and texture nodes share names with the geometry nodes versions. This change sorts them so that the geometry ones are registered last, overriding the incorrect ones. The Math node is handled separately, because here the ShaderNodeMath is the correct one to use, for some reason.
2023-04-21 16:56:05 -04:00

350 lines
16 KiB
Python

import bpy
import itertools
import enum
import re
import os
from .state import State
from .types import *
from .static.input_group import InputGroup
from ..absolute_path import absolute_path
class OutputsList(dict):
__getattr__ = dict.get
__setattr__ = dict.__setitem__
__delattr__ = dict.__delitem__
def build_node(node_type):
def build(_primary_arg=None, **kwargs):
for k, v in kwargs.copy().items():
if isinstance(v, InputGroup):
kwargs = { **kwargs, **v.__dict__ }
del kwargs[k]
if v is None:
del kwargs[k]
node = State.current_node_tree.nodes.new(node_type.__name__)
if _primary_arg is not None:
State.current_node_tree.links.new(_primary_arg._socket, node.inputs[0])
for prop in node.bl_rna.properties:
argname = prop.identifier.lower().replace(' ', '_')
if argname in kwargs:
value = kwargs[argname]
if isinstance(value, enum.Enum):
value = value.value
setattr(node, prop.identifier, value)
for node_input in (node.inputs[1:] if _primary_arg is not None else node.inputs):
if not node_input.enabled:
continue
argname = node_input.name.lower().replace(' ', '_')
all_with_name = []
for node_input2 in (node.inputs[1:] if _primary_arg is not None else node.inputs):
if node_input2.name.lower().replace(' ', '_') == argname and node_input2.type == node_input.type:
all_with_name.append(node_input2)
if argname in kwargs:
def set_or_create_link(x, node_input):
if issubclass(type(x), Type):
State.current_node_tree.links.new(x._socket, node_input)
else:
def link_constant():
constant = Type(value=x)
State.current_node_tree.links.new(constant._socket, node_input)
if node_input.hide_value:
link_constant()
else:
try:
node_input.default_value = x
except:
link_constant()
value = kwargs[argname]
if isinstance(value, enum.Enum):
value = value.value
if node_input.is_multi_input and hasattr(value, '__iter__') and len(value) > 0 and issubclass(type(next(iter(value))), Type):
for x in value:
for node_input in all_with_name:
State.current_node_tree.links.new(x._socket, node_input)
elif len(all_with_name) > 1 and issubclass(type(value), tuple) and len(value) > 0:
for i, x in enumerate(value):
set_or_create_link(x, all_with_name[i])
else:
for node_input in all_with_name:
set_or_create_link(value, node_input)
outputs = {}
for node_output in node.outputs:
if not node_output.enabled:
continue
outputs[node_output.name.lower().replace(' ', '_')] = Type(node_output)
if len(outputs) == 1:
return list(outputs.values())[0]
else:
return OutputsList(outputs)
return build
documentation = {}
registered_nodes = set()
def register_node(node_type, category_path=None):
if node_type in registered_nodes:
return
snake_case_name = node_type.bl_rna.name.lower().replace(' ', '_')
node_namespace_name = snake_case_name.replace('_', ' ').title().replace(' ', '')
globals()[snake_case_name] = build_node(node_type)
globals()[snake_case_name].bl_category_path = category_path
globals()[snake_case_name].bl_node_type = node_type
documentation[snake_case_name] = globals()[snake_case_name]
def build_node_method(node_type):
def build(self, *args, **kwargs):
return build_node(node_type)(self, *args, **kwargs)
return build
setattr(Type, snake_case_name, build_node_method(node_type))
parent_props = [prop.identifier for base in node_type.__bases__ for prop in base.bl_rna.properties]
for prop in node_type.bl_rna.properties:
if not prop.identifier in parent_props and prop.type == 'ENUM':
if node_namespace_name not in globals():
class NodeNamespace: pass
NodeNamespace.__name__ = node_namespace_name
globals()[node_namespace_name] = NodeNamespace
enum_type_name = prop.identifier.replace('_', ' ').title().replace(' ', '')
enum_type = enum.Enum(enum_type_name, { map_case_name(i): i.identifier for i in prop.enum_items })
setattr(globals()[node_namespace_name], enum_type_name, enum_type)
registered_nodes.add(node_type)
denylist = {'filter'} # some nodes should be excluded.
class_denylist = {'CompositorNodeMath', 'TextureNodeMath'}
node_types_to_register = []
for node_type_name in dir(bpy.types):
node_type = getattr(bpy.types, node_type_name)
if isinstance(node_type, type) and issubclass(node_type, bpy.types.Node):
if node_type.is_registered_node_type() and node_type.bl_rna.name.lower() not in denylist and node_type.__name__ not in class_denylist:
node_types_to_register.append(node_type)
node_types_to_register.sort(key=lambda node_type: node_type.__name__.startswith("GeometryNode"))
for node_type in node_types_to_register:
register_node(node_type)
def create_documentation():
temp_node_group = bpy.data.node_groups.new('temp_node_group', 'GeometryNodeTree')
color_mappings = {
'INT': '#598C5C',
'FLOAT': '#A1A1A1',
'BOOLEAN': '#CCA6D6',
'GEOMETRY': '#00D6A3',
'VALUE': '#A1A1A1',
'VECTOR': '#6363C7',
'MATERIAL': '#EB7582',
'TEXTURE': '#9E4FA3',
'COLLECTION': '#F5F5F5',
'OBJECT': '#ED9E5C',
'STRING': '#70B2FF',
'RGBA': '#C7C729',
}
default_color = '#A1A1A1'
docstrings = []
symbols = []
enums = {}
skipped_nodes = []
for func in sorted(documentation.keys()):
try:
method = documentation[func]
link = f"https://docs.blender.org/manual/en/latest/modeling/geometry_nodes/{method.bl_category_path}/{func}.html"
image = f"https://docs.blender.org/manual/en/latest/_images/node-types_{method.bl_node_type.__name__}"
node_instance = temp_node_group.nodes.new(method.bl_node_type.__name__)
props_inputs = {}
symbol_inputs = {}
parent_props = [prop.identifier for base in method.bl_node_type.__bases__ for prop in base.bl_rna.properties]
node_namespace_name = func.replace('_', ' ').title().replace(' ', '')
for prop in method.bl_node_type.bl_rna.properties:
if not prop.identifier in parent_props:
if prop.type == 'ENUM':
enum_name = prop.identifier.replace('_', ' ').title().replace(' ', '')
enum_cases = '\n '.join(map(lambda i: f"{map_case_name(i)} = '{i.identifier}'", prop.enum_items))
if node_namespace_name not in enums:
enums[node_namespace_name] = []
enums[node_namespace_name].append(f""" class {enum_name}(enum.Enum):
{enum_cases}""")
props_inputs[prop.identifier] = {f"<span style=\"color: {color_mappings['STRING']};\">{node_namespace_name}.{enum_name}</span>":1}
symbol_inputs[prop.identifier] = {f"{node_namespace_name}.{enum_name}": 1}
else:
props_inputs[prop.identifier] = {f"<span style=\"color: {color_mappings.get(prop.type, default_color)};\">{prop.type.title()}</span>":1}
symbol_inputs[prop.identifier] = {prop.type.title(): 1}
primary_arg = None
for node_input in node_instance.inputs:
name = node_input.name.lower().replace(' ', '_')
typename = type(node_input).__name__.replace('NodeSocket', '')
if node_input.is_multi_input:
typename = f"List[{typename}]"
type_str = f"<span style=\"color: {color_mappings.get(node_input.type, default_color)};\">{typename}</span>"
if name in props_inputs:
if type_str in props_inputs[name]:
props_inputs[name][type_str] += 1
symbol_inputs[name][typename] += 1
else:
props_inputs[name][type_str] = 1
symbol_inputs[name][typename] = 1
else:
props_inputs[name] = {type_str: 1}
symbol_inputs[name] = {typename: 1}
if primary_arg is None:
primary_arg = (name, list(props_inputs[name].keys())[0])
def collapse_inputs(inputs):
for k, v in inputs.items():
values = []
for t, c in v.items():
for c in range(1, c + 1):
value = ""
if c > 1:
value += "Tuple["
value += ', '.join(itertools.repeat(t, c))
if c > 1:
value += "]"
values.append(value)
inputs[k] = ' | '.join(values)
collapse_inputs(props_inputs)
collapse_inputs(symbol_inputs)
arg_docs = []
symbol_args = []
for name, value in props_inputs.items():
arg_docs.append(f"{name}: {value}")
symbol_args.append(f"{name}: {symbol_inputs[name]} | None = None")
outputs = {}
symbol_outputs = {}
for node_output in node_instance.outputs:
output_name = node_output.name.lower().replace(' ', '_')
output_type = type(node_output).__name__.replace('NodeSocket', '')
outputs[output_name] = f"<span style=\"color: {color_mappings.get(node_output.type, default_color)};\">{output_type}</span>"
symbol_outputs[output_name] = output_type
output_docs = []
output_symbols = []
for name, value in outputs.items():
output_docs.append(f"{name}: {value}")
output_symbols.append(f"{name}: {symbol_outputs[name]}")
outputs_doc = f"{{ {', '.join(output_docs)} }}" if len(output_docs) > 1 else ''.join(output_docs)
arg_separator = ',\n '
def primary_arg_docs():
return f"""
<h4>Chain Syntax</h4>
<pre><code>{primary_arg[0]}: {primary_arg[1]} = ...
{primary_arg[0]}.{func}(...)</code></pre>
"""
docstrings.append(f"""
<details style="margin: 10px 0;">
<summary><code>{func}</code> - <a href="{link}">{method.bl_node_type.bl_rna.name}</a></summary>
<div style="margin-top: 5px;">
<img src="{image}.webp" onerror="if (this.src != '{image}.png') this.src = '{image}.png'" />
<h4>Signature</h4>
<pre><code>{func}(
{arg_separator.join(arg_docs)}
)</code></pre>
<h4>Result</h4>
<pre><code>{outputs_doc}</code></pre>
{primary_arg_docs() if primary_arg is not None else ""}
</div>
</details>
""")
output_symbol_separator = '\n '
if len(output_symbols) > 1:
if node_namespace_name not in enums:
enums[node_namespace_name] = []
enums[node_namespace_name].append(f""" class Result:
{output_symbol_separator.join(output_symbols)}""")
return_type_hint = list(symbol_outputs.values())[0] if len(output_symbols) == 1 else f"{node_namespace_name}.Result"
symbols.append(f"""def {func}({', '.join(symbol_args)}) -> {return_type_hint}: \"\"\"![]({image}.webp)\"\"\"""")
except:
skipped_nodes.append(documentation[func].bl_node_type.__name__)
continue
bpy.data.node_groups.remove(temp_node_group)
html = f"""
<html>
<head>
<style>
html {{
background-color: #1D1D1D;
color: #FFFFFF;
}}
a {{
color: #4772B3;
}}
body {{
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
max-width: 60em;
margin: 0 auto;
}}
pre {{
overflow: scroll;
padding: 16px;
background-color: #303030;
border-radius: 5px;
}}
</style>
</head>
<body>
<h1>Geometry Script</h1>
<h3>Nodes</h3>
{''.join(docstrings)}
</body>
</html>
"""
with open(absolute_path('docs/documentation.html'), 'w') as f:
f.write(html)
with open(absolute_path('typeshed/geometry_script.pyi'), 'w') as fpyi, open(absolute_path('typeshed/geometry_script.py'), 'w') as fpy:
newline = '\n'
def type_symbol(t):
return f"class {t.__name__}(Type): pass"
def enum_namespace(k):
return f"""class {k}:
{newline.join(enums[k])}"""
def add_self_arg(x):
return re.sub('\(', '(self, ', x, 1)
contents = f"""from typing import *
import enum
def tree(builder):
\"\"\"
Marks a function as a node tree.
\"\"\"
pass
_SomeType = TypeVar('_SomeType', bound='Type')
class Type:
def __add__(self, other) -> Type: return self
def __radd__(self, other) -> Type: return self
def __sub__(self, other) -> Type: return self
def __rsub__(self, other) -> Type: return self
def __mul__(self, other) -> Type: return self
def __rmul__(self, other) -> Type: return self
def __truediv__(self, other) -> Type: return self
def __rtruediv__(self, other) -> Type: return self
def __mod__(self, other) -> Type: return self
def __rmod__(self, other) -> Type: return self
def __eq__(self, other) -> Type: return self
def __ne__(self, other) -> Type: return self
def __lt__(self, other) -> Type: return self
def __le__(self, other) -> Type: return self
def __gt__(self, other) -> Type: return self
def __ge__(self, other) -> Type: return self
def __invert__(self) -> Type: return self
def __getitem__(
self,
subscript: _SomeType | slice | Tuple[_SomeType | slice, SampleMode]
) -> Type: return self
x = Type()
y = Type()
z = Type()
def capture(self, attribute: Type, **kwargs) -> (Geometry, Type): return Geometry(), Type()
def transfer(self, attribute: Type, **kwargs) -> Type: return Type()
{(newline + ' ').join(map(add_self_arg, filter(lambda x: x.startswith('def'), symbols)))}
{newline.join(map(type_symbol, Type.__subclasses__()))}
{newline.join(map(enum_namespace, enums.keys()))}
{newline.join(symbols)}"""
static_path = absolute_path('api/static')
for path in os.listdir(static_path):
if os.path.splitext(path)[-1] != '.py':
continue
with open(os.path.join(static_path, path), 'r') as static_api:
contents += f"\n\n# {path}\n{static_api.read()}"
fpyi.write(contents)
fpy.write(contents)
if len(skipped_nodes) > 0:
pass # This could be reported later.
def create_docs():
create_documentation()
bpy.app.timers.register(create_docs)