575 lines
20 KiB
Python
575 lines
20 KiB
Python
"""Provides for the linking and/or appending of geometry nodes trees from vendored libraries included in Blender maxwell."""
|
|
|
|
import enum
|
|
from pathlib import Path
|
|
|
|
import bpy
|
|
|
|
from blender_maxwell import contracts as ct
|
|
from blender_maxwell.utils import logger
|
|
|
|
log = logger.get(__name__)
|
|
|
|
|
|
####################
|
|
# - GeoNodes Specification
|
|
####################
|
|
# GeoNodes Paths
|
|
## Internal
|
|
GN_INTERNAL_PATH = ct.addon.PATH_ASSETS / 'internal'
|
|
GN_INTERNAL_INPUTS_PATH = GN_INTERNAL_PATH / 'input'
|
|
GN_INTERNAL_SOURCES_PATH = GN_INTERNAL_PATH / 'source'
|
|
GN_INTERNAL_STRUCTURES_PATH = GN_INTERNAL_PATH / 'structure'
|
|
GN_INTERNAL_MONITORS_PATH = GN_INTERNAL_PATH / 'monitor'
|
|
GN_INTERNAL_SIMULATIONS_PATH = GN_INTERNAL_PATH / 'simulation'
|
|
|
|
## Structures
|
|
GN_STRUCTURES_PATH = ct.addon.PATH_ASSETS / 'structures'
|
|
GN_STRUCTURES_PRIMITIVES_PATH = GN_STRUCTURES_PATH / 'primitives'
|
|
GN_STRUCTURES_ARRAYS_PATH = GN_STRUCTURES_PATH / 'arrays'
|
|
|
|
|
|
class GeoNodes(enum.StrEnum):
|
|
"""Defines the names of available GeoNodes groups vendored as part of Blender Maxwell.
|
|
|
|
The values define both name of the .blend file containing the GeoNodes group, and the GeoNodes group itself.
|
|
"""
|
|
|
|
# Node Previews
|
|
## Input
|
|
InputConstantPhysicalPol = '_input_constant_physical_pol'
|
|
## Source
|
|
SourcePointDipole = '_source_point_dipole'
|
|
SourcePlaneWave = '_source_plane_wave'
|
|
SourceUniformCurrent = '_source_uniform_current'
|
|
SourceTFSF = '_source_tfsf'
|
|
SourceGaussianBeam = '_source_gaussian_beam'
|
|
SourceAstigmaticGaussianBeam = '_source_astigmatic_gaussian_beam'
|
|
SourceMode = '_source_mode'
|
|
SourceEHArray = '_source_eh_array'
|
|
SourceEHEquivArray = '_source_eh_equiv_array'
|
|
## Structure
|
|
StructurePrimitivePlane = '_structure_primitive_plane'
|
|
StructurePrimitiveBox = '_structure_primitive_box'
|
|
StructurePrimitiveSphere = '_structure_primitive_sphere'
|
|
StructurePrimitiveCylinder = '_structure_primitive_cylinder'
|
|
StructurePrimitiveRing = '_structure_primitive_ring'
|
|
StructurePrimitiveCapsule = '_structure_primitive_capsule'
|
|
StructurePrimitiveCone = '_structure_primitive_cone'
|
|
## Monitor
|
|
MonitorEHField = '_monitor_eh_field'
|
|
MonitorPowerFlux = '_monitor_power_flux'
|
|
MonitorEpsTensor = '_monitor_eps_tensor'
|
|
MonitorDiffraction = '_monitor_diffraction'
|
|
MonitorProjCartEHField = '_monitor_proj_eh_field'
|
|
MonitorProjAngEHField = '_monitor_proj_ang_eh_field'
|
|
MonitorProjKSpaceEHField = '_monitor_proj_k_space_eh_field'
|
|
## Simulation
|
|
SimulationSimDomain = '_simulation_sim_domain'
|
|
SimulationBoundConds = '_simulation_bound_conds'
|
|
SimulationBoundCondPML = '_simulation_bound_cond_pml'
|
|
SimulationBoundCondPEC = '_simulation_bound_cond_pec'
|
|
SimulationBoundCondPMC = '_simulation_bound_cond_pmc'
|
|
SimulationBoundCondBloch = '_simulation_bound_cond_bloch'
|
|
SimulationBoundCondPeriodic = '_simulation_bound_cond_periodic'
|
|
SimulationBoundCondAbsorbing = '_simulation_bound_cond_absorbing'
|
|
SimulationSimGrid = '_simulation_sim_grid'
|
|
SimulationSimGridAxisAuto = '_simulation_sim_grid_axis_auto'
|
|
SimulationSimGridAxisManual = '_simulation_sim_grid_axis_manual'
|
|
SimulationSimGridAxisUniform = '_simulation_sim_grid_axis_uniform'
|
|
SimulationSimGridAxisArray = '_simulation_sim_grid_axis_array'
|
|
|
|
# Structures
|
|
## Primitives
|
|
PrimitiveBox = 'box'
|
|
PrimitiveSphere = 'sphere'
|
|
PrimitiveCylinder = 'cylinder'
|
|
PrimitiveRing = 'ring'
|
|
## Arrays
|
|
ArrayRing = 'array_ring'
|
|
|
|
@property
|
|
def dedicated_node_type(self) -> ct.BLImportMethod:
|
|
"""Deduces the denode type that implements a vendored GeoNodes tree (usually just "GeoNodes Structure").
|
|
|
|
Generally, "GeoNodes Structure' is the generic triangle-mesh node that can do everything.
|
|
Indeed, one could make perfectly useful simulations exclusively using triangle meshes.
|
|
|
|
However, when nodes that use geometry directly supported by `Tidy3D` might be more performant, and support features (ex. differentiable parameters) critical to some simulations.
|
|
|
|
To bridge the gap, this method provides a regularized hard-coded method of getting whichever node type is most appropriate for the given structure.
|
|
|
|
Warnings:
|
|
**The strings be manually checked, statically**.
|
|
Since we don't have easy access to the `contracts` within the sim node tree, these are just plain strings.
|
|
|
|
It's not ideal, definitely a lazy workaround, but hey.
|
|
|
|
Returns:
|
|
The node type (as a string) that should be created to expose the GeoNodes group.
|
|
"""
|
|
dedicated_node_map = {
|
|
GeoNodes.PrimitiveBox: 'BoxStructureNodeType',
|
|
GeoNodes.PrimitiveSphere: 'SphereStructureNodeType',
|
|
GeoNodes.PrimitiveCylinder: 'CylinderStructureNodeType',
|
|
}
|
|
|
|
if dedicated_node_map.get(self) is None:
|
|
return 'GeoNodesStructureNodeType'
|
|
|
|
return dedicated_node_map[self]
|
|
|
|
@property
|
|
def import_method(self) -> ct.BLImportMethod:
|
|
"""Deduces whether a vendored GeoNodes tree should be linked or appended.
|
|
|
|
Currently, everything is linked.
|
|
If the user wants to modify a group, they can always make a local copy of a linked group and just use that.
|
|
|
|
Returns:
|
|
Either 'link' or 'append'.
|
|
"""
|
|
return 'link'
|
|
|
|
@property
|
|
def parent_path(self) -> Path:
|
|
"""Deduces the parent path of a vendored GeoNodes tree.
|
|
|
|
Warnings:
|
|
The guarantee of same-named `.blend` and tree name are not explicitly checked.
|
|
Also, the validity of GeoNodes tree interface parameters is also unchecked; this must be manually coordinated between the driving node, and the imported tree.
|
|
|
|
Returns:
|
|
The parent path of the given GeoNodes tree.
|
|
|
|
A `.blend` file of the same name as the value of the enum must be **guaranteed** to exist as a direct child of the returned path, and must be **guaranteed** to contain a GeoNodes tree of the same name.
|
|
"""
|
|
GN = GeoNodes
|
|
return {
|
|
# Node Previews
|
|
## Input
|
|
GN.InputConstantPhysicalPol: GN_INTERNAL_INPUTS_PATH,
|
|
## Source
|
|
GN.SourcePointDipole: GN_INTERNAL_SOURCES_PATH,
|
|
GN.SourcePlaneWave: GN_INTERNAL_SOURCES_PATH,
|
|
GN.SourceUniformCurrent: GN_INTERNAL_SOURCES_PATH,
|
|
GN.SourceTFSF: GN_INTERNAL_SOURCES_PATH,
|
|
GN.SourceGaussianBeam: GN_INTERNAL_SOURCES_PATH,
|
|
GN.SourceAstigmaticGaussianBeam: GN_INTERNAL_SOURCES_PATH,
|
|
GN.SourceMode: GN_INTERNAL_SOURCES_PATH,
|
|
GN.SourceEHArray: GN_INTERNAL_SOURCES_PATH,
|
|
GN.SourceEHEquivArray: GN_INTERNAL_SOURCES_PATH,
|
|
## Structure
|
|
GN.StructurePrimitivePlane: GN_INTERNAL_STRUCTURES_PATH,
|
|
GN.StructurePrimitiveBox: GN_INTERNAL_STRUCTURES_PATH,
|
|
GN.StructurePrimitiveSphere: GN_INTERNAL_STRUCTURES_PATH,
|
|
GN.StructurePrimitiveCylinder: GN_INTERNAL_STRUCTURES_PATH,
|
|
GN.StructurePrimitiveRing: GN_INTERNAL_STRUCTURES_PATH,
|
|
GN.StructurePrimitiveCapsule: GN_INTERNAL_STRUCTURES_PATH,
|
|
GN.StructurePrimitiveCone: GN_INTERNAL_STRUCTURES_PATH,
|
|
## Monitor
|
|
GN.MonitorEHField: GN_INTERNAL_STRUCTURES_PATH,
|
|
GN.MonitorPowerFlux: GN_INTERNAL_STRUCTURES_PATH,
|
|
GN.MonitorEpsTensor: GN_INTERNAL_STRUCTURES_PATH,
|
|
GN.MonitorDiffraction: GN_INTERNAL_STRUCTURES_PATH,
|
|
GN.MonitorProjCartEHField: GN_INTERNAL_STRUCTURES_PATH,
|
|
GN.MonitorProjAngEHField: GN_INTERNAL_STRUCTURES_PATH,
|
|
GN.MonitorProjKSpaceEHField: GN_INTERNAL_STRUCTURES_PATH,
|
|
## Simulation
|
|
GN.SimulationSimDomain: GN_INTERNAL_SIMULATIONS_PATH,
|
|
GN.SimulationBoundConds: GN_INTERNAL_SIMULATIONS_PATH,
|
|
GN.SimulationBoundCondPML: GN_INTERNAL_SIMULATIONS_PATH,
|
|
GN.SimulationBoundCondPEC: GN_INTERNAL_SIMULATIONS_PATH,
|
|
GN.SimulationBoundCondPMC: GN_INTERNAL_SIMULATIONS_PATH,
|
|
GN.SimulationBoundCondBloch: GN_INTERNAL_SIMULATIONS_PATH,
|
|
GN.SimulationBoundCondPeriodic: GN_INTERNAL_SIMULATIONS_PATH,
|
|
GN.SimulationBoundCondAbsorbing: GN_INTERNAL_SIMULATIONS_PATH,
|
|
GN.SimulationSimGrid: GN_INTERNAL_SIMULATIONS_PATH,
|
|
GN.SimulationSimGridAxisAuto: GN_INTERNAL_SIMULATIONS_PATH,
|
|
GN.SimulationSimGridAxisManual: GN_INTERNAL_SIMULATIONS_PATH,
|
|
GN.SimulationSimGridAxisUniform: GN_INTERNAL_SIMULATIONS_PATH,
|
|
GN.SimulationSimGridAxisArray: GN_INTERNAL_SIMULATIONS_PATH,
|
|
# Structures
|
|
## Primitives
|
|
GN.PrimitiveBox: GN_STRUCTURES_PRIMITIVES_PATH,
|
|
GN.PrimitiveRing: GN_STRUCTURES_PRIMITIVES_PATH,
|
|
GN.PrimitiveSphere: GN_STRUCTURES_PRIMITIVES_PATH,
|
|
## Arrays
|
|
GN.ArrayRing: GN_STRUCTURES_ARRAYS_PATH,
|
|
}[self]
|
|
|
|
|
|
####################
|
|
# - Import GeoNodes (Link/Append)
|
|
####################
|
|
def import_geonodes(
|
|
_geonodes: GeoNodes,
|
|
force_append: bool = False,
|
|
) -> bpy.types.GeometryNodeGroup:
|
|
"""Given vendored GeoNodes tree link/append and return the local datablock.
|
|
|
|
- `GeoNodes.import_method` is used to determine whether it should be linked or imported.
|
|
- `GeoNodes.parent_path` is used to determine
|
|
|
|
Parameters:
|
|
geonodes: The (name of the) GeoNodes group, which ships with Blender Maxwell.
|
|
|
|
Returns:
|
|
A GeoNodes group available in the current .blend file, which can ex. be attached to a 'GeoNodes Structure' node.
|
|
"""
|
|
# Parse Input
|
|
geonodes = GeoNodes(_geonodes)
|
|
import_method = geonodes.import_method
|
|
|
|
# Linked: Don't Re-Link
|
|
if import_method == 'link' and geonodes in bpy.data.node_groups:
|
|
return bpy.data.node_groups[geonodes]
|
|
|
|
filename = geonodes
|
|
filepath = str(geonodes.parent_path / (geonodes + '.blend') / 'NodeTree' / geonodes)
|
|
directory = filepath.removesuffix(geonodes)
|
|
log.info(
|
|
'%s GeoNodes (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 Panel for MaxwellSimTree
|
|
####################
|
|
ASSET_PANEL_NAME: str = 'blender_maxwell__node_asset_shelf'
|
|
WM_ACTIVE_NODE_ASSETS: str = 'blender_maxwell__active_node_asset_list'
|
|
WM_ACTIVE_ASSET_IDX: str = 'blender_maxwell__active_node_asset_index'
|
|
|
|
|
|
class NodeAssetPanel(bpy.types.Panel):
|
|
"""Provides a panel that displays vendored, usable GeoNodes trees, and enables drag-and-drop support to easily drag them into the simulation node editor."""
|
|
|
|
## TODO: Provide an option that forces appending? So that users can modify from a baseline. Just watch out - dealing with overlaps isn't trivial.
|
|
|
|
bl_idname = ct.PanelType.NodeAssetPanel
|
|
bl_label = 'Node GeoNodes Asset Panel'
|
|
bl_space_type = 'NODE_EDITOR'
|
|
bl_region_type = 'UI'
|
|
bl_category = 'Assets'
|
|
|
|
@classmethod
|
|
def poll(cls, context: bpy.types.Context) -> bool:
|
|
"""Require window manager properties to show node assets.
|
|
|
|
Notes:
|
|
Run by Blender when trying to show a panel.
|
|
|
|
Returns:
|
|
Whether the panel can show.
|
|
"""
|
|
## TODO: Check that we're in a MaxwellSim node tree as well.
|
|
wm = context.window_manager
|
|
return hasattr(wm, WM_ACTIVE_NODE_ASSETS) and hasattr(wm, WM_ACTIVE_ASSET_IDX)
|
|
|
|
def draw(self, context: bpy.types.Context) -> None:
|
|
"""Draw the asset library w/drag support.
|
|
|
|
Notes:
|
|
Run by Blender when the panel needs to be displayed.
|
|
|
|
Parameters:
|
|
context: The Blender context object.
|
|
Must contain `context.window_manager` and `context.workspace`.
|
|
"""
|
|
layout = self.layout
|
|
|
|
# Draw the Asset Panel w/Drag Support
|
|
_activate_op_props, _drag_op_props = layout.template_asset_view(
|
|
# list_id
|
|
## Identifies this particular UI-bound asset list.
|
|
## Must be unique, otherwise weird things happen if invoked shown twice.
|
|
## We use a custom name, presuming only one tree is shown at a time.
|
|
## TODO: Allow several trees to show at once.
|
|
ASSET_PANEL_NAME,
|
|
# asset_library_[dataptr|propname]
|
|
## Where to find the asset library to pull assets from.
|
|
## We pull it from a global
|
|
context.workspace,
|
|
'asset_library_reference',
|
|
# assets_[dataptr|propname]
|
|
## The actual assets to provide user access to.
|
|
context.window_manager,
|
|
WM_ACTIVE_NODE_ASSETS,
|
|
# assets_[dataptr|propname]
|
|
## The currently selected asset to highlight.
|
|
context.window_manager,
|
|
WM_ACTIVE_ASSET_IDX,
|
|
# draw_operator
|
|
## The operator to invoke() whenever the user **starts** dragging an asset.
|
|
drag_operator=ct.OperatorType.GeoNodesToStructureNode,
|
|
# Other Options
|
|
## The currently selected asset to highlight.
|
|
# display_options={'NO_LIBRARY'},
|
|
)
|
|
|
|
|
|
####################
|
|
# - Append GeoNodes Operator
|
|
####################
|
|
def get_view_location(
|
|
region: bpy.types.Region, coords: tuple[float, float], ui_scale: float
|
|
) -> tuple[float, float]:
|
|
"""Given a pair of coordinates defined on a Blender region, project the coordinates to the corresponding `bpy.types.View2D`.
|
|
|
|
Parameters:
|
|
region: The region within which the coordinates are defined.
|
|
coords: The coordinates within the region, ex. produced during a mouse click event.
|
|
ui_scale: Scaling factor applied to pixels, which compensates for screens with differing DPIs.
|
|
We must divide the coordinates we receive by this float to get the "real" $x,y$ coordinates.
|
|
|
|
Returns:
|
|
The $x,y$ coordinates within the region's `bpy.types.View2D`, correctly scaled with the current interface size/dpi.
|
|
"""
|
|
x, y = region.view2d.region_to_view(*coords)
|
|
return x / ui_scale, y / ui_scale
|
|
|
|
|
|
class GeoNodesToStructureNode(bpy.types.Operator):
|
|
"""Operator allowing the user to append a vendored GeoNodes tree for use in a simulation."""
|
|
|
|
bl_idname = ct.OperatorType.GeoNodesToStructureNode
|
|
bl_label = 'GeoNodes to Structure Node'
|
|
bl_description = 'Drag-and-drop operator'
|
|
bl_options = frozenset({'REGISTER'})
|
|
|
|
@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
|
|
|
|
####################
|
|
# - Properties
|
|
####################
|
|
_asset: bpy.types.AssetRepresentation | None = None
|
|
|
|
####################
|
|
# - Execution
|
|
####################
|
|
def invoke(
|
|
self, context: bpy.types.Context, _: bpy.types.Event
|
|
) -> set[ct.BLOperatorStatus]:
|
|
"""Commences the drag-and-drop of a GeoNodes asset.
|
|
|
|
- Starts the modal timer, which listens for a mouse-release event.
|
|
- Changes the mouse cursor to communicate the "dragging" state to the user.
|
|
|
|
Notes:
|
|
Run via `NodeAssetPanel` when the user starts dragging a vendored GeoNodes asset.
|
|
|
|
The asset being dragged can be found in `context.asset`.
|
|
|
|
Returns:
|
|
Indication that there is a modal running.
|
|
"""
|
|
self._asset = context.asset ## Guaranteed non-None by poll()
|
|
|
|
# Register Modal Operator
|
|
context.window_manager.modal_handler_add(self)
|
|
context.area.tag_redraw()
|
|
|
|
# Set Mouse Cursor
|
|
context.window.cursor_modal_set('CROSS')
|
|
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def modal(
|
|
self, context: bpy.types.Context, event: bpy.types.Event
|
|
) -> ct.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.
|
|
"""
|
|
# No Asset: Do Nothing
|
|
asset = self._asset
|
|
if asset is None:
|
|
return {'PASS_THROUGH'}
|
|
|
|
# Released LMB: Add Structure Node
|
|
## The user 'dropped' the asset!
|
|
## Now, we create an appropriate "Structure" node corresponding to the dropped asset.
|
|
if event.type == 'LEFTMOUSE' and event.value == 'RELEASE':
|
|
log.debug('Dropped Asset: %s', asset.name)
|
|
|
|
# Load Editor Region
|
|
area = context.area
|
|
editor_window_regions = [
|
|
region for region in area.regions.values() if region.type == 'WINDOW'
|
|
]
|
|
if editor_window_regions:
|
|
log.debug(
|
|
'Selected Node Editor Region out of %s Options',
|
|
str(len(editor_window_regions)),
|
|
)
|
|
editor_region = editor_window_regions[0]
|
|
else:
|
|
msg = 'No valid editor region in context where asset was dropped'
|
|
raise RuntimeError(msg)
|
|
|
|
# Check if Mouse Coordinates are:
|
|
## - INSIDE of Node Editor
|
|
## - INSIDE of Node Editor's WINDOW Region
|
|
# Check Mouse Coordinates against Node Editor
|
|
## The asset must be dropped inside the Maxwell Sim node editor.
|
|
if (
|
|
## Check: Mouse in Area (contextual)
|
|
(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 (
|
|
## Check: Mouse in Node Editor Region
|
|
(
|
|
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
|
|
)
|
|
):
|
|
space = context.space_data
|
|
node_tree = space.node_tree
|
|
|
|
# Computing GeoNodes View Location
|
|
## 1. node_tree.cursor_location gives clicked loc, not released.
|
|
## 2. event.mouse_region_* has inverted x wrt. event.mouse_*.
|
|
## -> View2D.region_to_view expects the event.mouse_* order.
|
|
## -> Is it a bug? Who knows!
|
|
## 3. We compute it manually, to avoid the jank.
|
|
node_location = get_view_location(
|
|
editor_region,
|
|
[
|
|
event.mouse_x - editor_region.x,
|
|
event.mouse_y - editor_region.y,
|
|
],
|
|
context.preferences.system.ui_scale,
|
|
)
|
|
|
|
# Create GeoNodes Structure Node
|
|
## 1. Deselect other nodes
|
|
## 2. Select the new one
|
|
## 3. Move it into place
|
|
## 4. Redraw (so we see the new node right away)
|
|
geonodes = GeoNodes(asset.name)
|
|
|
|
log.info(
|
|
'Creating Node "%s" at (%d, %d)',
|
|
geonodes.dedicated_node_type,
|
|
*tuple(node_location),
|
|
)
|
|
bpy.ops.node.select_all(action='DESELECT')
|
|
node = node_tree.nodes.new(geonodes.dedicated_node_type)
|
|
node.select = True
|
|
node.location.x = node_location[0]
|
|
node.location.y = node_location[1]
|
|
context.area.tag_redraw()
|
|
|
|
# GeoNodes Structure Specific Setup
|
|
## Since the node doesn't itself handle the structure, we must.
|
|
## We just import the GN tree, then attach the data block to the node.
|
|
if geonodes.dedicated_node_type == 'GeoNodesStructureNodeType':
|
|
geonodes_data = import_geonodes(asset.name)
|
|
node.inputs['GeoNodes'].value = geonodes_data
|
|
## TODO: Is this too presumptuous? Or a fine little hack?
|
|
|
|
# Restore the Pre-Modal Mouse Cursor Shape
|
|
context.window.cursor_modal_restore()
|
|
return {'FINISHED'}
|
|
|
|
return {'RUNNING_MODAL'}
|
|
|
|
|
|
####################
|
|
# - Blender Registration
|
|
####################
|
|
ASSET_LIB_POSTFIX: str = ' | BLMaxwell'
|
|
ASSET_LIB_SPECS: dict[str, Path] = {
|
|
'Primitives': GN_STRUCTURES_PRIMITIVES_PATH,
|
|
'Arrays': GN_STRUCTURES_ARRAYS_PATH,
|
|
}
|
|
|
|
|
|
@bpy.app.handlers.persistent
|
|
def initialize_asset_libraries(_: bpy.types.Scene):
|
|
"""Before loading a `.blend` file, ensure that the WindowManager properties relied on by NodeAssetPanel are available.
|
|
|
|
- Several asset libraries, defined under the global `ASSET_LIB_SPECS`, are added/replaced such that the name:path map is respected.
|
|
- Existing asset libraries are left entirely alone.
|
|
- The names attached to `bpy.types.WindowManager` are specifically the globals `WM_ACTIVE_NODE_ASSETS` and `WM_ACTIVE_ASSET_IDX`.
|
|
|
|
Warnings:
|
|
**Changing the name of an asset library will leave it dangling on all `.blend` files**.
|
|
|
|
Therefore, an addon-specific postfix `ASSET_LIB_POSTFIX` is appended to all asset library names.
|
|
**This postfix must never, ever change, across any and all versions of the addon**.
|
|
|
|
Again, if it does, **all Blender installations having ever used the addon will have a dangling asset library until the user manually notices + removes it**.
|
|
"""
|
|
asset_libraries = bpy.context.preferences.filepaths.asset_libraries
|
|
|
|
# Guarantee All Asset Libraries
|
|
for _asset_lib_name, asset_lib_path in ASSET_LIB_SPECS.items():
|
|
asset_lib_name = _asset_lib_name + ASSET_LIB_POSTFIX
|
|
|
|
# Remove Existing Asset Library
|
|
asset_library_idx = asset_libraries.find(asset_lib_name)
|
|
if asset_library_idx != -1 and asset_libraries[asset_lib_name].path != str(
|
|
asset_lib_path
|
|
):
|
|
log.debug('Removing Asset Library: %s', asset_lib_name)
|
|
bpy.ops.preferences.asset_library_remove(asset_library_idx)
|
|
|
|
# Add Asset Library
|
|
if asset_lib_name not in asset_libraries:
|
|
log.debug('Add Asset Library: %s', asset_lib_name)
|
|
|
|
bpy.ops.preferences.asset_library_add()
|
|
asset_library = asset_libraries[-1] ## Was added to the end
|
|
asset_library.name = asset_lib_name
|
|
asset_library.path = str(asset_lib_path)
|
|
|
|
# Set WindowManager Props
|
|
## Set Active Assets Collection
|
|
setattr(
|
|
bpy.types.WindowManager,
|
|
WM_ACTIVE_NODE_ASSETS,
|
|
bpy.props.CollectionProperty(type=bpy.types.AssetHandle),
|
|
)
|
|
## Set Active Assets Collection
|
|
setattr(
|
|
bpy.types.WindowManager,
|
|
WM_ACTIVE_ASSET_IDX,
|
|
bpy.props.IntProperty(),
|
|
)
|
|
|
|
|
|
bpy.app.handlers.load_pre.append(initialize_asset_libraries)
|
|
|
|
BL_REGISTER = [
|
|
NodeAssetPanel,
|
|
GeoNodesToStructureNode,
|
|
]
|
|
|
|
BL_HOTKEYS = []
|