diff --git a/.gitignore b/.gitignore
index 98fd9f0..d594108 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,7 +16,6 @@ dist/
downloads/
eggs/
.eggs/
-lib/
lib64/
parts/
sdist/
diff --git a/lib/__init__.py b/lib/__init__.py
new file mode 100644
index 0000000..e2a6537
--- /dev/null
+++ b/lib/__init__.py
@@ -0,0 +1,3 @@
+from .tree import *
+from .types import *
+from .node_mapper import *
\ No newline at end of file
diff --git a/lib/node_mapper.py b/lib/node_mapper.py
new file mode 100644
index 0000000..c0f6673
--- /dev/null
+++ b/lib/node_mapper.py
@@ -0,0 +1,208 @@
+import bpy
+from .state import State
+from .types import Type
+
+class OutputsList(dict):
+ __getattr__ = dict.get
+ __setattr__ = dict.__setitem__
+ __delattr__ = dict.__delitem__
+
+def build_node(node_type):
+ def build(_primary_arg=None, **kwargs):
+ 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.name.lower().replace(' ', '_')
+ if argname in kwargs:
+ setattr(node, prop.identifier, kwargs[argname])
+ for node_input in (node.inputs[1:] if _primary_arg is not None else node.inputs):
+ argname = node_input.name.lower().replace(' ', '_')
+ if argname in kwargs:
+ if node_input.is_multi_input and hasattr(kwargs[argname], '__iter__') and len(kwargs[argname]) > 0 and issubclass(type(next(iter(kwargs[argname]))), Type):
+ for x in kwargs[argname]:
+ State.current_node_tree.links.new(x._socket, node_input)
+ elif issubclass(type(kwargs[argname]), Type):
+ State.current_node_tree.links.new(kwargs[argname]._socket, node_input)
+ else:
+ try:
+ node_input.default_value = kwargs[argname]
+ except:
+ constant = Type(value=kwargs[argname])
+ State.current_node_tree.links.new(constant._socket, node_input)
+ outputs = {}
+ for node_output in node.outputs:
+ 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(' ', '_')
+ 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))
+ registered_nodes.add(node_type)
+for category_name in list(filter(lambda x: x.startswith('NODE_MT_category_GEO_'), dir(bpy.types))):
+ category = getattr(bpy.types, category_name)
+ category_path = category.category.name.lower().replace(' ', '_')
+ for node in category.category.items(None):
+ node_type = getattr(bpy.types, node.nodetype)
+ register_node(node_type, category_path)
+for node_type in bpy.types.GeometryNode.__subclasses__():
+ 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 = []
+ for func in sorted(documentation.keys()):
+ 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]
+ for prop in method.bl_node_type.bl_rna.properties:
+ if not prop.identifier in parent_props:
+ if prop.type == 'ENUM':
+ enum_items = 'Literal[' + ', '.join(map(lambda i: f"'{i.identifier}'", prop.enum_items)) + ']'
+ props_inputs[prop.identifier] = f"{enum_items}"
+ symbol_inputs[prop.identifier] = enum_items
+ else:
+ props_inputs[prop.identifier] = f"{prop.type.title()}"
+ symbol_inputs[prop.identifier] = prop.type.title()
+ 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"{typename}"
+ if name in props_inputs:
+ props_inputs[name] = props_inputs[name] + f' | {type_str}'
+ symbol_inputs[name] = symbol_inputs[name] + f' | {typename}'
+ else:
+ props_inputs[name] = type_str
+ symbol_inputs[name] = typename
+ if primary_arg is None:
+ primary_arg = (name, props_inputs[name])
+ 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]}")
+ 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"{output_type}"
+ 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"""
+
Chain Syntax
+ {primary_arg[0]}: {primary_arg[1]} = ...
+{primary_arg[0]}.{func}(...)
+ """
+ docstrings.append(f"""
+
+ {func}
- {method.bl_node_type.bl_rna.name}
+
+

+
Signature
+
{func}(
+ {arg_separator.join(arg_docs)}
+)
+
Result
+
{outputs_doc}
+ {primary_arg_docs() if primary_arg is not None else ""}
+
+
+ """)
+ output_symbol_separator = '\n '
+ symbol_return_type = f"_{func}_result"
+ if len(output_symbols) > 1:
+ symbols.append(f"""class {symbol_return_type}:
+ {output_symbol_separator.join(output_symbols)}""")
+ symbols.append(f"""def {func}({', '.join(symbol_args)}) -> {list(symbol_outputs.values())[0] if len(output_symbols) == 1 else symbol_return_type}: pass""")
+ bpy.data.node_groups.remove(temp_node_group)
+ html = f"""
+
+
+
+
+
+ Geometry Script
+ Nodes
+ {''.join(docstrings)}
+
+
+ """
+ with open('documentation.html', 'w') as f:
+ f.write(html)
+ with open('geometry_script.py', 'w') as f:
+ newline = '\n'
+ def type_symbol(t):
+ return f"class {t.__name__}: pass"
+ f.write(f"""from typing import *
+{newline.join(map(type_symbol, Type.__subclasses__()))}
+{newline.join(symbols)}""")
+
+def create_docs():
+ create_documentation()
+bpy.app.timers.register(create_docs)
\ No newline at end of file
diff --git a/lib/state.py b/lib/state.py
new file mode 100644
index 0000000..b61133f
--- /dev/null
+++ b/lib/state.py
@@ -0,0 +1,3 @@
+# Tree generation state
+class State:
+ current_node_tree = None
\ No newline at end of file
diff --git a/lib/tree.py b/lib/tree.py
new file mode 100644
index 0000000..3db607c
--- /dev/null
+++ b/lib/tree.py
@@ -0,0 +1,107 @@
+import bpy
+import re
+from inspect import getfullargspec
+try:
+ import node_arrange as node_arrange
+except:
+ pass
+from .state import State
+from .types import Type
+from .node_mapper import *
+
+def _as_iterable(input):
+ try:
+ return iter(input)
+ except TypeError:
+ return {input}
+
+def tree(name):
+ tree_name = name
+ def build_tree(builder):
+ # Locate or create the node group
+ node_group = None
+ if tree_name in bpy.data.node_groups:
+ node_group = bpy.data.node_groups[tree_name]
+ else:
+ node_group = bpy.data.node_groups.new(tree_name, 'GeometryNodeTree')
+ # Clear the node group before building
+ for node in node_group.nodes:
+ node_group.nodes.remove(node)
+ for group_input in node_group.inputs:
+ node_group.inputs.remove(group_input)
+ for group_output in node_group.outputs:
+ node_group.outputs.remove(group_output)
+
+ # Setup the group inputs
+ group_input_node = node_group.nodes.new('NodeGroupInput')
+ group_output_node = node_group.nodes.new('NodeGroupOutput')
+
+ # Collect the inputs
+ argspec = getfullargspec(builder)
+ inputs = {}
+ for arg in argspec.args:
+ if not arg in argspec.annotations:
+ raise Exception(f"Tree input '{arg}' has no type specified. Please specify a valid NodeInput subclass.")
+ type_annotation = argspec.annotations[arg]
+ if not issubclass(type_annotation, Type):
+ raise Exception(f"Type of tree input '{arg}' is not a valid 'Type' subclass.")
+ inputs[arg] = type_annotation
+
+ # Create the input sockets and collect input values.
+ builder_inputs = []
+ for i, arg in enumerate(inputs.items()):
+ node_group.inputs.new(arg[1].socket_type, re.sub('([A-Z])', r' \1', arg[0]).title())
+ builder_inputs.append(arg[1](group_input_node.outputs[i]))
+
+ # Run the builder function
+ State.current_node_tree = node_group
+ outputs = builder(*builder_inputs)
+
+ # Create the output sockets
+ for i, result in enumerate(_as_iterable(outputs)):
+ if not issubclass(type(result), Type):
+ result = Type(value=result)
+ # raise Exception(f"Return value '{result}' is not a valid 'Type' subclass.")
+ node_group.outputs.new(result.socket_type, 'Result')
+ link = node_group.links.new(result._socket, group_output_node.inputs[i])
+
+ # Attempt to run the "Node Arrange" add-on on the tree.
+ try:
+ for area in bpy.context.screen.areas:
+ for space in area.spaces:
+ if space.type == 'NODE_EDITOR':
+ space.node_tree = node_group
+ with bpy.context.temp_override(area=area, space=space, space_data=space):
+ ntree = node_group
+ ntree.nodes[0].select = True
+ ntree.nodes.active = ntree.nodes[0]
+ n_groups = []
+ for i in ntree.nodes:
+ if i.type == 'GROUP':
+ n_groups.append(i)
+
+ while n_groups:
+ j = n_groups.pop(0)
+ node_arrange.nodes_iterate(j.node_tree)
+ for i in j.node_tree.nodes:
+ if i.type == 'GROUP':
+ n_groups.append(i)
+
+ node_arrange.nodes_iterate(ntree)
+
+ # arrange nodes + this center nodes together
+ if bpy.context.scene.node_center:
+ node_arrange.nodes_center(ntree)
+ except:
+ pass
+
+ # Return a function that creates a NodeGroup node in the tree.
+ # This lets @trees be used in other @trees via simple function calls.
+ def group_reference(*args, **kwargs):
+ return group(node_tree=node_group, *args, **kwargs)
+ return group_reference
+ if isinstance(name, str):
+ return build_tree
+ else:
+ tree_name = name.__name__
+ return build_tree(name)
\ No newline at end of file
diff --git a/lib/types.py b/lib/types.py
new file mode 100644
index 0000000..581f7b4
--- /dev/null
+++ b/lib/types.py
@@ -0,0 +1,53 @@
+import bpy
+from .state import State
+
+# The base class all exposed socket types conform to.
+class Type:
+ socket_type: str
+
+ def __init__(self, socket: bpy.types.NodeSocket = None, value = None):
+ if value is not None:
+ input_nodes = {
+ int: ('FunctionNodeInputInt', 'integer'),
+ bool: ('FunctionNodeInputBool', 'boolean'),
+ str: ('FunctionNodeInputString', 'string'),
+ tuple: ('FunctionNodeInputVector', 'vector'),
+ float: ('ShaderNodeValue', None),
+ }
+ if not type(value) in input_nodes:
+ raise Exception(f"'{value}' cannot be expressed as a node.")
+ input_node_info = input_nodes[type(value)]
+ value_node = State.current_node_tree.nodes.new(input_node_info[0])
+ if input_node_info[1] is None:
+ value_node.outputs[0].default_value = value
+ else:
+ setattr(value_node, input_node_info[1], value)
+ socket = value_node.outputs[0]
+ self._socket = socket
+ self.socket_type = type(socket).__name__
+
+ def _math(self, other, operation):
+ math_node = State.current_node_tree.nodes.new('ShaderNodeVectorMath' if self._socket.type else 'ShaderNodeMath')
+ math_node.operation = operation
+ State.current_node_tree.links.new(self._socket, math_node.inputs[0])
+ if issubclass(type(other), Type):
+ State.current_node_tree.links.new(other._socket, math_node.inputs[1])
+ else:
+ math_node.inputs[1].default_value = other
+ return Type(math_node.outputs[0])
+
+ def __add__(self, other):
+ return self._math(other, 'ADD')
+
+ def __sub__(self, other):
+ return self._math(other, 'SUBTRACT')
+
+ def __mul__(self, other):
+ return self._math(other, 'SUBTRACT')
+
+ def __truediv__(self, other):
+ return self._math(other, 'DIVIDE')
+
+for standard_socket in bpy.types.NodeSocketStandard.__subclasses__():
+ name = standard_socket.__name__.replace('NodeSocket', '')
+ globals()[name] = type(name, (Type,), { 'socket_type': standard_socket.__name__ })
\ No newline at end of file