enhance: Streamlined GN asset system
parent
4e1eb19a88
commit
0599eed2b2
|
@ -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
|
||||
|
|
|
@ -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)
BIN
src/blender_maxwell/assets/assets.blend (Stored with Git LFS)
Binary file not shown.
|
@ -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
|
|
@ -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 = []
|
|
@ -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,
|
||||
# }
|
||||
]
|
BIN
src/blender_maxwell/assets/internal/monitor/_monitor_eh_field.blend (Stored with Git LFS)
100644
BIN
src/blender_maxwell/assets/internal/monitor/_monitor_eh_field.blend (Stored with Git LFS)
100644
Binary file not shown.
BIN
src/blender_maxwell/assets/internal/monitor/_monitor_power_flux.blend (Stored with Git LFS)
100644
BIN
src/blender_maxwell/assets/internal/monitor/_monitor_power_flux.blend (Stored with Git LFS)
100644
Binary file not shown.
BIN
src/blender_maxwell/assets/internal/simulation/_simulation_sim_domain.blend (Stored with Git LFS)
100644
BIN
src/blender_maxwell/assets/internal/simulation/_simulation_sim_domain.blend (Stored with Git LFS)
100644
Binary file not shown.
BIN
src/blender_maxwell/assets/internal/source/_source_plane_wave.blend (Stored with Git LFS)
100644
BIN
src/blender_maxwell/assets/internal/source/_source_plane_wave.blend (Stored with Git LFS)
100644
Binary file not shown.
BIN
src/blender_maxwell/assets/internal/structure/_structure_primitive_box.blend (Stored with Git LFS)
100644
BIN
src/blender_maxwell/assets/internal/structure/_structure_primitive_box.blend (Stored with Git LFS)
100644
Binary file not shown.
BIN
src/blender_maxwell/assets/internal/structure/_structure_primitive_ring.blend (Stored with Git LFS)
100644
BIN
src/blender_maxwell/assets/internal/structure/_structure_primitive_ring.blend (Stored with Git LFS)
100644
Binary file not shown.
BIN
src/blender_maxwell/assets/internal/structure/_structure_primitive_sphere.blend (Stored with Git LFS)
100644
BIN
src/blender_maxwell/assets/internal/structure/_structure_primitive_sphere.blend (Stored with Git LFS)
100644
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
src/blender_maxwell/assets/structures/primitives/sphere.blend (Stored with Git LFS)
BIN
src/blender_maxwell/assets/structures/primitives/sphere.blend (Stored with Git LFS)
Binary file not shown.
|
@ -16,6 +16,8 @@ class OperatorType(enum.StrEnum):
|
|||
|
||||
ConnectViewerNode = enum.auto()
|
||||
|
||||
GeoNodesToStructureNode = enum.auto()
|
||||
|
||||
# Socket: Tidy3DCloudTask
|
||||
SocketCloudAuthenticate = enum.auto()
|
||||
SocketReloadCloudFolderList = enum.auto()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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='')
|
||||
|
||||
|
||||
####################
|
||||
|
|
Loading…
Reference in New Issue