diff --git a/doc/_quarto.yml b/doc/_quarto.yml index 3098996..b776d24 100644 --- a/doc/_quarto.yml +++ b/doc/_quarto.yml @@ -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 diff --git a/src/blender_maxwell/assets/__init__.py b/src/blender_maxwell/assets/__init__.py index fefb20c..f6a2943 100644 --- a/src/blender_maxwell/assets/__init__.py +++ b/src/blender_maxwell/assets/__init__.py @@ -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__ = [ diff --git a/src/blender_maxwell/assets/assets.blend b/src/blender_maxwell/assets/assets.blend index b293485..bdfeebf 100644 --- a/src/blender_maxwell/assets/assets.blend +++ b/src/blender_maxwell/assets/assets.blend @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:faec3c9b23a081a7829eca1bfc8feef6f1458d808bc6b275a9d35ba7d33d97a5 -size 737120 +oid sha256:f39523534effa2218d088206a317a86c8a32c3d0a8e64594f3d2644b4b695276 +size 733040 diff --git a/src/blender_maxwell/assets/blender_assets.cats.txt~ b/src/blender_maxwell/assets/blender_assets.cats.txt~ deleted file mode 100644 index efd41a1..0000000 --- a/src/blender_maxwell/assets/blender_assets.cats.txt~ +++ /dev/null @@ -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 diff --git a/src/blender_maxwell/assets/geonodes.py b/src/blender_maxwell/assets/geonodes.py new file mode 100644 index 0000000..47b2755 --- /dev/null +++ b/src/blender_maxwell/assets/geonodes.py @@ -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 = [] diff --git a/src/blender_maxwell/assets/import_geonodes.py b/src/blender_maxwell/assets/import_geonodes.py deleted file mode 100644 index e3b02db..0000000 --- a/src/blender_maxwell/assets/import_geonodes.py +++ /dev/null @@ -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, - # } -] diff --git a/src/blender_maxwell/assets/internal/monitor/_monitor_eh_field.blend b/src/blender_maxwell/assets/internal/monitor/_monitor_eh_field.blend new file mode 100644 index 0000000..fe5e702 --- /dev/null +++ b/src/blender_maxwell/assets/internal/monitor/_monitor_eh_field.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7459adc5923562dc295a5ee65b662d3bd26abb7e75c6612a48ee8db9d30fe68 +size 874743 diff --git a/src/blender_maxwell/assets/internal/monitor/_monitor_power_flux.blend b/src/blender_maxwell/assets/internal/monitor/_monitor_power_flux.blend new file mode 100644 index 0000000..bcd4320 --- /dev/null +++ b/src/blender_maxwell/assets/internal/monitor/_monitor_power_flux.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8198f28fb0ef9803ac98624cee3a449a251677f7e5caa4951d5322684367540e +size 865119 diff --git a/src/blender_maxwell/assets/internal/simulation/_simulation_sim_domain.blend b/src/blender_maxwell/assets/internal/simulation/_simulation_sim_domain.blend new file mode 100644 index 0000000..e202dba --- /dev/null +++ b/src/blender_maxwell/assets/internal/simulation/_simulation_sim_domain.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a989ca283df0b9be0dc0c3005d3be13335b0b54968d87209562ae2ac97993ff +size 805812 diff --git a/src/blender_maxwell/assets/internal/source/_source_plane_wave.blend b/src/blender_maxwell/assets/internal/source/_source_plane_wave.blend new file mode 100644 index 0000000..f354a99 --- /dev/null +++ b/src/blender_maxwell/assets/internal/source/_source_plane_wave.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e19965400c8d26c20cf5e2318162532fbdcc15297d41e4e6eee83cad1808d3ba +size 805477 diff --git a/src/blender_maxwell/assets/internal/structure/_structure_primitive_box.blend b/src/blender_maxwell/assets/internal/structure/_structure_primitive_box.blend new file mode 100644 index 0000000..25c9a63 --- /dev/null +++ b/src/blender_maxwell/assets/internal/structure/_structure_primitive_box.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:189773c90bec67c272cf9270e096c0b19ad41f57f829f616e44a388f35680d7a +size 855469 diff --git a/src/blender_maxwell/assets/internal/structure/_structure_primitive_ring.blend b/src/blender_maxwell/assets/internal/structure/_structure_primitive_ring.blend new file mode 100644 index 0000000..17b6db9 --- /dev/null +++ b/src/blender_maxwell/assets/internal/structure/_structure_primitive_ring.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:975894c7674df55f0bafffd1986180b36e79b1f5584ab8db10b93e01c3a0b235 +size 883891 diff --git a/src/blender_maxwell/assets/internal/structure/_structure_primitive_sphere.blend b/src/blender_maxwell/assets/internal/structure/_structure_primitive_sphere.blend new file mode 100644 index 0000000..b29aca9 --- /dev/null +++ b/src/blender_maxwell/assets/internal/structure/_structure_primitive_sphere.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee2fbe2560a62f9d1d5c331cd284bdd11d970c40fe923c770326b40adab4749e +size 853546 diff --git a/src/blender_maxwell/assets/structures/arrays/array_ring.blend b/src/blender_maxwell/assets/structures/arrays/array_ring.blend new file mode 100644 index 0000000..69d4699 --- /dev/null +++ b/src/blender_maxwell/assets/structures/arrays/array_ring.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:025e4210b0bf4700d106945b19b8a13a09c56d1872509ca47397ccbaa3a6ae13 +size 911025 diff --git a/src/blender_maxwell/assets/structures/primitives/box.blend11 b/src/blender_maxwell/assets/structures/primitives/box.blend11 deleted file mode 100644 index c8b67af..0000000 Binary files a/src/blender_maxwell/assets/structures/primitives/box.blend11 and /dev/null differ diff --git a/src/blender_maxwell/assets/structures/primitives/sphere.blend b/src/blender_maxwell/assets/structures/primitives/sphere.blend index 41de196..128688a 100644 --- a/src/blender_maxwell/assets/structures/primitives/sphere.blend +++ b/src/blender_maxwell/assets/structures/primitives/sphere.blend @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9621bca9d715216b78310d2154440e1c875c1e5239377e5e8e8b47fb7be4f61e +oid sha256:cff3151287c8e991aa49cd8717de1e94ccaec1b2d8adb5087ee5b31d1e513c7d size 853330 diff --git a/src/blender_maxwell/contracts/operator_types.py b/src/blender_maxwell/contracts/operator_types.py index c95c326..998b312 100644 --- a/src/blender_maxwell/contracts/operator_types.py +++ b/src/blender_maxwell/contracts/operator_types.py @@ -16,6 +16,8 @@ class OperatorType(enum.StrEnum): ConnectViewerNode = enum.auto() + GeoNodesToStructureNode = enum.auto() + # Socket: Tidy3DCloudTask SocketCloudAuthenticate = enum.auto() SocketReloadCloudFolderList = enum.auto() diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_types.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_types.py index 7429fcd..eafe451 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_types.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_types.py @@ -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() diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/eh_field_monitor.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/eh_field_monitor.py index c7bac93..8356744 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/eh_field_monitor.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/eh_field_monitor.py @@ -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'], diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/field_power_flux_monitor.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/field_power_flux_monitor.py index 2470048..79334c5 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/field_power_flux_monitor.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/field_power_flux_monitor.py @@ -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'], diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/sim_domain.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/sim_domain.py index ba7f199..c924a71 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/sim_domain.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/sim_domain.py @@ -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'], diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/box_structure.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/box_structure.py index e096972..c1e37be 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/box_structure.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/box_structure.py @@ -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'], diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/sphere_structure.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/sphere_structure.py index 75cfcc0..8363045 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/sphere_structure.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/sphere_structure.py @@ -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'], diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/medium.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/medium.py index 75c196c..ce619a1 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/medium.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/medium.py @@ -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='') ####################