"""Provides for the linking and/or appending of geometry nodes trees from vendored libraries included in Blender maxwell.""" import enum import typing as typ from pathlib import Path import bpy import typing_extensions as typx from .. import info from ..utils import logger log = logger.get(__name__) BLOperatorStatus: typ.TypeAlias = set[ typx.Literal['RUNNING_MODAL', 'CANCELLED', 'FINISHED', 'PASS_THROUGH', 'INTERFACE'] ] #################### # - GeoNodes Specification #################### class GeoNodes(enum.StrEnum): """Defines available GeoNodes groups vendored as part of Blender Maxwell. The value of this StrEnum is both the name of the .blend file containing the GeoNodes group, and of the GeoNodes group itself. """ PrimitiveBox = 'box' PrimitiveRing = 'ring' PrimitiveSphere = 'sphere' # GeoNodes Path Mapping GN_PRIMITIVES_PATH = info.PATH_ASSETS / 'geonodes' / 'primitives' GN_PARENT_PATHS: dict[GeoNodes, Path] = { GeoNodes.PrimitiveBox: GN_PRIMITIVES_PATH, GeoNodes.PrimitiveRing: GN_PRIMITIVES_PATH, GeoNodes.PrimitiveSphere: GN_PRIMITIVES_PATH, } #################### # - Import GeoNodes (Link/Append) #################### ImportMethod: typ.TypeAlias = typx.Literal['append', 'link'] def import_geonodes( geonodes: GeoNodes, import_method: ImportMethod, force_import: bool = False, ) -> bpy.types.GeometryNodeGroup: """Given a pre-defined GeoNodes group packaged with Blender Maxwell. The procedure is as follows: - Link it to the current .blend file. - Retrieve the node group and return it. """ if geonodes in bpy.data.node_groups and not force_import: log.info( 'Found Existing GeoNodes Tree (name=%s)', geonodes ) return bpy.data.node_groups[geonodes] filename = geonodes filepath = str( GN_PARENT_PATHS[geonodes] / (geonodes + '.blend') / 'NodeTree' / geonodes ) directory = filepath.removesuffix(geonodes) log.info( '% GeoNodes Tree (filename=%s, directory=%s, filepath=%s)', "Linking" if import_method == 'link' else "Appending" filename, directory, filepath, ) bpy.ops.wm.append( filepath=filepath, directory=directory, filename=filename, check_existing=False, set_fake=True, link=import_method == 'link', ) return bpy.data.node_groups[geonodes] #################### # - GeoNodes Asset Shelf #################### # class GeoNodesAssetShelf(bpy.types.AssetShelf): # bl_space_type = 'NODE_EDITOR' # bl_idname = 'blender_maxwell.asset_shelf__geonodes' # bl_options = {'NO_ASSET_DRAG'} # # @classmethod # def poll(cls, context): # return ( # (space := context.get('space_data')) # and (node_tree := space.get('node_tree')) # and (node_tree.bl_idname == 'MaxwellSimTreeType') # ) # # @classmethod # def asset_poll(cls, asset: bpy.types.AssetRepresentation): # return asset.id_type == 'NODETREE' #################### # - GeoNodes Asset Shelf Panel for MaxwellSimTree #################### class NodeAssetPanel(bpy.types.Panel): bl_idname = 'blender_maxwell.panel__node_asset_panel' bl_label = 'Node GeoNodes Asset Panel' bl_space_type = 'NODE_EDITOR' bl_region_type = 'UI' bl_category = 'Assets' # @classmethod # def poll(cls, context): # return ( # (space := context.get('space_data')) is not None # and (node_tree := space.get('node_tree')) is not None # and (node_tree.bl_idname == 'MaxwellSimTreeType') # ) def draw(self, context): layout = self.layout workspace = context.workspace wm = context.window_manager # list_id must be unique otherwise behaviour gets weird when the template_asset_view is shown twice # (drag operator stops working in AssetPanelDrag, clickable area of all Assets in AssetPanelNoDrag gets # reduced to below the Asset name and clickable area of Current File Assets in AssetPanelDrag gets # reduced as if it didn't have a drag operator) _activate_op_props, _drag_op_props = layout.template_asset_view( 'geo_nodes_asset_shelf', workspace, 'asset_library_reference', wm, 'active_asset_list', wm, 'active_asset_index', drag_operator=AppendGeoNodes.bl_idname, ) #################### # - Append GeoNodes Operator #################### def get_view_location(region, coords, ui_scale): x, y = region.view2d.region_to_view(*coords) return x / ui_scale, y / ui_scale class AppendGeoNodes(bpy.types.Operator): """Operator allowing the user to append a vendored GeoNodes tree for use in a simulation.""" bl_idname = 'blender_maxwell.blends__import_geo_nodes' bl_label = 'Import GeoNode Tree' bl_description = 'Append a geometry node tree from the Blender Maxwell plugin, either via linking or appending' bl_options = frozenset({'REGISTER'}) #################### # - Properties #################### _asset: bpy.types.AssetRepresentation | None = None _start_drag_x: bpy.props.IntProperty() _start_drag_y: bpy.props.IntProperty() #################### # - UI #################### def draw(self, _: bpy.types.Context) -> None: """Draws the UI of the operator.""" layout = self.layout col = layout.column() col.prop(self, 'geonodes_to_append', expand=True) #################### # - Execution #################### @classmethod def poll(cls, context: bpy.types.Context) -> bool: """Defines when the operator can be run. Returns: Whether the operator can be run. """ return context.asset is not None def invoke(self, context, event): self._start_drag_x = event.mouse_x self._start_drag_y = event.mouse_y return self.execute(context) def execute(self, context: bpy.types.Context) -> BLOperatorStatus: """Initializes the while-dragging modal handler, which executes custom logic when the mouse button is released. Runs in response to drag_handler of a `UILayout.template_asset_view`. """ asset: bpy.types.AssetRepresentation = context.asset log.info('Dragging Asset: %s', asset.name) # Store Asset for Modal & Drag Start self._asset = context.asset # Register Modal Operator & Tag Area for Redraw context.window_manager.modal_handler_add(self) context.area.tag_redraw() # Set Modal Cursor context.window.cursor_modal_set('CROSS') # Return Status of Running Modal return {'RUNNING_MODAL'} def modal( self, context: bpy.types.Context, event: bpy.types.Event ) -> BLOperatorStatus: """When LMB is released, creates a GeoNodes Structure node. Runs in response to events in the node editor while dragging an asset from the side panel. """ if (asset := self._asset) is None: return {'PASS_THROUGH'} if event.type == 'LEFTMOUSE' and event.value == 'RELEASE': log.info('Released Dragged Asset: %s', asset.name) area = context.area editor_region = next( region for region in area.regions.values() if region.type == 'WINDOW' ) # Check if Mouse Coordinates are: ## - INSIDE of Node Editor ## - INSIDE of Node Editor's WINDOW Region if ( (event.mouse_x >= area.x and event.mouse_x < area.x + area.width) and (event.mouse_y >= area.y and event.mouse_y < area.y + area.height) ) and ( ( event.mouse_x >= editor_region.x and event.mouse_x < editor_region.x + editor_region.width ) and ( event.mouse_y >= editor_region.y and event.mouse_y < editor_region.y + editor_region.height ) ): log.info( 'Asset "%s" Released in Main Window of Node Editor', asset.name ) space = context.space_data node_tree = space.node_tree ui_scale = context.preferences.system.ui_scale node_location = get_view_location( editor_region, [ event.mouse_x - editor_region.x, event.mouse_y - editor_region.y, ], ui_scale, ) # Create GeoNodes Structure Node #space.cursor_location_from_region(*node_location) log.info( 'Creating GeoNodes Structure Node at (%d, %d)', *tuple(space.cursor_location), ) bpy.ops.node.select_all(action='DESELECT') structure_node = node_tree.nodes.new('GeoNodesStructureNodeType') structure_node.select = True structure_node.location.x = node_location[0] structure_node.location.y = node_location[1] context.area.tag_redraw() print(structure_node.location) # Import the GeoNodes Structure geonodes = import_geonodes(asset.name, 'append') # Create the GeoNodes Node # Create a GeoNodes Structure w/Designated GeoNodes Group @ Mouse Position context.window.cursor_modal_restore() return {'FINISHED'} return {'RUNNING_MODAL'} #################### # - Blender Registration #################### # def initialize_asset_libraries(_: bpy.types.Scene): # bpy.app.handlers.load_post.append(initialize_asset_libraries) ## TODO: Move to top-level registration. asset_libraries = bpy.context.preferences.filepaths.asset_libraries if ( asset_library_idx := asset_libraries.find('Blender Maxwell') ) != -1 and asset_libraries['Blender Maxwell'].path != str(info.PATH_ASSETS): bpy.ops.preferences.asset_library_remove(asset_library_idx) if 'Blender Maxwell' not in asset_libraries: bpy.ops.preferences.asset_library_add() asset_library = asset_libraries[-1] ## Since the operator adds to the end asset_library.name = 'Blender Maxwell' asset_library.path = str(info.PATH_ASSETS) bpy.types.WindowManager.active_asset_list = bpy.props.CollectionProperty( type=bpy.types.AssetHandle ) bpy.types.WindowManager.active_asset_index = bpy.props.IntProperty() ## TODO: Do something differently BL_REGISTER = [ # GeoNodesAssetShelf, NodeAssetPanel, AppendGeoNodes, ] BL_KEYMAP_ITEM_DEFS = [ # { # '_': [ # AppendGeoNodes.bl_idname, # 'LEFTMOUSE', # 'CLICK_DRAG', # ], # 'ctrl': False, # 'shift': False, # 'alt': False, # } ]