import bpy, pathlib, sys
from . import compat
class NodeModifier():
'''Base wrapper class for asset geometry nodes modifiers'''
input_map = dict()
prop_draw_map = dict()
modifier = None
last_valid_group_name = ''
default_name = ''
asset_name = '' # name of asset as in library blend file
_filepath = '' # used for cacheing string
blend_filename = 'geometry_nodes_essentials.blend' #5.0 node lib
# Input access, keyed by socket identifier. 5.2 moved inputs off IDProperties
@property
def inputs(self):
return compat.ModifierInputs(self.modifier)
# Copy shared modifier interface
@property
def show_on_cage(self):
return self.modifier.show_on_cage
@show_on_cage.setter
def show_on_cage(self, val):
self.modifier.show_on_cage = val
@property
def show_in_editmode(self):
return self.modifier.show_in_editmode
@show_in_editmode.setter
def show_in_editmode(self, val):
self.modifier.show_in_editmode = val
@property
def show_viewport(self):
return self.modifier.show_viewport
@show_viewport.setter
def show_viewport(self, val):
self.modifier.show_viewport = val
@property
def show_render(self):
return self.modifier.show_render
@show_render.setter
def show_render(self, val):
self.modifier.show_render = val
@property
def show_expanded(self):
return self.modifier.show_render
@show_expanded.setter
def show_expanded(self, val):
self.modifier.show_expanded = val
@property
def name(self):
return self.modifier.name
@name.setter
def name(self, val):
self.modifier.name = val
@property
def use_pin_to_last(self):
return getattr(self.modifier, 'use_pin_to_last', False)
@use_pin_to_last.setter
def use_pin_to_last(self, val):
try:
self.modifier.use_pin_to_last = val
except:
pass
@property
def show_group_selector(self):
return getattr(self.modifier, 'show_group_selector', False)
@show_group_selector.setter
def show_group_selector(self, val):
try:
self.modifier.show_group_selector = val
except:
pass
# Shared implementation
@classmethod
def new(cls, modifier):
'''Return new wrapper for modifer or None if modifier's node group is not valid'''
if not cls.is_valid_modifier(modifier): return None
return cls.new_unchecked(modifier)
@classmethod
def from_object (cls, object):
'''Create new modifier on object and return wrapper'''
modifier = object.modifiers.new(cls.default_name, 'NODES')
modifier.node_group = cls.get_or_load_node_group()
modifier.node_group.is_modifier = False # keep modifier dropdown clean
modifier.node_group.asset_clear() # match ops behavior
return cls.new_unchecked(modifier)
@classmethod
def get_filepath(cls):
'''Return chached filepath to the blend file'''
if not cls._filepath:
cls._filepath = cls.calc_asset_path()
return cls._filepath
@classmethod
def new_unchecked (cls, modifier):
'''Crate new wrapper from modifier without checking modifier validity and create input map for the types'''
if not cls.input_map:
cls.input_map_build(modifier)
new = cls()
new.modifier = modifier
new.show_group_selector = False
return new
@classmethod
def input_map_build(cls, modifier):
cls.input_map = {input.name : input.identifier for input in modifier.node_group.interface.items_tree if input.item_type == 'SOCKET' and input.in_out=='INPUT'}
@classmethod
def prop_draw_map_build(cls, modifier):
cls.prop_draw_map = {input.name.lower().replace(' ', '_') : input.identifier for input in modifier.node_group.interface.items_tree if input.item_type == 'SOCKET' and input.in_out=='INPUT'}
@classmethod
def is_valid_modifier (cls, modifier) -> bool:
'''Check if modifier object can be wrapped'''
if modifier.type != 'NODES': return False
if not modifier.node_group: return False
if not cls.is_asset_node_group(modifier.node_group): return False
if not cls.is_valid_node_group(modifier.node_group): return False
return True
@classmethod
def is_asset_node_group(cls, node_group) -> bool:
'''Check if node group has appropriate asset asset_name and filepath'''
weak_ref = node_group.library_weak_reference
# is not packed, so check id_name
if weak_ref:
return bool(weak_ref.id_name.endswith(cls.asset_name))
# is packed, so also check filename
# the name is not-user editable but it is not clear for name conflicts
if node_group.library:
name = pathlib.Path(node_group.library.filepath).name
if name != cls.blend_filename: return False
return node_group.name == cls.asset_name
return False
@classmethod
def builtin_geometry_nodes_path(cls) -> pathlib.Path:
'''Return OS-specific path to builtin geometry_nodes folder.'''
ver = f'{bpy.app.version[0]}.{bpy.app.version[1]}'
path = pathlib.Path(bpy.app.binary_path).resolve().parent # exec folder
if sys.platform == 'darwin':
path = path.parent / 'Resources' / ver
elif sys.platform == 'linux':
# installed via apt install
if path.name == 'bin':
path = path.parent / 'share' / 'blender'
path_ver = path / ver
if path_ver.exists():
path = path_ver
else:
path = path / ver
else:
path = path / ver
path = path / 'datafiles' / 'assets'
nodes = path / 'nodes'
if nodes.exists() : return nodes
return path / 'geometry_nodes'
@classmethod
def prop_draw_data(cls, modifier, string) -> tuple:
'''Get (data, property) pair for drawing modifier's prop with layout.prop\n
Inputs live on the modifier's IDProperties up to 5.1 and on their own RNA struct from 5.2\n
Returns (modifier, string) if the pair can't be calculated e.g. input modifier of incompatible type'''
if hasattr(modifier, string): return modifier, string
if not cls.prop_draw_map:
if not cls.is_valid_modifier(modifier): return modifier, string
cls.prop_draw_map_build(modifier)
identifier = cls.prop_draw_map.get(string, None)
if not identifier: return modifier, string
data, prop = compat.value_prop(modifier, identifier)
if data is None: return modifier, string
return data, prop
def update_tag(self):
'''Tag object and UI for update\n
Setting modifier values doesn't tag object update and has to be done manually\n'''
self.modifier.id_data.update_tag()
for area in bpy.context.screen.areas:
area.tag_redraw()
# Mandatory overloads
@classmethod
def is_valid_node_group(cls, node_group) -> bool:
'''Returns true if node group is compatible with wrapper e.g. I/O matches wrapper's expectation'''
raise NotImplementedError
@classmethod
def get_or_load_node_group(cls):
'''Get node group from data or load new copy if it's invalid'''
raise NotImplementedError
@classmethod
def calc_asset_path(cls) -> str:
'''Calculate absolute path to blend file the asset is stored in'''
raise NotImplementedError