kopia lustrzana https://github.com/carson-katri/geometry-script
Add default argument value support
rodzic
7dc083e361
commit
c28d6ced93
|
@ -118,6 +118,10 @@ To open an external Python file:
|
||||||
|
|
||||||
![A screenshot of the top of the Text Editor, with the Auto Resolve option checked](resources/auto_resolve.png)
|
![A screenshot of the top of the Text Editor, with the Auto Resolve option checked](resources/auto_resolve.png)
|
||||||
|
|
||||||
|
5. *(Optional)* Enable *Text* > *Live Edit* to automatically rebuild the Geometry Node tree every time the file is changed.
|
||||||
|
|
||||||
|
![A screenshot of the top of the Text Editor, with the Live Edit option checked](resources/live_edit.png)
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
Documentation and typeshed files are automatically generated when you install the add-on. You can find instructions for using them with your IDE in the add-on preferences.
|
Documentation and typeshed files are automatically generated when you install the add-on. You can find instructions for using them with your IDE in the add-on preferences.
|
||||||
|
|
||||||
|
|
22
__init__.py
22
__init__.py
|
@ -17,6 +17,7 @@ import webbrowser
|
||||||
|
|
||||||
from .api.tree import *
|
from .api.tree import *
|
||||||
from .preferences import GeometryScriptPreferences
|
from .preferences import GeometryScriptPreferences
|
||||||
|
from .absolute_path import absolute_path
|
||||||
|
|
||||||
# Set the `geometry_script` module to the current module in case the folder is named differently.
|
# Set the `geometry_script` module to the current module in case the folder is named differently.
|
||||||
import sys
|
import sys
|
||||||
|
@ -49,7 +50,7 @@ class OpenDocumentation(bpy.types.Operator):
|
||||||
bl_label = "Open Documentation"
|
bl_label = "Open Documentation"
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
webbrowser.open('file://' + os.path.join(os.path.dirname(__file__), 'docs/documentation.html'))
|
webbrowser.open('file://' + absolute_path('docs/documentation.html'))
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
class GeometryScriptSettings(bpy.types.PropertyGroup):
|
class GeometryScriptSettings(bpy.types.PropertyGroup):
|
||||||
|
@ -75,13 +76,18 @@ def editor_header_draw(self, context):
|
||||||
|
|
||||||
def auto_resolve():
|
def auto_resolve():
|
||||||
if bpy.context.scene.geometry_script_settings.auto_resolve:
|
if bpy.context.scene.geometry_script_settings.auto_resolve:
|
||||||
for area in bpy.context.screen.areas:
|
try:
|
||||||
for space in area.spaces:
|
for area in bpy.context.screen.areas:
|
||||||
if space.type == 'NODE_EDITOR':
|
for space in area.spaces:
|
||||||
with bpy.context.temp_override(area=area, space=space):
|
if space.type == 'TEXT_EDITOR':
|
||||||
text = bpy.context.space_data.text
|
with bpy.context.temp_override(area=area, space=space):
|
||||||
if text and text.is_modified:
|
text = bpy.context.space_data.text
|
||||||
bpy.ops.text.resolve_conflict(resolution='RELOAD')
|
if text and text.is_modified:
|
||||||
|
bpy.ops.text.resolve_conflict(resolution='RELOAD')
|
||||||
|
if bpy.context.space_data.use_live_edit:
|
||||||
|
bpy.ops.text.run_script()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
def absolute_path(component):
|
||||||
|
"""
|
||||||
|
Returns the absolute path to a file in the addon directory.
|
||||||
|
|
||||||
|
Alternative to `os.abspath` that works the same on macOS and Windows.
|
||||||
|
"""
|
||||||
|
return os.path.join(os.path.dirname(os.path.realpath(__file__)), component)
|
|
@ -1,7 +1,8 @@
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.types import GeometryNodeCurveToMesh
|
import bl_ui
|
||||||
from .state import State
|
from .state import State
|
||||||
from .types import *
|
from .types import *
|
||||||
|
from ..absolute_path import absolute_path
|
||||||
|
|
||||||
class OutputsList(dict):
|
class OutputsList(dict):
|
||||||
__getattr__ = dict.get
|
__getattr__ = dict.get
|
||||||
|
@ -14,7 +15,7 @@ def build_node(node_type):
|
||||||
if _primary_arg is not None:
|
if _primary_arg is not None:
|
||||||
State.current_node_tree.links.new(_primary_arg._socket, node.inputs[0])
|
State.current_node_tree.links.new(_primary_arg._socket, node.inputs[0])
|
||||||
for prop in node.bl_rna.properties:
|
for prop in node.bl_rna.properties:
|
||||||
argname = prop.name.lower().replace(' ', '_')
|
argname = prop.identifier.lower().replace(' ', '_')
|
||||||
if argname in kwargs:
|
if argname in kwargs:
|
||||||
setattr(node, prop.identifier, kwargs[argname])
|
setattr(node, prop.identifier, kwargs[argname])
|
||||||
for node_input in (node.inputs[1:] if _primary_arg is not None else node.inputs):
|
for node_input in (node.inputs[1:] if _primary_arg is not None else node.inputs):
|
||||||
|
@ -58,10 +59,29 @@ def register_node(node_type, category_path=None):
|
||||||
registered_nodes.add(node_type)
|
registered_nodes.add(node_type)
|
||||||
for category_name in list(filter(lambda x: x.startswith('NODE_MT_category_GEO_'), dir(bpy.types))):
|
for category_name in list(filter(lambda x: x.startswith('NODE_MT_category_GEO_'), dir(bpy.types))):
|
||||||
category = getattr(bpy.types, category_name)
|
category = getattr(bpy.types, category_name)
|
||||||
category_path = category.category.name.lower().replace(' ', '_')
|
if not hasattr(category, 'category'):
|
||||||
for node in category.category.items(None):
|
category_path = category.bl_label.lower().replace(' ', '_')
|
||||||
node_type = getattr(bpy.types, node.nodetype)
|
add_node_type = bl_ui.node_add_menu.add_node_type
|
||||||
register_node(node_type, category_path)
|
draw_node_group_add_menu = bl_ui.node_add_menu.draw_node_group_add_menu
|
||||||
|
draw_assets_for_catalog = bl_ui.node_add_menu.draw_assets_for_catalog
|
||||||
|
bl_ui.node_add_menu.add_node_type = lambda _layout, node_type_name: register_node(getattr(bpy.types, node_type_name), category_path)
|
||||||
|
bl_ui.node_add_menu.draw_node_group_add_menu = lambda _context, _layout: None
|
||||||
|
bl_ui.node_add_menu.draw_assets_for_catalog = lambda _context, _layout: None
|
||||||
|
class CategoryStub:
|
||||||
|
bl_label = ""
|
||||||
|
def __init__(self):
|
||||||
|
self.layout = Layout()
|
||||||
|
class Layout:
|
||||||
|
def separator(self): pass
|
||||||
|
category.draw(CategoryStub(), None)
|
||||||
|
bl_ui.node_add_menu.add_node_type = add_node_type
|
||||||
|
bl_ui.node_add_menu.draw_node_group_add_menu = draw_node_group_add_menu
|
||||||
|
bl_ui.node_add_menu.draw_assets_for_catalog = draw_assets_for_catalog
|
||||||
|
else:
|
||||||
|
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_name in list(filter(lambda x: 'GeometryNode' in x, dir(bpy.types))):
|
for node_type_name in list(filter(lambda x: 'GeometryNode' in x, dir(bpy.types))):
|
||||||
node_type = getattr(bpy.types, node_type_name)
|
node_type = getattr(bpy.types, node_type_name)
|
||||||
if issubclass(node_type, bpy.types.GeometryNode):
|
if issubclass(node_type, bpy.types.GeometryNode):
|
||||||
|
@ -200,9 +220,9 @@ def create_documentation():
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
with open('docs/documentation.html', 'w') as f:
|
with open(absolute_path('docs/documentation.html'), 'w') as f:
|
||||||
f.write(html)
|
f.write(html)
|
||||||
with open('typeshed/geometry_script.pyi', 'w') as fpyi, open('typeshed/geometry_script.py', 'w') as fpy:
|
with open(absolute_path('typeshed/geometry_script.pyi'), 'w') as fpyi, open(absolute_path('typeshed/geometry_script.py'), 'w') as fpy:
|
||||||
newline = '\n'
|
newline = '\n'
|
||||||
def type_symbol(t):
|
def type_symbol(t):
|
||||||
return f"class {t.__name__}(Type): pass"
|
return f"class {t.__name__}(Type): pass"
|
||||||
|
|
39
api/tree.py
39
api/tree.py
|
@ -1,6 +1,5 @@
|
||||||
import bpy
|
import bpy
|
||||||
import re
|
import inspect
|
||||||
from inspect import getfullargspec
|
|
||||||
try:
|
try:
|
||||||
import node_arrange as node_arrange
|
import node_arrange as node_arrange
|
||||||
except:
|
except:
|
||||||
|
@ -18,6 +17,8 @@ def _as_iterable(x):
|
||||||
def tree(name):
|
def tree(name):
|
||||||
tree_name = name
|
tree_name = name
|
||||||
def build_tree(builder):
|
def build_tree(builder):
|
||||||
|
signature = inspect.signature(builder)
|
||||||
|
|
||||||
# Locate or create the node group
|
# Locate or create the node group
|
||||||
node_group = None
|
node_group = None
|
||||||
if tree_name in bpy.data.node_groups:
|
if tree_name in bpy.data.node_groups:
|
||||||
|
@ -27,8 +28,8 @@ def tree(name):
|
||||||
# Clear the node group before building
|
# Clear the node group before building
|
||||||
for node in node_group.nodes:
|
for node in node_group.nodes:
|
||||||
node_group.nodes.remove(node)
|
node_group.nodes.remove(node)
|
||||||
for group_input in node_group.inputs:
|
while len(node_group.inputs) > len(signature.parameters):
|
||||||
node_group.inputs.remove(group_input)
|
node_group.inputs.remove(node_group.inputs[-1])
|
||||||
for group_output in node_group.outputs:
|
for group_output in node_group.outputs:
|
||||||
node_group.outputs.remove(group_output)
|
node_group.outputs.remove(group_output)
|
||||||
|
|
||||||
|
@ -37,21 +38,31 @@ def tree(name):
|
||||||
group_output_node = node_group.nodes.new('NodeGroupOutput')
|
group_output_node = node_group.nodes.new('NodeGroupOutput')
|
||||||
|
|
||||||
# Collect the inputs
|
# Collect the inputs
|
||||||
argspec = getfullargspec(builder)
|
|
||||||
inputs = {}
|
inputs = {}
|
||||||
for arg in argspec.args:
|
for param in signature.parameters.values():
|
||||||
if not arg in argspec.annotations:
|
if param.annotation == inspect.Parameter.empty:
|
||||||
raise Exception(f"Tree input '{arg}' has no type specified. Please specify a valid NodeInput subclass.")
|
raise Exception(f"Tree input '{param.name}' has no type specified. Please annotate with a valid node input type.")
|
||||||
type_annotation = argspec.annotations[arg]
|
if not issubclass(param.annotation, Type):
|
||||||
if not issubclass(type_annotation, Type):
|
raise Exception(f"Type of tree input '{param.name}' is not a valid 'Type' subclass.")
|
||||||
raise Exception(f"Type of tree input '{arg}' is not a valid 'Type' subclass.")
|
inputs[param.name] = (param.annotation, param.default)
|
||||||
inputs[arg] = type_annotation
|
|
||||||
|
|
||||||
# Create the input sockets and collect input values.
|
# Create the input sockets and collect input values.
|
||||||
|
for i, node_input in enumerate(node_group.inputs):
|
||||||
|
if node_input.bl_socket_idname != list(inputs.values())[i][0].socket_type:
|
||||||
|
for ni in node_group.inputs:
|
||||||
|
node_group.inputs.remove(ni)
|
||||||
|
break
|
||||||
builder_inputs = []
|
builder_inputs = []
|
||||||
for i, arg in enumerate(inputs.items()):
|
for i, arg in enumerate(inputs.items()):
|
||||||
node_group.inputs.new(arg[1].socket_type, re.sub('([A-Z])', r' \1', arg[0]).title())
|
input_name = arg[0].replace('_', ' ').title()
|
||||||
builder_inputs.append(arg[1](group_input_node.outputs[i]))
|
if len(node_group.inputs) > i:
|
||||||
|
node_group.inputs[i].name = input_name
|
||||||
|
node_input = node_group.inputs[i]
|
||||||
|
else:
|
||||||
|
node_input = node_group.inputs.new(arg[1][0].socket_type, input_name)
|
||||||
|
if arg[1][1] != inspect.Parameter.empty:
|
||||||
|
node_input.default_value = arg[1][1]
|
||||||
|
builder_inputs.append(arg[1][0](group_input_node.outputs[i]))
|
||||||
|
|
||||||
# Run the builder function
|
# Run the builder function
|
||||||
State.current_node_tree = node_group
|
State.current_node_tree = node_group
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
from geometry_script import *
|
|
||||||
|
|
||||||
@tree("Jellyfish")
|
|
||||||
def jellyfish(geometry: Geometry, head_radius: Float):
|
|
||||||
curve_points = geometry.curve_to_points(mode='EVALUATED').points
|
|
||||||
for i, points in curve_points:
|
|
||||||
return instance_on_points()
|
|
||||||
head = ico_sphere(radius=head_radius).transform(
|
|
||||||
translation=head_transform.position,
|
|
||||||
rotation=rotate_euler(space='LOCAL', rotation=align_euler_to_vector(vector=head_transform.tangent), rotate_by=(90, 0, 0)),
|
|
||||||
scale=(1, 1, 0.5)
|
|
||||||
)
|
|
||||||
return join_geometry(geometry=[head, geometry])
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
# NOTE: This example requires Blender 3.4+
|
||||||
|
|
||||||
|
from geometry_script import *
|
||||||
|
|
||||||
|
@tree("LEGO")
|
||||||
|
def lego(size: Vector, stud_radius: Float, stud_depth: Float, count_x: Int, count_y: Int):
|
||||||
|
base = cube(size=size)
|
||||||
|
stud_shape = cylinder(fill_type='NGON', radius=stud_radius, depth=stud_depth, vertices=8).mesh
|
||||||
|
stud = stud_shape.transform(translation=combine_xyz(z=(stud_depth / 2) + (size.z / 2)))
|
||||||
|
hole = stud_shape.transform(translation=combine_xyz(z=(stud_depth / 2) - (size.z / 2)))
|
||||||
|
segment = mesh_boolean(
|
||||||
|
operation='DIFFERENCE',
|
||||||
|
mesh_1=mesh_boolean(operation='UNION', mesh_2=[base, stud]).mesh,
|
||||||
|
mesh_2=hole
|
||||||
|
).mesh
|
||||||
|
return mesh_line(count=count_x, offset=(1, 0, 0)).instance_on_points(
|
||||||
|
instance=mesh_line(count=count_y, offset=(0, 1, 0)).instance_on_points(instance=segment)
|
||||||
|
).realize_instances().merge_by_distance()
|
||||||
|
|
||||||
|
@tree("Mesh to LEGO")
|
||||||
|
def mesh_to_lego(geometry: Geometry, resolution: Float=0.2):
|
||||||
|
return geometry.mesh_to_volume(interior_band_width=resolution, fill_volume=False).distribute_points_in_volume(
|
||||||
|
mode='DENSITY_GRID',
|
||||||
|
spacing=resolution
|
||||||
|
).instance_on_points(
|
||||||
|
instance=lego(size=resolution, stud_radius=resolution / 3, stud_depth=resolution / 8, count_x=1, count_y=1)
|
||||||
|
).realize_instances().merge_by_distance()
|
Plik binarny nie jest wyświetlany.
Przed Szerokość: | Wysokość: | Rozmiar: 79 KiB Po Szerokość: | Wysokość: | Rozmiar: 20 KiB |
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 48 KiB |
Ładowanie…
Reference in New Issue