enhance: Streamlined GN asset system

main
Sofus Albert Høgsbro Rose 2024-04-27 15:55:15 +02:00
parent 4e1eb19a88
commit 0599eed2b2
Signed by: so-rose
GPG Key ID: AD901CB0F3701434
24 changed files with 638 additions and 482 deletions

View File

@ -108,7 +108,7 @@ quartodoc:
desc: Blender assets bundled w/Blender Maxwell
contents:
- assets
- assets.import_geonodes
- assets.geonodes
- subtitle: "`bl_maxwell.nodeps`"
desc: No-Dependency

View File

@ -1,11 +1,11 @@
from . import import_geonodes
from . import geonodes
BL_REGISTER = [
*import_geonodes.BL_REGISTER,
*geonodes.BL_REGISTER,
]
BL_HOTKEYS = [
*import_geonodes.BL_HOTKEYS,
*geonodes.BL_HOTKEYS,
]
__all__ = [

BIN
src/blender_maxwell/assets/assets.blend (Stored with Git LFS)

Binary file not shown.

View File

@ -1,10 +0,0 @@
# This is an Asset Catalog Definition file for Blender.
#
# Empty lines and lines starting with `#` will be ignored.
# The first non-ignored line should be the version indicator.
# Other lines are of the format "UUID:catalog/path/for/assets:simple catalog name"
VERSION 1
783a7efe-c424-42b1-9771-42c862515891:Structures:Structures
ccb80eec-7e20-453d-89fb-0486b7abf7d4:Structures/Primitives:Structures-Primitives

View File

@ -0,0 +1,574 @@
"""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 = []

View File

@ -1,410 +0,0 @@
"""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
####################
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.
"""
# 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'
PrimitiveRing = 'ring'
PrimitiveSphere = 'sphere'
# GeoNodes Paths
## Internal
GN_INTERNAL_PATH = ct.addon.PATH_ASSETS / 'internal' / 'primitives'
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_PARENT_PATHS: dict[GeoNodes, Path] = {
# Node Previews
## Input
GeoNodes.InputConstantPhysicalPol: GN_INTERNAL_INPUTS_PATH,
## Source
GeoNodes.SourcePointDipole: GN_INTERNAL_SOURCES_PATH,
GeoNodes.SourcePlaneWave: GN_INTERNAL_SOURCES_PATH,
GeoNodes.SourceUniformCurrent: GN_INTERNAL_SOURCES_PATH,
GeoNodes.SourceTFSF: GN_INTERNAL_SOURCES_PATH,
GeoNodes.SourceGaussianBeam: GN_INTERNAL_SOURCES_PATH,
GeoNodes.SourceAstigmaticGaussianBeam: GN_INTERNAL_SOURCES_PATH,
GeoNodes.SourceMode: GN_INTERNAL_SOURCES_PATH,
GeoNodes.SourceEHArray: GN_INTERNAL_SOURCES_PATH,
GeoNodes.SourceEHEquivArray: GN_INTERNAL_SOURCES_PATH,
## Structure
GeoNodes.StructurePrimitivePlane: GN_INTERNAL_STRUCTURES_PATH,
GeoNodes.StructurePrimitiveBox: GN_INTERNAL_STRUCTURES_PATH,
GeoNodes.StructurePrimitiveSphere: GN_INTERNAL_STRUCTURES_PATH,
GeoNodes.StructurePrimitiveCylinder: GN_INTERNAL_STRUCTURES_PATH,
GeoNodes.StructurePrimitiveRing: GN_INTERNAL_STRUCTURES_PATH,
GeoNodes.StructurePrimitiveCapsule: GN_INTERNAL_STRUCTURES_PATH,
GeoNodes.StructurePrimitiveCone: GN_INTERNAL_STRUCTURES_PATH,
## Monitor
GeoNodes.MonitorEHField: GN_INTERNAL_STRUCTURES_PATH,
GeoNodes.MonitorPowerFlux: GN_INTERNAL_STRUCTURES_PATH,
GeoNodes.MonitorEpsTensor: GN_INTERNAL_STRUCTURES_PATH,
GeoNodes.MonitorDiffraction: GN_INTERNAL_STRUCTURES_PATH,
GeoNodes.MonitorProjCartEHField: GN_INTERNAL_STRUCTURES_PATH,
GeoNodes.MonitorProjAngEHField: GN_INTERNAL_STRUCTURES_PATH,
GeoNodes.MonitorProjKSpaceEHField: GN_INTERNAL_STRUCTURES_PATH,
## Simulation
GeoNodes.SimulationSimDomain: GN_INTERNAL_SIMULATIONS_PATH,
GeoNodes.SimulationBoundConds: GN_INTERNAL_SIMULATIONS_PATH,
GeoNodes.SimulationBoundCondPML: GN_INTERNAL_SIMULATIONS_PATH,
GeoNodes.SimulationBoundCondPEC: GN_INTERNAL_SIMULATIONS_PATH,
GeoNodes.SimulationBoundCondPMC: GN_INTERNAL_SIMULATIONS_PATH,
GeoNodes.SimulationBoundCondBloch: GN_INTERNAL_SIMULATIONS_PATH,
GeoNodes.SimulationBoundCondPeriodic: GN_INTERNAL_SIMULATIONS_PATH,
GeoNodes.SimulationBoundCondAbsorbing: GN_INTERNAL_SIMULATIONS_PATH,
GeoNodes.SimulationSimGrid: GN_INTERNAL_SIMULATIONS_PATH,
GeoNodes.SimulationSimGridAxisAuto: GN_INTERNAL_SIMULATIONS_PATH,
GeoNodes.SimulationSimGridAxisManual: GN_INTERNAL_SIMULATIONS_PATH,
GeoNodes.SimulationSimGridAxisUniform: GN_INTERNAL_SIMULATIONS_PATH,
GeoNodes.SimulationSimGridAxisArray: GN_INTERNAL_SIMULATIONS_PATH,
# Structures
GeoNodes.PrimitiveBox: GN_STRUCTURES_PRIMITIVES_PATH,
GeoNodes.PrimitiveRing: GN_STRUCTURES_PRIMITIVES_PATH,
GeoNodes.PrimitiveSphere: GN_STRUCTURES_PRIMITIVES_PATH,
}
####################
# - Import GeoNodes (Link/Append)
####################
def import_geonodes(
geonodes: GeoNodes,
import_method: ct.BLImportMethod,
) -> bpy.types.GeometryNodeGroup:
"""Given a (name of a) GeoNodes group packaged with Blender Maxwell, link/append it to the current file, and return the node group.
Parameters:
geonodes: The (name of the) GeoNodes group, which ships with Blender Maxwell.
import_method: Whether to link or append the GeoNodes group.
When 'link', repeated calls will not link a new group; the existing group will simply be returned.
Returns:
A GeoNodes group available in the current .blend file, which can ex. be attached to a 'GeoNodes Structure' node.
"""
if import_method == 'link' and geonodes in bpy.data.node_groups:
return bpy.data.node_groups[geonodes]
filename = geonodes
filepath = str(
GN_PARENT_PATHS[geonodes] / (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
####################
class NodeAssetPanel(bpy.types.Panel):
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):
# 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
####################
# - 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: bpy.types.Context, _):
return self.execute(context)
def execute(self, context: bpy.types.Context) -> ct.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.debug('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
) -> 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.
"""
if (asset := self._asset) is None:
return {'PASS_THROUGH'}
if event.type == 'LEFTMOUSE' and event.value == 'RELEASE':
log.debug('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
# 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)
log.info(
'Creating GeoNodes Structure Node at (%d, %d)',
*tuple(node_location),
)
bpy.ops.node.select_all(action='DESELECT')
node = node_tree.nodes.new('GeoNodesStructureNodeType')
node.select = True
node.location.x = node_location[0]
node.location.y = node_location[1]
context.area.tag_redraw()
# Import & Attach the GeoNodes Tree to the Node
geonodes = import_geonodes(asset.name, 'append')
node.inputs['GeoNodes'].value = geonodes
# Restore the Pre-Modal Mouse Cursor Shape
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(ct.addon.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(ct.addon.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_HOTKEYS = [
# {
# '_': [
# AppendGeoNodes.bl_idname,
# 'LEFTMOUSE',
# 'CLICK_DRAG',
# ],
# 'ctrl': False,
# 'shift': False,
# 'alt': False,
# }
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -16,6 +16,8 @@ class OperatorType(enum.StrEnum):
ConnectViewerNode = enum.auto()
GeoNodesToStructureNode = enum.auto()
# Socket: Tidy3DCloudTask
SocketCloudAuthenticate = enum.auto()
SocketReloadCloudFolderList = enum.auto()

View File

@ -82,7 +82,7 @@ class NodeType(blender_type_enum.BlenderTypeEnum):
## Structures / Primitives
BoxStructure = enum.auto()
SphereStructure = enum.auto()
# CylinderStructure = enum.auto()
CylinderStructure = enum.auto()
# Bounds
BoundConds = enum.auto()

View File

@ -3,10 +3,10 @@ import typing as typ
import sympy as sp
import tidy3d as td
from blender_maxwell.assets.geonodes import GeoNodes, import_geonodes
from blender_maxwell.utils import extra_sympy_units as spux
from blender_maxwell.utils import logger
from .....assets.import_geonodes import GeoNodes, import_geonodes
from ... import contracts as ct
from ... import managed_objs, sockets
from .. import base, events
@ -123,7 +123,7 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
managed_objs['mesh'].bl_object(location=input_sockets['Center']),
'NODES',
{
'node_group': import_geonodes(GeoNodes.PrimitiveBox, 'link'),
'node_group': import_geonodes(GeoNodes.MonitorEHField),
'unit_system': unit_systems['BlenderUnits'],
'inputs': {
'Size': input_sockets['Size'],

View File

@ -3,10 +3,10 @@ import typing as typ
import sympy as sp
import tidy3d as td
from blender_maxwell.assets.geonodes import GeoNodes, import_geonodes
from blender_maxwell.utils import extra_sympy_units as spux
from blender_maxwell.utils import logger
from .....assets.import_geonodes import GeoNodes, import_geonodes
from ... import contracts as ct
from ... import managed_objs, sockets
from .. import base, events
@ -124,7 +124,7 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
managed_objs['mesh'].bl_object(location=input_sockets['Center']),
'NODES',
{
'node_group': import_geonodes(GeoNodes.PrimitiveBox, 'link'),
'node_group': import_geonodes(GeoNodes.MonitorPowerFlux),
'unit_system': unit_systems['BlenderUnits'],
'inputs': {
'Size': input_sockets['Size'],

View File

@ -3,7 +3,8 @@ import typing as typ
import sympy as sp
import sympy.physics.units as spu
from .....assets.import_geonodes import GeoNodes, import_geonodes
from blender_maxwell.assets.geonodes import GeoNodes, import_geonodes
from ... import contracts as ct
from ... import managed_objs, sockets
from .. import base, events
@ -79,7 +80,7 @@ class SimDomainNode(base.MaxwellSimNode):
managed_objs['mesh'].bl_object(location=input_sockets['Center']),
'NODES',
{
'node_group': import_geonodes(GeoNodes.PrimitiveBox, 'link'),
'node_group': import_geonodes(GeoNodes.SimulationSimDomain),
'unit_system': unit_systems['BlenderUnits'],
'inputs': {
'Size': input_sockets['Size'],

View File

@ -4,7 +4,8 @@ import sympy as sp
import sympy.physics.units as spu
import tidy3d as td
from ......assets.import_geonodes import GeoNodes, import_geonodes
from blender_maxwell.assets.geonodes import GeoNodes, import_geonodes
from .... import contracts as ct
from .... import managed_objs, sockets
from ... import base, events
@ -79,7 +80,7 @@ class BoxStructureNode(base.MaxwellSimNode):
managed_objs['mesh'].bl_object(location=input_sockets['Center']),
'NODES',
{
'node_group': import_geonodes(GeoNodes.PrimitiveBox, 'link'),
'node_group': import_geonodes(GeoNodes.StructurePrimitiveBox),
'unit_system': unit_systems['BlenderUnits'],
'inputs': {
'Size': input_sockets['Size'],

View File

@ -3,7 +3,8 @@ import typing as typ
import sympy.physics.units as spu
import tidy3d as td
from ......assets.import_geonodes import GeoNodes, import_geonodes
from blender_maxwell.assets.geonodes import GeoNodes, import_geonodes
from .... import contracts as ct
from .... import managed_objs, sockets
from ... import base, events
@ -18,11 +19,11 @@ class SphereStructureNode(base.MaxwellSimNode):
# - Sockets
####################
input_sockets: typ.ClassVar = {
'Medium': sockets.MaxwellMediumSocketDef(),
'Center': sockets.PhysicalPoint3DSocketDef(),
'Radius': sockets.PhysicalLengthSocketDef(
default_value=150 * spu.nm,
),
'Medium': sockets.MaxwellMediumSocketDef(),
}
output_sockets: typ.ClassVar = {
'Structure': sockets.MaxwellStructureSocketDef(),
@ -82,7 +83,7 @@ class SphereStructureNode(base.MaxwellSimNode):
managed_objs['mesh'].bl_object(location=input_sockets['Center']),
'NODES',
{
'node_group': import_geonodes(GeoNodes.PrimitiveSphere, 'link'),
'node_group': import_geonodes(GeoNodes.StructurePrimitiveSphere),
'unit_system': unit_systems['BlenderUnits'],
'inputs': {
'Radius': input_sockets['Radius'],

View File

@ -10,24 +10,16 @@ from .. import base
VAC_SPEED_OF_LIGHT = sc.constants.speed_of_light * spu.meter / spu.second
FIXED_WL = 500 * spu.nm
class MaxwellMediumBLSocket(base.MaxwellSimSocket):
socket_type = ct.SocketType.MaxwellMedium
bl_label = 'Maxwell Medium'
use_units = True
####################
# - Properties
####################
wl: bpy.props.FloatProperty(
name='WL',
description='WL to evaluate conductivity at',
default=500.0,
precision=4,
step=50,
update=(lambda self, context: self.on_prop_changed('wl', context)),
)
rel_permittivity: bpy.props.FloatVectorProperty(
name='Relative Permittivity',
description='Represents a simple, complex permittivity',
@ -40,28 +32,13 @@ class MaxwellMediumBLSocket(base.MaxwellSimSocket):
)
####################
# - Socket UI
####################
def draw_value(self, col: bpy.types.UILayout) -> None:
col.prop(self, 'wl', text='λ')
col.separator(factor=1.0)
split = col.split(factor=0.35, align=False)
col = split.column(align=True)
col.label(text='ϵ_r ()')
col = split.column(align=True)
col.prop(self, 'rel_permittivity', text='')
####################
# - Computation of Default Value
# - FlowKinds
####################
@property
def value(self) -> td.Medium:
freq = (
spu.convert_to(
VAC_SPEED_OF_LIGHT / (self.wl * self.unit),
VAC_SPEED_OF_LIGHT / FIXED_WL,
spu.hertz,
)
/ spu.hertz
@ -76,25 +53,21 @@ class MaxwellMediumBLSocket(base.MaxwellSimSocket):
def value(
self, value: tuple[spux.ConstrSympyExpr(allow_variables=False), complex]
) -> None:
_wl, rel_permittivity = value
rel_permittivity = value
wl = float(
spu.convert_to(
_wl,
self.unit,
)
/ self.unit
)
self.wl = wl
self.rel_permittivity = (rel_permittivity.real, rel_permittivity.imag)
def sync_unit_change(self):
"""Override unit change to only alter frequency unit."""
self.value = (
self.wl * self.prev_unit,
complex(*self.rel_permittivity),
)
self.prev_active_unit = self.active_unit
####################
# - UI
####################
def draw_value(self, col: bpy.types.UILayout) -> None:
split = col.split(factor=0.35, align=False)
col = split.column(align=True)
col.label(text='ϵ_r ()')
col = split.column(align=True)
col.prop(self, 'rel_permittivity', text='')
####################