Compare commits

..

No commits in common. "c2db40ca6d788e9286ba6aba4b78c16a3cedb38c" and "a4e764ba219eb496f7bf598ff2dd1cfa9cb7244f" have entirely different histories.

153 changed files with 2469 additions and 2967 deletions

33
TODO.md
View File

@ -33,7 +33,6 @@
## Outputs ## Outputs
[x] Viewer [x] Viewer
- [ ] **BIG ONE**: Remove image preview when disabling plots. - [ ] **BIG ONE**: Remove image preview when disabling plots.
- [ ] Declare Preview unit system on the viewer node.
- [ ] Either enforce singleton, or find a way to have several viewers at the same time. - [ ] Either enforce singleton, or find a way to have several viewers at the same time.
- [ ] A setting that live-previews just a value. - [ ] A setting that live-previews just a value.
- [ ] Pop-up multiline string print as alternative to console print. - [ ] Pop-up multiline string print as alternative to console print.
@ -175,24 +174,13 @@
[ ] Tests / Monkey (suzanne deserves to be simulated, she may need manifolding up though :)) [ ] Tests / Monkey (suzanne deserves to be simulated, she may need manifolding up though :))
[ ] Tests / Wood Pile [ ] Tests / Wood Pile
[ ] Structures / Primitives / Plane [ ] Primitives / Plane
[x] Structures / Primitives / Box [ ] Primitives / Box
[x] Structures / Primitives / Sphere [ ] Primitives / Sphere
[ ] Structures / Primitives / Cylinder [ ] Primitives / Cylinder
[x] Structures / Primitives / Ring [ ] Primitives / Ring
[ ] Structures / Primitives / Capsule [ ] Primitives / Capsule
[ ] Structures / Primitives / Cone [ ] Primitives / Cone
[ ] Structures / Arrays / Square
[ ] Structures / Arrays / Square-Hole
[ ] Structures / Arrays / Cyl
[ ] Structures / Arrays / Cyl-Hole
[x] Structures / Arrays / Box
[x] Structures / Arrays / Sphere
[ ] Structures / Arrays / Cylinder
[-] Structures / Arrays / Ring
[ ] Structures / Arrays / Capsule
[ ] Structures / Arrays / Cone
[ ] Array / Square Array **NOTE: Ring and cylinder** [ ] Array / Square Array **NOTE: Ring and cylinder**
[ ] Array / Hex Array **NOTE: Ring and cylinder** [ ] Array / Hex Array **NOTE: Ring and cylinder**
@ -349,15 +337,12 @@
# Architecture # Architecture
## CRITICAL ## CRITICAL
With these things in place, we're in tip top shape: With these things in place
[ ] Linkability / Appendability of library GeoNodes groups, including being able to semantically ask for a particular GeoNodes tree without 'magic strings' that are entirely end-user-file dependent, is completely critical. especially [ ] Linkability / Appendability of library GeoNodes groups, including being able to semantically ask for a particular GeoNodes tree without 'magic strings' that are entirely end-user-file dependent, is completely critical. especially
[ ] Simplify the boilerplate needed to add a particular 3D preview driven by the input sockets of a particular GeoNodes group. It's currently hard for all the wrong reasons, and greatly halts our velocity in developing useful 3D previews of any/everything.
[ ] Finalize Viewer node unit systems.
[ ] Introduce a simplified (maybe caching) method of translating sympy-enabled values, ex. 'Center', into values for external use (ex. in a Tidy3D object or in a Blender preview) based on
[ ] Abstract the actual unit system dict-like data structure out from the UnitSystem socket.
[ ] Ship the addon with libraries of GeoNodes groups (with NO dependency on the addon), which are linked (internal use) or appended (end-user-selected structures) when needed for previewing. [ ] Ship the addon with libraries of GeoNodes groups (with NO dependency on the addon), which are linked (internal use) or appended (end-user-selected structures) when needed for previewing.
- I don't know that library overrides are the correct approach when it comes to structures used by the end-user. It's extremely easy to make a change to a library structure (or have one made for us by a Blender update!) that completely wrecks all end-user simulations that use it, or override it. By appending, the structure becomes 'part of' the user's simulation, which also makes it quite a bit easier for the user to alter (even drastically) for their own needs. - I don't know that library overrides are the correct approach when it comes to structures used by the end-user. It's extremely easy to make a change to a library structure (or have one made for us by a Blender update!) that completely wrecks all end-user simulations that use it, or override it. By appending, the structure becomes 'part of' the user's simulation, which also makes it quite a bit easier for the user to alter (even drastically) for their own needs.
[ ] License header UI for MaxwellSimTrees, to clarify the AGPL-compatible potentially user-selected license that trees must be distributed under. [ ] License header UI for MaxwellSimTrees, to clarify the AGPL-compatible potentially user-selected license that trees must be distributed under.
[ ] Simplify the boilerplate needed to add a particular 3D preview driven by the input sockets of a particular GeoNodes group. It's currently hard for all the wrong reasons, and greatly halts our velocity in developing useful 3D previews of any/everything.
## Registration and Contracts ## Registration and Contracts
[x] Finish the contract code converting from Blender sockets to our sockets based on dimensionality and the property description. [x] Finish the contract code converting from Blender sockets to our sockets based on dimensionality and the property description.

View File

@ -14,6 +14,7 @@ dependencies = [
"networkx==3.2.*", "networkx==3.2.*",
"rich==12.5.*", "rich==12.5.*",
"rtree==1.2.*", "rtree==1.2.*",
# Pin Blender 4.1.0-Compatible Versions # Pin Blender 4.1.0-Compatible Versions
## The dependency resolver will report if anything is wonky. ## The dependency resolver will report if anything is wonky.
"urllib3==1.26.8", "urllib3==1.26.8",
@ -99,7 +100,6 @@ ignore = [
"Q001", # Conflicts w/Formatter "Q001", # Conflicts w/Formatter
"Q002", # Conflicts w/Formatter "Q002", # Conflicts w/Formatter
"Q003", # Conflicts w/Formatter "Q003", # Conflicts w/Formatter
"D206", # Conflicts w/Formatter
"B008", # FastAPI uses this for Depends(), Security(), etc. . "B008", # FastAPI uses this for Depends(), Security(), etc. .
"E701", # class foo(Parent): pass or if simple: return are perfectly elegant "E701", # class foo(Parent): pass or if simple: return are perfectly elegant
"ERA001", # 'Commented-out code' seems to be just about anything to ruff "ERA001", # 'Commented-out code' seems to be just about anything to ruff

View File

@ -46,10 +46,9 @@ BL_REGISTER__BEFORE_DEPS = [
def BL_REGISTER__AFTER_DEPS(path_deps: Path): def BL_REGISTER__AFTER_DEPS(path_deps: Path):
log.info('Loading After-Deps BL_REGISTER') log.info('Loading After-Deps BL_REGISTER')
with pydeps.importable_addon_deps(path_deps): with pydeps.importable_addon_deps(path_deps):
from . import assets, node_trees, operators from . import node_trees, operators
return [ return [
*operators.BL_REGISTER, *operators.BL_REGISTER,
*assets.BL_REGISTER,
*node_trees.BL_REGISTER, *node_trees.BL_REGISTER,
] ]
@ -63,10 +62,9 @@ BL_KEYMAP_ITEM_DEFS__BEFORE_DEPS = [
def BL_KEYMAP_ITEM_DEFS__AFTER_DEPS(path_deps: Path): def BL_KEYMAP_ITEM_DEFS__AFTER_DEPS(path_deps: Path):
log.info('Loading After-Deps BL_KEYMAP_ITEM_DEFS') log.info('Loading After-Deps BL_KEYMAP_ITEM_DEFS')
with pydeps.importable_addon_deps(path_deps): with pydeps.importable_addon_deps(path_deps):
from . import assets, operators from . import operators
return [ return [
*operators.BL_KEYMAP_ITEM_DEFS, *operators.BL_KEYMAP_ITEM_DEFS,
*assets.BL_KEYMAP_ITEM_DEFS,
] ]

View File

@ -1,14 +0,0 @@
from . import import_geonodes
BL_REGISTER = [
*import_geonodes.BL_REGISTER,
]
BL_KEYMAP_ITEM_DEFS = [
*import_geonodes.BL_KEYMAP_ITEM_DEFS,
]
__all__ = [
'BL_REGISTER',
'BL_KEYMAP_ITEM_DEFS',
]

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

Binary file not shown.

View File

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

View File

@ -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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,320 +0,0 @@
"""Provides for the linking and/or appending of geometry nodes trees from vendored libraries included in Blender maxwell."""
import enum
import typing as typ
from pathlib import Path
import bpy
import typing_extensions as typx
from .. import info
from ..utils import logger
log = logger.get(__name__)
BLOperatorStatus: typ.TypeAlias = set[
typx.Literal['RUNNING_MODAL', 'CANCELLED', 'FINISHED', 'PASS_THROUGH', 'INTERFACE']
]
####################
# - GeoNodes Specification
####################
class GeoNodes(enum.StrEnum):
"""Defines available GeoNodes groups vendored as part of Blender Maxwell.
The value of this StrEnum is both the name of the .blend file containing the GeoNodes group, and of the GeoNodes group itself.
"""
PrimitiveBox = 'box'
PrimitiveRing = 'ring'
PrimitiveSphere = 'sphere'
# GeoNodes Path Mapping
GN_PRIMITIVES_PATH = info.PATH_ASSETS / 'geonodes' / 'primitives'
GN_PARENT_PATHS: dict[GeoNodes, Path] = {
GeoNodes.PrimitiveBox: GN_PRIMITIVES_PATH,
GeoNodes.PrimitiveRing: GN_PRIMITIVES_PATH,
GeoNodes.PrimitiveSphere: GN_PRIMITIVES_PATH,
}
####################
# - Import GeoNodes (Link/Append)
####################
ImportMethod: typ.TypeAlias = typx.Literal['append', 'link']
def import_geonodes(
geonodes: GeoNodes,
import_method: ImportMethod,
force_import: bool = False,
) -> bpy.types.GeometryNodeGroup:
"""Given a pre-defined GeoNodes group packaged with Blender Maxwell.
The procedure is as follows:
- Link it to the current .blend file.
- Retrieve the node group and return it.
"""
if geonodes in bpy.data.node_groups and not force_import:
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 = 'blender_maxwell.panel__node_asset_panel'
bl_label = 'Node GeoNodes Asset Panel'
bl_space_type = 'NODE_EDITOR'
bl_region_type = 'UI'
bl_category = 'Assets'
# @classmethod
# def poll(cls, context):
# return (
# (space := context.get('space_data')) is not None
# and (node_tree := space.get('node_tree')) is not None
# and (node_tree.bl_idname == 'MaxwellSimTreeType')
# )
def draw(self, context):
layout = self.layout
workspace = context.workspace
wm = context.window_manager
# list_id must be unique otherwise behaviour gets weird when the template_asset_view is shown twice
# (drag operator stops working in AssetPanelDrag, clickable area of all Assets in AssetPanelNoDrag gets
# reduced to below the Asset name and clickable area of Current File Assets in AssetPanelDrag gets
# reduced as if it didn't have a drag operator)
_activate_op_props, _drag_op_props = layout.template_asset_view(
'geo_nodes_asset_shelf',
workspace,
'asset_library_reference',
wm,
'active_asset_list',
wm,
'active_asset_index',
drag_operator=AppendGeoNodes.bl_idname,
)
####################
# - Append GeoNodes Operator
####################
def get_view_location(region, coords, ui_scale):
x, y = region.view2d.region_to_view(*coords)
return x / ui_scale, y / ui_scale
class AppendGeoNodes(bpy.types.Operator):
"""Operator allowing the user to append a vendored GeoNodes tree for use in a simulation."""
bl_idname = 'blender_maxwell.blends__import_geo_nodes'
bl_label = 'Import GeoNode Tree'
bl_description = 'Append a geometry node tree from the Blender Maxwell plugin, either via linking or appending'
bl_options = frozenset({'REGISTER'})
####################
# - Properties
####################
_asset: bpy.types.AssetRepresentation | None = None
_start_drag_x: bpy.props.IntProperty()
_start_drag_y: bpy.props.IntProperty()
####################
# - UI
####################
def draw(self, _: bpy.types.Context) -> None:
"""Draws the UI of the operator."""
layout = self.layout
col = layout.column()
col.prop(self, 'geonodes_to_append', expand=True)
####################
# - Execution
####################
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
"""Defines when the operator can be run.
Returns:
Whether the operator can be run.
"""
return context.asset is not None
def invoke(self, context, event):
self._start_drag_x = event.mouse_x
self._start_drag_y = event.mouse_y
return self.execute(context)
def execute(self, context: bpy.types.Context) -> BLOperatorStatus:
"""Initializes the while-dragging modal handler, which executes custom logic when the mouse button is released.
Runs in response to drag_handler of a `UILayout.template_asset_view`.
"""
asset: bpy.types.AssetRepresentation = context.asset
log.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
) -> 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(info.PATH_ASSETS):
bpy.ops.preferences.asset_library_remove(asset_library_idx)
if 'Blender Maxwell' not in asset_libraries:
bpy.ops.preferences.asset_library_add()
asset_library = asset_libraries[-1] ## Since the operator adds to the end
asset_library.name = 'Blender Maxwell'
asset_library.path = str(info.PATH_ASSETS)
bpy.types.WindowManager.active_asset_list = bpy.props.CollectionProperty(
type=bpy.types.AssetHandle
)
bpy.types.WindowManager.active_asset_index = bpy.props.IntProperty()
## TODO: Do something differently
BL_REGISTER = [
# GeoNodesAssetShelf,
NodeAssetPanel,
AppendGeoNodes,
]
BL_KEYMAP_ITEM_DEFS = [
# {
# '_': [
# AppendGeoNodes.bl_idname,
# 'LEFTMOUSE',
# 'CLICK_DRAG',
# ],
# 'ctrl': False,
# 'shift': False,
# 'alt': False,
# }
]

View File

@ -3,54 +3,38 @@ from pathlib import Path
import bpy import bpy
PATH_ADDON_ROOT = Path(__file__).resolve().parent
#################### ####################
# - Addon Info # - Addon Info
#################### ####################
PATH_ADDON_ROOT = Path(__file__).resolve().parent
# Addon Information
## bl_info is filled with PROJ_SPEC when packing the .zip.
with (PATH_ADDON_ROOT / 'pyproject.toml').open('rb') as f: with (PATH_ADDON_ROOT / 'pyproject.toml').open('rb') as f:
PROJ_SPEC = tomllib.load(f) PROJ_SPEC = tomllib.load(f)
## bl_info is filled with PROJ_SPEC when packing the .zip.
ADDON_NAME = PROJ_SPEC['project']['name'] ADDON_NAME = PROJ_SPEC['project']['name']
ADDON_VERSION = PROJ_SPEC['project']['version'] ADDON_VERSION = PROJ_SPEC['project']['version']
#################### # PyDeps Path Info
# - Asset Info
####################
PATH_ASSETS = PATH_ADDON_ROOT / 'assets'
####################
# - PyDeps Info
####################
PATH_REQS = PATH_ADDON_ROOT / 'requirements.lock'
DEFAULT_PATH_DEPS = PATH_ADDON_ROOT / '.addon_dependencies'
## requirements.lock is written when packing the .zip. ## requirements.lock is written when packing the .zip.
## By default, the addon pydeps are kept in the addon dir. ## By default, the addon pydeps are kept in the addon dir.
PATH_REQS = PATH_ADDON_ROOT / 'requirements.lock'
DEFAULT_PATH_DEPS = PATH_ADDON_ROOT / '.addon_dependencies'
#################### # Logging Info
# - Local Addon Cache
####################
ADDON_CACHE = PATH_ADDON_ROOT / '.addon_cache'
ADDON_CACHE.mkdir(exist_ok=True)
## TODO: Addon preferences?
####################
# - Logging Info
####################
DEFAULT_LOG_PATH = PATH_ADDON_ROOT / 'addon.log'
DEFAULT_LOG_PATH.touch(exist_ok=True)
## By default, the addon file log writes to the addon dir. ## By default, the addon file log writes to the addon dir.
## The initial .log_level contents are written when packing the .zip. ## The initial .log_level contents are written when packing the .zip.
## Subsequent changes are managed by nodeps.utils.simple_logger.py. ## Subsequent changes are managed by nodeps.utils.simple_logger.py.
DEFAULT_LOG_PATH = PATH_ADDON_ROOT / 'addon.log'
DEFAULT_LOG_PATH.touch(exist_ok=True)
PATH_BOOTSTRAP_LOG_LEVEL = PATH_ADDON_ROOT / '.bootstrap_log_level' PATH_BOOTSTRAP_LOG_LEVEL = PATH_ADDON_ROOT / '.bootstrap_log_level'
with PATH_BOOTSTRAP_LOG_LEVEL.open('r') as f: with PATH_BOOTSTRAP_LOG_LEVEL.open('r') as f:
BOOTSTRAP_LOG_LEVEL = int(f.read().strip()) BOOTSTRAP_LOG_LEVEL = int(f.read().strip())
#################### ####################
# - Addon Prefs Info # - Addon Getters
#################### ####################
def addon_prefs() -> bpy.types.AddonPreferences | None: def addon_prefs() -> bpy.types.AddonPreferences | None:
if (addon := bpy.context.preferences.addons.get(ADDON_NAME)) is None: if (addon := bpy.context.preferences.addons.get(ADDON_NAME)) is None:

View File

@ -5,7 +5,10 @@ sp.printing.str.StrPrinter._default_settings['abbrev'] = True
## By configuring this in __init__.py, we guarantee it for all subimports. ## By configuring this in __init__.py, we guarantee it for all subimports.
## (Unless, elsewhere, this setting is changed. Be careful!) ## (Unless, elsewhere, this setting is changed. Be careful!)
from . import categories, node_tree, nodes, sockets from . import sockets
from . import node_tree
from . import nodes
from . import categories
BL_REGISTER = [ BL_REGISTER = [
*sockets.BL_REGISTER, *sockets.BL_REGISTER,

View File

@ -1,29 +1,44 @@
"""Tools for translating between BLMaxwell sockets and pure Blender sockets.
Attributes:
BL_SOCKET_3D_TYPE_PREFIXES: Blender socket prefixes which indicate that the Blender socket has three values.
BL_SOCKET_4D_TYPE_PREFIXES: Blender socket prefixes which indicate that the Blender socket has four values.
"""
import functools
import typing as typ import typing as typ
import bpy import bpy
import sympy as sp import sympy as sp
import sympy.physics.units as spu
import typing_extensions as typx
from ...utils import extra_sympy_units as spux
from ...utils import logger as _logger from ...utils import logger as _logger
from . import contracts as ct from . import contracts as ct
from . import sockets from . import sockets as sck
from .contracts import SocketType as ST
log = _logger.get(__name__) log = _logger.get(__name__)
BLSocketType: typ.TypeAlias = str ## A Blender-Defined Socket Type # TODO: Caching?
BLSocketValue: typ.TypeAlias = typ.Any ## A Blender Socket Value # TODO: Move the manual labor stuff to contracts
BLSocketSize: typ.TypeAlias = int
DescType: typ.TypeAlias = str BLSocketType = str ## A Blender-Defined Socket Type
Unit: typ.TypeAlias = typ.Any ## Type of a valid unit BLSocketSize = int
## TODO: Move this kind of thing to contracts DescType = str
Unit = typ.Any ## Type of a valid unit
####################
# - Socket to SocketDef
####################
SOCKET_DEFS = {
socket_type: getattr(
sck,
socket_type.value.removesuffix('SocketType') + 'SocketDef',
)
for socket_type in ST
if hasattr(sck, socket_type.value.removesuffix('SocketType') + 'SocketDef')
}
## TODO: Bit of a hack. Is it robust enough?
for socket_type in ST:
if not hasattr(
sck,
socket_type.value.removesuffix('SocketType') + 'SocketDef',
):
log.warning('Missing SocketDef for %s', socket_type.value)
#################### ####################
@ -38,11 +53,9 @@ BL_SOCKET_4D_TYPE_PREFIXES = {
} }
@functools.lru_cache(maxsize=4096) def size_from_bl_interface_socket(
def _size_from_bl_socket( bl_interface_socket: bpy.types.NodeTreeInterfaceSocket,
description: str, ) -> typx.Literal[1, 2, 3, 4]:
bl_socket_type: BLSocketType,
):
"""Parses the `size`, aka. number of elements, contained within the `default_value` of a Blender interface socket. """Parses the `size`, aka. number of elements, contained within the `default_value` of a Blender interface socket.
Since there are no 2D sockets in Blender, the user can specify "2D" in the Blender socket's description to "promise" that only the first two values will be used. Since there are no 2D sockets in Blender, the user can specify "2D" in the Blender socket's description to "promise" that only the first two values will be used.
@ -52,15 +65,15 @@ def _size_from_bl_socket(
- For 3D sockets, a hard-coded list of Blender node socket types is used. - For 3D sockets, a hard-coded list of Blender node socket types is used.
- Else, it is a 1D socket type. - Else, it is a 1D socket type.
""" """
if description.startswith('2D'): if bl_interface_socket.description.startswith('2D'):
return 2 return 2
if any( if any(
bl_socket_type.startswith(bl_socket_3d_type_prefix) bl_interface_socket.socket_type.startswith(bl_socket_3d_type_prefix)
for bl_socket_3d_type_prefix in BL_SOCKET_3D_TYPE_PREFIXES for bl_socket_3d_type_prefix in BL_SOCKET_3D_TYPE_PREFIXES
): ):
return 3 return 3
if any( if any(
bl_socket_type.startswith(bl_socket_4d_type_prefix) bl_interface_socket.socket_type.startswith(bl_socket_4d_type_prefix)
for bl_socket_4d_type_prefix in BL_SOCKET_4D_TYPE_PREFIXES for bl_socket_4d_type_prefix in BL_SOCKET_4D_TYPE_PREFIXES
): ):
return 4 return 4
@ -71,185 +84,176 @@ def _size_from_bl_socket(
#################### ####################
# - BL Socket Type / Unit Parser # - BL Socket Type / Unit Parser
#################### ####################
@functools.lru_cache(maxsize=4096) def parse_bl_interface_socket(
def _socket_type_from_bl_socket( bl_interface_socket: bpy.types.NodeTreeInterfaceSocket,
description: str, ) -> tuple[ST, sp.Expr | None]:
bl_socket_type: BLSocketType, """Parse a Blender interface socket by parsing its description, falling back to any direct type links.
) -> ct.SocketType:
"""Parse a Blender socket for a matching BLMaxwell socket type, relying on both the Blender socket type and user-generated hints in the description.
Arguments: Arguments:
description: The description from Blender socket, aka. `bl_socket.description`. bl_interface_socket: An interface socket associated with the global input to a node tree.
bl_socket_type: The Blender socket type, aka. `bl_socket.socket_type`.
Returns: Returns:
The type of a MaxwellSimSocket that corresponds to the Blender socket. The type of a corresponding MaxwellSimSocket, as well as a unit (if a particular unit was requested by the Blender interface socket).
""" """
size = _size_from_bl_socket(description, bl_socket_type) size = size_from_bl_interface_socket(bl_interface_socket)
# Determine Socket Type Directly # Determine Direct Socket Type
## The naive mapping from BL socket -> Maxwell socket may be good enough.
if ( if (
direct_socket_type := ct.BL_SOCKET_DIRECT_TYPE_MAP.get((bl_socket_type, size)) direct_socket_type := ct.BL_SOCKET_DIRECT_TYPE_MAP.get(
(bl_interface_socket.socket_type, size)
)
) is None: ) is None:
msg = "Blender interface socket has no mapping among 'MaxwellSimSocket's." msg = "Blender interface socket has no mapping among 'MaxwellSimSocket's."
raise ValueError(msg) raise ValueError(msg)
# (No Description) Return Direct Socket Type # (Maybe) Return Direct Socket Type
if ct.BL_SOCKET_DESCR_ANNOT_STRING not in description: ## When there's no description, that's it; return.
return direct_socket_type if not ct.BL_SOCKET_DESCR_ANNOT_STRING in bl_interface_socket.description:
return (direct_socket_type, None)
# Parse Description for Socket Type # Parse Description for Socket Type
## The "2D" token is special; don't include it if it's there. tokens = (
descr_params = description.split(ct.BL_SOCKET_DESCR_ANNOT_STRING)[0] _tokens
directive = ( if (_tokens := bl_interface_socket.description.split(' '))[0] != '2D'
_tokens[0] if (_tokens := descr_params.split(' '))[0] != '2D' else _tokens[1] else _tokens[1:]
) ) ## Don't include the "2D" token, if defined.
if directive == 'Preview':
return direct_socket_type ## TODO: Preview element handling
if ( if (
socket_type := ct.BL_SOCKET_DESCR_TYPE_MAP.get( socket_type := ct.BL_SOCKET_DESCR_TYPE_MAP.get(
(directive, bl_socket_type, size) (tokens[0], bl_interface_socket.socket_type, size)
) )
) is None: ) is None:
msg = f'Socket description "{(directive, bl_socket_type, size)}" doesn\'t map to a socket type + unit' return (
raise ValueError(msg) direct_socket_type,
None,
) ## Description doesn't map to anything
return socket_type # Determine Socket Unit (to use instead of "unit system")
## This is entirely OPTIONAL
socket_unit = None
if socket_type in ct.SOCKET_UNITS:
## Case: Unit is User-Defined
if len(tokens) > 1 and '(' in tokens[1] and ')' in tokens[1]:
# Compute (<unit_str>) as Unit Token
unit_token = tokens[1].removeprefix('(').removesuffix(')')
# Compare Unit Token to Valid Sympy-Printed Units
socket_unit = (
_socket_unit
if (
_socket_unit := [
unit
for unit in ct.SOCKET_UNITS[socket_type][
'values'
].values()
if str(unit) == unit_token
]
)
else ct.SOCKET_UNITS[socket_type]['values'][
ct.SOCKET_UNITS[socket_type]['default']
]
)
## TODO: Enforce abbreviated sympy printing here, not globally
return (socket_type, socket_unit)
#################### ####################
# - BL Socket Interface Definition # - BL Socket Interface Definition
#################### ####################
@functools.lru_cache(maxsize=4096) def socket_def_from_bl_interface_socket(
def _socket_def_from_bl_socket(
description: str,
bl_socket_type: BLSocketType,
) -> ct.SocketType:
return sockets.SOCKET_DEFS[_socket_type_from_bl_socket(description, bl_socket_type)]
def socket_def_from_bl_socket(
bl_interface_socket: bpy.types.NodeTreeInterfaceSocket, bl_interface_socket: bpy.types.NodeTreeInterfaceSocket,
) -> ct.schemas.SocketDef: ):
"""Computes an appropriate (no-arg) SocketDef from the given `bl_interface_socket`, by parsing it.""" """Computes an appropriate (no-arg) SocketDef from the given `bl_interface_socket`, by parsing it."""
return _socket_def_from_bl_socket( return SOCKET_DEFS[parse_bl_interface_socket(bl_interface_socket)[0]]
bl_interface_socket.description, bl_interface_socket.bl_socket_idname
)
#################### ####################
# - Extract Default Interface Socket Value # - Extract Default Interface Socket Value
#################### ####################
def _read_bl_socket_default_value( def value_from_bl(
description: str,
bl_socket_type: BLSocketType,
bl_socket_value: BLSocketValue,
unit_system: dict | None = None,
allow_unit_not_in_unit_system: bool = False,
) -> typ.Any:
# Parse the BL Socket Type and Value
## The 'lambda' delays construction until size is determined.
socket_type = _socket_type_from_bl_socket(description, bl_socket_type)
parsed_socket_value = {
1: lambda: bl_socket_value,
2: lambda: sp.Matrix(tuple(bl_socket_value)[:2]),
3: lambda: sp.Matrix(tuple(bl_socket_value)),
4: lambda: sp.Matrix(tuple(bl_socket_value)),
}[_size_from_bl_socket(description, bl_socket_type)]()
# Add Unit-System Unit to Parsed
## Use the matching socket type to lookup the unit in the unit system.
if unit_system is not None:
if (unit := unit_system.get(socket_type)) is None:
if allow_unit_not_in_unit_system:
return parsed_socket_value
msg = f'Unit system does not provide a unit for {socket_type}'
raise RuntimeError(msg)
return parsed_socket_value * unit
return parsed_socket_value
def read_bl_socket_default_value(
bl_interface_socket: bpy.types.NodeTreeInterfaceSocket, bl_interface_socket: bpy.types.NodeTreeInterfaceSocket,
unit_system: dict | None = None, unit_system: dict | None = None,
allow_unit_not_in_unit_system: bool = False,
) -> typ.Any: ) -> typ.Any:
"""Reads the `default_value` of a Blender socket, guaranteeing a well-formed value consistent with the passed unit system. """Reads the value of any Blender socket, and writes its `default_value` to the `value` of any `MaxwellSimSocket`.
- If the size of the Blender socket is >1, then `value` is written to as a `sympy.Matrix`.
Arguments: - If a unit system is given, then the Blender socket is matched to a `MaxwellSimSocket`, which is used to lookup an appropriate unit in the given `unit_system`.
bl_interface_socket: The Blender interface socket to analyze for description, socket type, and default value.
unit_system: The mapping from BLMaxwell SocketType to corresponding unit, used to apply the appropriate unit to the output.
Returns:
The parsed, well-formed version of `bl_socket.default_value`, of the appropriate form and unit.
""" """
return _read_bl_socket_default_value( ## TODO: Consider sympy.S()'ing the default_value
bl_interface_socket.description, parsed_bl_socket_value = {
bl_interface_socket.bl_socket_idname, 1: lambda: bl_interface_socket.default_value,
bl_interface_socket.default_value, 2: lambda: sp.Matrix(tuple(bl_interface_socket.default_value)[:2]),
unit_system=unit_system, 3: lambda: sp.Matrix(tuple(bl_interface_socket.default_value)),
allow_unit_not_in_unit_system=allow_unit_not_in_unit_system, 4: lambda: sp.Matrix(tuple(bl_interface_socket.default_value)),
) }[size_from_bl_interface_socket(bl_interface_socket)]()
## The 'lambda' delays construction until size is determined
socket_type, unit = parse_bl_interface_socket(bl_interface_socket)
# Add Unit to Parsed (if relevant)
if unit is not None:
parsed_bl_socket_value *= unit
elif unit_system is not None:
parsed_bl_socket_value *= unit_system[socket_type]
return parsed_bl_socket_value
def _writable_bl_socket_value( ####################
description: str, # - Convert to Blender-Compatible Value
bl_socket_type: BLSocketType, ####################
value: typ.Any, def make_scalar_bl_compat(scalar: typ.Any) -> typ.Any:
unit_system: dict | None = None, """Blender doesn't accept ex. Sympy numbers as values.
allow_unit_not_in_unit_system: bool = False, Therefore, we need to do some conforming.
) -> typ.Any:
socket_type = _socket_type_from_bl_socket(description, bl_socket_type)
# Retrieve Unit-System Unit Currently hard-coded; this is probably best.
if unit_system is not None:
if (unit := unit_system.get(socket_type)) is None:
if allow_unit_not_in_unit_system:
_bl_socket_value = value
else:
msg = f'Unit system does not provide a unit for {socket_type}'
raise RuntimeError(msg)
else:
_bl_socket_value = spux.scale_to_unit(value, unit)
else:
_bl_socket_value = value
# Compute Blender Socket Value
if isinstance(_bl_socket_value, sp.Basic):
bl_socket_value = spux.sympy_to_python(_bl_socket_value)
else:
bl_socket_value = _bl_socket_value
if _size_from_bl_socket(description, bl_socket_type) == 2: # noqa: PLR2004
bl_socket_value = bl_socket_value[:2]
return bl_socket_value
def writable_bl_socket_value(
bl_interface_socket: bpy.types.NodeTreeInterfaceSocket,
value: typ.Any,
unit_system: dict | None = None,
allow_unit_not_in_unit_system: bool = False,
) -> typ.Any:
"""Processes a value to be ready-to-write to a Blender socket.
Arguments:
bl_interface_socket: The Blender interface socket to analyze
value: The value to prepare for writing to the given Blender socket.
unit_system: The mapping from BLMaxwell SocketType to corresponding unit, used to scale the value to the the appropriate unit.
Returns:
A value corresponding to the input, which is guaranteed to be compatible with the Blender socket (incl. via a GeoNodes modifier), as well as correctly scaled with respect to the given unit system.
""" """
return _writable_bl_socket_value( if isinstance(scalar, sp.Integer):
bl_interface_socket.description, return int(scalar)
bl_interface_socket.bl_socket_idname, elif isinstance(scalar, sp.Float):
value, return float(scalar)
unit_system=unit_system, elif isinstance(scalar, sp.Rational):
allow_unit_not_in_unit_system=allow_unit_not_in_unit_system, return float(scalar)
elif isinstance(scalar, sp.Expr):
return float(scalar.n())
## TODO: More?
return scalar
def value_to_bl(
bl_interface_socket: bpy.types.NodeSocket,
value: typ.Any,
unit_system: dict | None = None,
) -> typ.Any:
socket_type, unit = parse_bl_interface_socket(bl_interface_socket)
# Set Socket
if unit is not None:
bl_socket_value = spu.convert_to(value, unit) / unit
elif unit_system is not None and socket_type in unit_system:
bl_socket_value = (
spu.convert_to(value, unit_system[socket_type])
/ unit_system[socket_type]
) )
else:
bl_socket_value = value
return {
1: lambda: make_scalar_bl_compat(bl_socket_value),
2: lambda: tuple(
[
make_scalar_bl_compat(bl_socket_value[0]),
make_scalar_bl_compat(bl_socket_value[1]),
bl_interface_socket.default_value[2],
## Don't touch (unused) 3rd bl_socket coordinate
]
),
3: lambda: tuple(
[make_scalar_bl_compat(el) for el in bl_socket_value]
),
4: lambda: tuple(
[make_scalar_bl_compat(el) for el in bl_socket_value]
),
}[size_from_bl_interface_socket(bl_interface_socket)]()
## The 'lambda' delays construction until size is determined

View File

@ -2,7 +2,6 @@
import bpy import bpy
import nodeitems_utils import nodeitems_utils
from . import contracts as ct from . import contracts as ct
from .nodes import BL_NODES from .nodes import BL_NODES
@ -82,7 +81,9 @@ BL_NODE_CATEGORIES = mk_node_categories(
syllable_prefix=['MAXWELLSIM'], syllable_prefix=['MAXWELLSIM'],
) )
## TODO: refactor, this has a big code smell ## TODO: refactor, this has a big code smell
BL_REGISTER = [*DYNAMIC_SUBMENU_REGISTRATIONS] ## Must be run after, right now. BL_REGISTER = [
*DYNAMIC_SUBMENU_REGISTRATIONS
] ## Must be run after, right now.
## TEST - TODO this is a big code smell ## TEST - TODO this is a big code smell

View File

@ -30,8 +30,6 @@ from .socket_units import SOCKET_UNITS
from .socket_colors import SOCKET_COLORS from .socket_colors import SOCKET_COLORS
from .socket_shapes import SOCKET_SHAPES from .socket_shapes import SOCKET_SHAPES
from .unit_systems import UNITS_BLENDER, UNITS_TIDY3D
from .socket_from_bl_desc import BL_SOCKET_DESCR_TYPE_MAP from .socket_from_bl_desc import BL_SOCKET_DESCR_TYPE_MAP
from .socket_from_bl_direct import BL_SOCKET_DIRECT_TYPE_MAP from .socket_from_bl_direct import BL_SOCKET_DIRECT_TYPE_MAP
@ -75,8 +73,6 @@ __all__ = [
'SOCKET_UNITS', 'SOCKET_UNITS',
'SOCKET_COLORS', 'SOCKET_COLORS',
'SOCKET_SHAPES', 'SOCKET_SHAPES',
'UNITS_BLENDER',
'UNITS_TIDY3D',
'BL_SOCKET_DESCR_TYPE_MAP', 'BL_SOCKET_DESCR_TYPE_MAP',
'BL_SOCKET_DIRECT_TYPE_MAP', 'BL_SOCKET_DIRECT_TYPE_MAP',
'BL_SOCKET_DESCR_ANNOT_STRING', 'BL_SOCKET_DESCR_ANNOT_STRING',

View File

@ -4,10 +4,5 @@ from ....utils.blender_type_enum import BlenderTypeEnum
class ManagedObjType(BlenderTypeEnum): class ManagedObjType(BlenderTypeEnum):
ManagedBLObject = enum.auto()
ManagedBLImage = enum.auto() ManagedBLImage = enum.auto()
ManagedBLCollection = enum.auto()
ManagedBLEmpty = enum.auto()
ManagedBLMesh = enum.auto()
ManagedBLVolume = enum.auto()
ManagedBLModifier = enum.auto()

View File

@ -1,3 +1,6 @@
import sympy.physics.units as spu
from ....utils import extra_sympy_units as spuex
from .socket_types import SocketType as ST from .socket_types import SocketType as ST
## TODO: Don't just presume sRGB. ## TODO: Don't just presume sRGB.
@ -39,7 +42,6 @@ SOCKET_COLORS = {
ST.PhysicalPol: (0.5, 0.4, 0.2, 1.0), # Dark Orange ST.PhysicalPol: (0.5, 0.4, 0.2, 1.0), # Dark Orange
ST.PhysicalFreq: (1.0, 0.7, 0.5, 1.0), # Light Peach ST.PhysicalFreq: (1.0, 0.7, 0.5, 1.0), # Light Peach
# Blender # Blender
ST.BlenderMaterial: (0.8, 0.6, 1.0, 1.0), # Lighter Purple
ST.BlenderObject: (0.7, 0.5, 1.0, 1.0), # Light Purple ST.BlenderObject: (0.7, 0.5, 1.0, 1.0), # Light Purple
ST.BlenderCollection: (0.6, 0.45, 0.9, 1.0), # Medium Light Purple ST.BlenderCollection: (0.6, 0.45, 0.9, 1.0), # Medium Light Purple
ST.BlenderImage: (0.5, 0.4, 0.8, 1.0), # Medium Purple ST.BlenderImage: (0.5, 0.4, 0.8, 1.0), # Medium Purple

View File

@ -1,15 +1,11 @@
from .socket_types import SocketType as ST from .socket_types import SocketType as ST
BL_SOCKET_DIRECT_TYPE_MAP = { BL_SOCKET_DIRECT_TYPE_MAP = {
# Blender ('NodeSocketString', 1): ST.String,
('NodeSocketBool', 1): ST.Bool,
('NodeSocketCollection', 1): ST.BlenderCollection, ('NodeSocketCollection', 1): ST.BlenderCollection,
('NodeSocketImage', 1): ST.BlenderImage, ('NodeSocketImage', 1): ST.BlenderImage,
('NodeSocketObject', 1): ST.BlenderObject, ('NodeSocketObject', 1): ST.BlenderObject,
('NodeSocketMaterial', 1): ST.BlenderMaterial,
# Basic
('NodeSocketString', 1): ST.String,
('NodeSocketBool', 1): ST.Bool,
# Float
('NodeSocketFloat', 1): ST.RealNumber, ('NodeSocketFloat', 1): ST.RealNumber,
# ("NodeSocketFloatAngle", 1): ST.PhysicalAngle, # ("NodeSocketFloatAngle", 1): ST.PhysicalAngle,
# ("NodeSocketFloatDistance", 1): ST.PhysicalLength, # ("NodeSocketFloatDistance", 1): ST.PhysicalLength,
@ -17,14 +13,12 @@ BL_SOCKET_DIRECT_TYPE_MAP = {
('NodeSocketFloatPercentage', 1): ST.RealNumber, ('NodeSocketFloatPercentage', 1): ST.RealNumber,
# ("NodeSocketFloatTime", 1): ST.PhysicalTime, # ("NodeSocketFloatTime", 1): ST.PhysicalTime,
# ("NodeSocketFloatTimeAbsolute", 1): ST.PhysicalTime, # ("NodeSocketFloatTimeAbsolute", 1): ST.PhysicalTime,
# Int
('NodeSocketInt', 1): ST.IntegerNumber, ('NodeSocketInt', 1): ST.IntegerNumber,
('NodeSocketIntFactor', 1): ST.IntegerNumber, ('NodeSocketIntFactor', 1): ST.IntegerNumber,
('NodeSocketIntPercentage', 1): ST.IntegerNumber, ('NodeSocketIntPercentage', 1): ST.IntegerNumber,
('NodeSocketIntUnsigned', 1): ST.IntegerNumber, ('NodeSocketIntUnsigned', 1): ST.IntegerNumber,
# Array-Like
('NodeSocketColor', 3): ST.Color,
('NodeSocketRotation', 2): ST.PhysicalRot2D, ('NodeSocketRotation', 2): ST.PhysicalRot2D,
('NodeSocketColor', 3): ST.Color,
('NodeSocketVector', 2): ST.Real2DVector, ('NodeSocketVector', 2): ST.Real2DVector,
('NodeSocketVector', 3): ST.Real3DVector, ('NodeSocketVector', 3): ST.Real3DVector,
# ("NodeSocketVectorAcceleration", 2): ST.PhysicalAccel2D, # ("NodeSocketVectorAcceleration", 2): ST.PhysicalAccel2D,

View File

@ -38,7 +38,6 @@ SOCKET_SHAPES = {
ST.PhysicalPol: 'CIRCLE', ST.PhysicalPol: 'CIRCLE',
ST.PhysicalFreq: 'CIRCLE', ST.PhysicalFreq: 'CIRCLE',
# Blender # Blender
ST.BlenderMaterial: 'DIAMOND',
ST.BlenderObject: 'DIAMOND', ST.BlenderObject: 'DIAMOND',
ST.BlenderCollection: 'DIAMOND', ST.BlenderCollection: 'DIAMOND',
ST.BlenderImage: 'DIAMOND', ST.BlenderImage: 'DIAMOND',

View File

@ -3,6 +3,7 @@ import enum
from ....utils.blender_type_enum import ( from ....utils.blender_type_enum import (
BlenderTypeEnum, BlenderTypeEnum,
append_cls_name_to_values, append_cls_name_to_values,
wrap_values_in_MT,
) )
@ -33,7 +34,6 @@ class SocketType(BlenderTypeEnum):
Complex3DVector = enum.auto() Complex3DVector = enum.auto()
# Blender # Blender
BlenderMaterial = enum.auto()
BlenderObject = enum.auto() BlenderObject = enum.auto()
BlenderCollection = enum.auto() BlenderCollection = enum.auto()

View File

@ -1,7 +1,7 @@
import sympy.physics.units as spu import sympy.physics.units as spu
from ....utils import extra_sympy_units as spux from ....utils import extra_sympy_units as spux
from .socket_types import SocketType as ST # noqa: N817
from .socket_types import SocketType as ST
SOCKET_UNITS = { SOCKET_UNITS = {
ST.PhysicalTime: { ST.PhysicalTime: {

View File

@ -1,58 +0,0 @@
import typing as typ
import sympy.physics.units as spu
from ....utils import extra_sympy_units as spux
from ....utils.pydantic_sympy import SympyExpr
from .socket_types import SocketType as ST # noqa: N817
from .socket_units import SOCKET_UNITS
def _socket_units(socket_type):
return SOCKET_UNITS[socket_type]['values']
UnitSystem: typ.TypeAlias = dict[ST, SympyExpr]
####################
# - Unit Systems
####################
UNITS_BLENDER: UnitSystem = {
ST.PhysicalTime: spu.picosecond,
ST.PhysicalAngle: spu.radian,
ST.PhysicalLength: spu.micrometer,
ST.PhysicalArea: spu.micrometer**2,
ST.PhysicalVolume: spu.micrometer**3,
ST.PhysicalPoint2D: spu.micrometer,
ST.PhysicalPoint3D: spu.micrometer,
ST.PhysicalSize2D: spu.micrometer,
ST.PhysicalSize3D: spu.micrometer,
ST.PhysicalMass: spu.microgram,
ST.PhysicalSpeed: spu.um / spu.second,
ST.PhysicalAccelScalar: spu.um / spu.second**2,
ST.PhysicalForceScalar: spux.micronewton,
ST.PhysicalAccel3D: spu.um / spu.second**2,
ST.PhysicalForce3D: spux.micronewton,
ST.PhysicalFreq: spux.terahertz,
ST.PhysicalPol: spu.radian,
} ## TODO: Load (dynamically?) from addon preferences
UNITS_TIDY3D: UnitSystem = {
## https://docs.flexcompute.com/projects/tidy3d/en/latest/faq/docs/faq/What-are-the-units-used-in-the-simulation.html
ST.PhysicalTime: spu.second,
ST.PhysicalAngle: spu.radian,
ST.PhysicalLength: spu.micrometer,
ST.PhysicalArea: spu.micrometer**2,
ST.PhysicalVolume: spu.micrometer**3,
ST.PhysicalPoint2D: spu.micrometer,
ST.PhysicalPoint3D: spu.micrometer,
ST.PhysicalSize2D: spu.micrometer,
ST.PhysicalSize3D: spu.micrometer,
ST.PhysicalMass: spu.microgram,
ST.PhysicalSpeed: spu.um / spu.second,
ST.PhysicalAccelScalar: spu.um / spu.second**2,
ST.PhysicalForceScalar: spux.micronewton,
ST.PhysicalAccel3D: spu.um / spu.second**2,
ST.PhysicalForce3D: spux.micronewton,
ST.PhysicalFreq: spu.hertz,
ST.PhysicalPol: spu.radian,
}

View File

@ -1,19 +1,2 @@
#from .managed_bl_empty import ManagedBLEmpty
from .managed_bl_image import ManagedBLImage from .managed_bl_image import ManagedBLImage
from .managed_bl_object import ManagedBLObject
# from .managed_bl_collection import ManagedBLCollection
# from .managed_bl_object import ManagedBLObject
from .managed_bl_mesh import ManagedBLMesh
# from .managed_bl_volume import ManagedBLVolume
from .managed_bl_modifier import ManagedBLModifier
__all__ = [
#'ManagedBLEmpty',
'ManagedBLImage',
#'ManagedBLCollection',
#'ManagedBLObject',
'ManagedBLMesh',
#'ManagedBLVolume',
'ManagedBLModifier',
]

View File

@ -1,42 +0,0 @@
import functools
import bpy
from ....utils import logger
log = logger.get(__name__)
MANAGED_COLLECTION_NAME = 'BLMaxwell'
PREVIEW_COLLECTION_NAME = 'BLMaxwell Visible'
####################
# - Global Collection Handling
####################
@functools.cache
def collection(collection_name: str, view_layer_exclude: bool) -> bpy.types.Collection:
# Init the "Managed Collection"
# Ensure Collection exists (and is in the Scene collection)
if collection_name not in bpy.data.collections:
collection = bpy.data.collections.new(collection_name)
bpy.context.scene.collection.children.link(collection)
else:
collection = bpy.data.collections[collection_name]
## Ensure synced View Layer exclusion
if (
layer_collection := bpy.context.view_layer.layer_collection.children[
collection_name
]
).exclude != view_layer_exclude:
layer_collection.exclude = view_layer_exclude
return collection
def managed_collection() -> bpy.types.Collection:
return collection(MANAGED_COLLECTION_NAME, view_layer_exclude=True)
def preview_collection() -> bpy.types.Collection:
return collection(PREVIEW_COLLECTION_NAME, view_layer_exclude=False)

View File

@ -1,10 +1,12 @@
import io
import typing as typ import typing as typ
import typing_extensions as typx
import io
import numpy as np
import pydantic as pyd
import matplotlib.axis as mpl_ax
import bpy import bpy
import matplotlib.axis as mpl_ax
import numpy as np
import typing_extensions as typx
from .. import contracts as ct from .. import contracts as ct
@ -107,7 +109,9 @@ class ManagedBLImage(ct.schemas.ManagedObj):
""" """
if preview_area := self.preview_area: if preview_area := self.preview_area:
return next( return next(
space for space in preview_area.spaces if space.type == SPACE_TYPE space
for space in preview_area.spaces
if space.type == SPACE_TYPE
) )
#################### ####################
@ -157,7 +161,7 @@ class ManagedBLImage(ct.schemas.ManagedObj):
height_px = int(_height_inches * _dpi) height_px = int(_height_inches * _dpi)
else: else:
msg = 'There must either be a preview area, or defined `width_inches`, `height_inches`, and `dpi`' msg = f'There must either be a preview area, or defined `width_inches`, `height_inches`, and `dpi`'
raise ValueError(msg) raise ValueError(msg)
# Compute Plot Dimensions # Compute Plot Dimensions

View File

@ -1,237 +0,0 @@
import contextlib
import bmesh
import bpy
import numpy as np
from ....utils import logger
from .. import contracts as ct
from .managed_bl_collection import managed_collection, preview_collection
log = logger.get(__name__)
####################
# - BLMesh
####################
class ManagedBLMesh(ct.schemas.ManagedObj):
managed_obj_type = ct.ManagedObjType.ManagedBLMesh
_bl_object_name: str | None = None
####################
# - BL Object Name
####################
@property
def name(self):
return self._bl_object_name
@name.setter
def name(self, value: str) -> None:
log.info(
'Changing BLMesh w/Name "%s" to Name "%s"', self._bl_object_name, value
)
if (bl_object := bpy.data.objects.get(value)) is None:
log.info(
'Desired BLMesh Name "%s" Not Taken',
value,
)
if self._bl_object_name is None:
log.info(
'Set New BLMesh Name to "%s"',
value,
)
elif (bl_object := bpy.data.objects.get(self._bl_object_name)) is not None:
log.info(
'Changed BLMesh Name to "%s"',
value,
)
bl_object.name = value
else:
msg = f'ManagedBLMesh with name "{self._bl_object_name}" was deleted'
raise RuntimeError(msg)
# Set Internal Name
self._bl_object_name = value
else:
log.info(
'Desired BLMesh Name "%s" is Taken. Using Blender Rename',
value,
)
# Set Name Anyway, but Respect Blender's Renaming
## When a name already exists, Blender adds .### to prevent overlap.
## `set_name` is allowed to change the name; nodes account for this.
bl_object.name = value
self._bl_object_name = bl_object.name
log.info(
'Changed BLMesh Name to "%s"',
bl_object.name,
)
####################
# - Allocation
####################
def __init__(self, name: str):
self.name = name
####################
# - Deallocation
####################
def free(self):
if (bl_object := bpy.data.objects.get(self.name)) is None:
return
# Delete the Underlying Datablock
## This automatically deletes the object too
log.info('Removing "%s" BLMesh', bl_object.type)
bpy.data.meshes.remove(bl_object.data)
####################
# - Actions
####################
def show_preview(self) -> None:
"""Moves the managed Blender object to the preview collection.
If it's already included, do nothing.
"""
if (bl_object := bpy.data.objects.get(self.name)) is not None:
if bl_object.name not in preview_collection().objects:
log.info('Moving "%s" to Preview Collection', bl_object.name)
preview_collection().objects.link(bl_object)
else:
msg = 'Managed BLMesh does not exist'
raise ValueError(msg)
def hide_preview(self) -> None:
"""Removes the managed Blender object from the preview collection.
If it's already removed, do nothing.
"""
if (bl_object := bpy.data.objects.get(self.name)) is not None:
if bl_object.name in preview_collection().objects:
log.info('Removing "%s" from Preview Collection', bl_object.name)
preview_collection().objects.unlink(bl_object)
else:
msg = 'Managed BLMesh does not exist'
raise ValueError(msg)
def bl_select(self) -> None:
"""Selects the managed Blender object, causing it to be ex. outlined in the 3D viewport."""
if (bl_object := bpy.data.objects.get(self.name)) is not None:
bpy.ops.object.select_all(action='DESELECT')
bl_object.select_set(True)
msg = 'Managed BLMesh does not exist'
raise ValueError(msg)
####################
# - BLMesh Management
####################
def bl_object(self, location: tuple[float, float, float] = (0, 0, 0)):
"""Returns the managed blender object."""
# Create Object w/Appropriate Data Block
if not (bl_object := bpy.data.objects.get(self.name)):
log.info(
'Creating BLMesh Object "%s"',
self.name,
)
bl_data = bpy.data.meshes.new(self.name)
bl_object = bpy.data.objects.new(self.name, bl_data)
log.debug(
'Linking "%s" to Base Collection',
bl_object.name,
)
managed_collection().objects.link(bl_object)
for i, coord in enumerate(location):
if bl_object.location[i] != coord:
bl_object.location[i] = coord
return bl_object
####################
# - Mesh Data Properties
####################
@property
def mesh_data(self) -> bpy.types.Mesh:
"""Directly loads the Blender mesh data.
Raises:
ValueError: If the object has no mesh data.
"""
if bl_object := bpy.data.objects.get(self.name):
return bl_object.data
msg = f'Requested mesh data from {self.name} of type {bl_object.type}'
raise ValueError(msg)
@contextlib.contextmanager
def mesh_as_bmesh(
self,
evaluate: bool = True,
triangulate: bool = False,
) -> bpy.types.Mesh:
if (bl_object := bpy.data.objects.get(self.name)) and bl_object.type == 'MESH':
bmesh_mesh = None
try:
bmesh_mesh = bmesh.new()
if evaluate:
bmesh_mesh.from_object(
bl_object,
bpy.context.evaluated_depsgraph_get(),
)
else:
bmesh_mesh.from_object(bl_object)
if triangulate:
bmesh.ops.triangulate(bmesh_mesh, faces=bmesh_mesh.faces)
yield bmesh_mesh
finally:
if bmesh_mesh:
bmesh_mesh.free()
else:
msg = f'Requested BMesh from "{self.name}" of type "{bl_object.type}"'
raise ValueError(msg)
@property
def mesh_as_arrays(self) -> dict:
## TODO: Cached
# Ensure Updated Geometry
log.debug('Updating View Layer')
bpy.context.view_layer.update()
# Compute Evaluted + Triangulated Mesh
log.debug('Casting BMesh of "%s" to Temporary Mesh', self.name)
_mesh = bpy.data.meshes.new(name='TemporaryMesh')
with self.mesh_as_bmesh(evaluate=True, triangulate=True) as bmesh_mesh:
bmesh_mesh.to_mesh(_mesh)
# Optimized Vertex Copy
## See <https://blog.michelanders.nl/2016/02/copying-vertices-to-numpy-arrays-in_4.html>
log.debug('Copying Vertices from "%s"', self.name)
verts = np.zeros(3 * len(_mesh.vertices), dtype=np.float64)
_mesh.vertices.foreach_get('co', verts)
verts.shape = (-1, 3)
# Optimized Triangle Copy
## To understand, read it, **carefully**.
log.debug('Copying Faces from "%s"', self.name)
faces = np.zeros(3 * len(_mesh.polygons), dtype=np.uint64)
_mesh.polygons.foreach_get('vertices', faces)
faces.shape = (-1, 3)
# Remove Temporary Mesh
log.debug('Removing Temporary Mesh')
bpy.data.meshes.remove(_mesh)
return {
'verts': verts,
'faces': faces,
}

View File

@ -1,212 +0,0 @@
import typing as typ
import bpy
import typing_extensions as typx
from ....utils import analyze_geonodes, logger
from .. import bl_socket_map
from .. import contracts as ct
log = logger.get(__name__)
ModifierType: typ.TypeAlias = typx.Literal['NODES', 'ARRAY']
NodeTreeInterfaceID: typ.TypeAlias = str
class ModifierAttrsNODES(typ.TypedDict):
node_group: bpy.types.GeometryNodeTree
unit_system: bpy.types.GeometryNodeTree
inputs: dict[NodeTreeInterfaceID, typ.Any]
class ModifierAttrsARRAY(typ.TypedDict):
pass
ModifierAttrs: typ.TypeAlias = ModifierAttrsNODES | ModifierAttrsARRAY
MODIFIER_NAMES = {
'NODES': 'BLMaxwell_GeoNodes',
'ARRAY': 'BLMaxwell_Array',
}
####################
# - Read Modifier Information
####################
def read_modifier(bl_modifier: bpy.types.Modifier) -> ModifierAttrs:
if bl_modifier.type == 'NODES':
return {
'node_group': bl_modifier.node_group,
}
elif bl_modifier.type == 'ARRAY':
raise NotImplementedError
raise NotImplementedError
####################
# - Write Modifier Information
####################
def write_modifier_geonodes(
bl_modifier: bpy.types.Modifier,
modifier_attrs: ModifierAttrsNODES,
) -> bool:
modifier_altered = False
# Alter GeoNodes Group
if bl_modifier.node_group != modifier_attrs['node_group']:
log.info(
'Changing GeoNodes Modifier NodeTree from "%s" to "%s"',
str(bl_modifier.node_group),
str(modifier_attrs['node_group']),
)
bl_modifier.node_group = modifier_attrs['node_group']
modifier_altered = True
# Alter GeoNodes Modifier Inputs
## First we retrieve the interface items by-Socket Name
geonodes_interface = analyze_geonodes.interface(
bl_modifier.node_group, direc='INPUT'
)
for (
socket_name,
value,
) in modifier_attrs['inputs'].items():
# Compute Writable BL Socket Value
## Analyzes the socket and unitsys to prep a ready-to-write value.
## Write directly to the modifier dict.
bl_socket_value = bl_socket_map.writable_bl_socket_value(
geonodes_interface[socket_name],
value,
unit_system=modifier_attrs['unit_system'],
allow_unit_not_in_unit_system=True,
)
# Compute Interface ID from Socket Name
## We can't index the modifier by socket name; only by Interface ID.
## Still, we require that socket names are unique.
iface_id = geonodes_interface[socket_name].identifier
# IF List-Like: Alter Differing Elements
if isinstance(bl_socket_value, tuple):
for i, bl_socket_subvalue in enumerate(bl_socket_value):
if bl_modifier[iface_id][i] != bl_socket_subvalue:
bl_modifier[iface_id][i] = bl_socket_subvalue
modifier_altered = True
# IF int/float Mismatch: Assign Float-Cast of Integer
## Blender is strict; only floats can set float vals.
## We are less strict; if the user passes an int, that's okay.
elif isinstance(bl_socket_value, int) and isinstance(
bl_modifier[iface_id],
float,
):
bl_modifier[iface_id] = float(bl_socket_value)
modifier_altered = True
else:
bl_modifier[iface_id] = bl_socket_value
modifier_altered = True
return modifier_altered
def write_modifier(
bl_modifier: bpy.types.Modifier,
modifier_attrs: ModifierAttrs,
) -> bool:
"""Writes modifier attributes to the modifier, changing only what's needed.
Returns:
True if the modifier was altered.
"""
modifier_altered = False
if bl_modifier.type == 'NODES':
modifier_altered = write_modifier_geonodes(bl_modifier, modifier_attrs)
elif bl_modifier.type == 'ARRAY':
raise NotImplementedError
else:
raise NotImplementedError
return modifier_altered
####################
# - ManagedObj
####################
class ManagedBLModifier(ct.schemas.ManagedObj):
managed_obj_type = ct.ManagedObjType.ManagedBLModifier
_modifier_name: str | None = None
####################
# - BL Object Name
####################
@property
def name(self):
return self._modifier_name
@name.setter
def name(self, value: str) -> None:
## TODO: Handle name conflict within same BLObject
log.info(
'Changing BLModifier w/Name "%s" to Name "%s"', self._modifier_name, value
)
self._modifier_name = value
####################
# - Allocation
####################
def __init__(self, name: str):
self.name = name
####################
# - Deallocation
####################
def free(self):
pass
####################
# - Modifiers
####################
def bl_modifier(
self,
bl_object: bpy.types.Object,
modifier_type: ModifierType,
modifier_attrs: ModifierAttrs,
):
"""Creates a new modifier for the current `bl_object`.
- Modifier Type Names: <https://docs.blender.org/api/current/bpy_types_enum_items/object_modifier_type_items.html#rna-enum-object-modifier-type-items>
"""
# Remove Mismatching Modifier
modifier_was_removed = False
if (
bl_modifier := bl_object.modifiers.get(self.name)
) and bl_modifier.type != modifier_type:
log.info(
'Removing (recreating) BLModifier "%s" on BLObject "%s" (existing modifier_type is "%s", but "%s" is requested)',
bl_modifier.name,
bl_object.name,
bl_modifier.type,
modifier_type,
)
self.free()
modifier_was_removed = True
# Create Modifier
if bl_modifier is None or modifier_was_removed:
log.info(
'Creating BLModifier "%s" on BLObject "%s" with modifier_type "%s"',
self.name,
bl_object.name,
modifier_type,
)
bl_modifier = bl_object.modifiers.new(
name=self.name,
type=modifier_type,
)
if modifier_altered := write_modifier(bl_modifier, modifier_attrs):
bl_object.data.update()
return bl_modifier

View File

@ -1,106 +1,106 @@
import contextlib import typing as typ
import bmesh
import bpy
import numpy as np
import typing_extensions as typx import typing_extensions as typx
import functools
import contextlib
import io
import numpy as np
import pydantic as pyd
import matplotlib.axis as mpl_ax
import bpy
import bmesh
from ....utils import logger
from .. import contracts as ct from .. import contracts as ct
from .managed_bl_collection import managed_collection, preview_collection
log = logger.get(__name__)
ModifierType = typx.Literal['NODES', 'ARRAY'] ModifierType = typx.Literal['NODES', 'ARRAY']
MODIFIER_NAMES = { MODIFIER_NAMES = {
'NODES': 'BLMaxwell_GeoNodes', 'NODES': 'BLMaxwell_GeoNodes',
'ARRAY': 'BLMaxwell_Array', 'ARRAY': 'BLMaxwell_Array',
} }
MANAGED_COLLECTION_NAME = 'BLMaxwell'
PREVIEW_COLLECTION_NAME = 'BLMaxwell Visible'
def bl_collection(
collection_name: str, view_layer_exclude: bool
) -> bpy.types.Collection:
# Init the "Managed Collection"
# Ensure Collection exists (and is in the Scene collection)
if collection_name not in bpy.data.collections:
collection = bpy.data.collections.new(collection_name)
bpy.context.scene.collection.children.link(collection)
else:
collection = bpy.data.collections[collection_name]
## Ensure synced View Layer exclusion
if (
layer_collection := bpy.context.view_layer.layer_collection.children[
collection_name
]
).exclude != view_layer_exclude:
layer_collection.exclude = view_layer_exclude
return collection
####################
# - BLObject
####################
class ManagedBLObject(ct.schemas.ManagedObj): class ManagedBLObject(ct.schemas.ManagedObj):
managed_obj_type = ct.ManagedObjType.ManagedBLObject managed_obj_type = ct.ManagedObjType.ManagedBLObject
_bl_object_name: str | None = None _bl_object_name: str
#################### def __init__(self, name: str):
# - BL Object Name self._bl_object_name = name
####################
# Object Name
@property @property
def name(self): def name(self):
return self._bl_object_name return self._bl_object_name
@name.setter @name.setter
def name(self, value: str) -> None: def set_name(self, value: str) -> None:
log.info( # Object Doesn't Exist
'Changing BLObject w/Name "%s" to Name "%s"', self._bl_object_name, value if not (bl_object := bpy.data.objects.get(self._bl_object_name)):
) # ...AND Desired Object Name is Not Taken
if not bpy.data.objects.get(value): if not bpy.data.objects.get(value):
log.info(
'Desired BLObject Name "%s" Not Taken',
value,
)
if self._bl_object_name is None:
log.info(
'Set New BLObject Name to "%s"',
value,
)
elif bl_object := bpy.data.objects.get(self._bl_object_name):
log.info(
'Changed BLObject Name to "%s"',
value,
)
bl_object.name = value
else:
msg = f'ManagedBLObject with name "{self._bl_object_name}" was deleted'
raise RuntimeError(msg)
# Set Internal Name
self._bl_object_name = value self._bl_object_name = value
else: return
log.info(
'Desired BLObject Name "%s" is Taken. Using Blender Rename',
value,
)
# Set Name Anyway, but Respect Blender's Renaming # ...AND Desired Object Name is Taken
## When a name already exists, Blender adds .### to prevent overlap. else:
## `set_name` is allowed to change the name; nodes account for this. msg = f'Desired name {value} for BL object is taken'
raise ValueError(msg)
# Object DOES Exist
bl_object.name = value bl_object.name = value
self._bl_object_name = bl_object.name self._bl_object_name = bl_object.name
## - When name exists, Blender adds .### to prevent overlap.
## - `set_name` is allowed to change the name; nodes account for this.
log.info( # Object Datablock Name
'Changed BLObject Name to "%s"', @property
bl_object.name, def bl_mesh_name(self):
) return self.name
#################### @property
# - Allocation def bl_volume_name(self):
#################### return self.name
def __init__(self, name: str):
self.name = name
#################### # Deallocation
# - Deallocation
####################
def free(self): def free(self):
if (bl_object := bpy.data.objects.get(self.name)) is None: if not (bl_object := bpy.data.objects.get(self.name)):
return return ## Nothing to do
# Delete the Underlying Datablock # Delete the Underlying Datablock
## This automatically deletes the object too ## This automatically deletes the object too
log.info('Removing "%s" BLObject', bl_object.type) if bl_object.type == 'MESH':
if bl_object.type in {'MESH', 'EMPTY'}: bpy.data.meshes.remove(bl_object.data)
elif bl_object.type == 'EMPTY':
bpy.data.meshes.remove(bl_object.data) bpy.data.meshes.remove(bl_object.data)
elif bl_object.type == 'VOLUME': elif bl_object.type == 'VOLUME':
bpy.data.volumes.remove(bl_object.data) bpy.data.volumes.remove(bl_object.data)
else: else:
msg = f'BLObject "{bl_object.name}" has invalid kind "{bl_object.type}"' msg = f'Type of to-delete `bl_object`, {bl_object.type}, is not valid'
raise RuntimeError(msg) raise ValueError(msg)
#################### ####################
# - Actions # - Actions
@ -125,17 +125,17 @@ class ManagedBLObject(ct.schemas.ManagedObj):
If it's already included, do nothing. If it's already included, do nothing.
""" """
bl_object = self.bl_object(kind) bl_object = self.bl_object(kind)
if bl_object.name not in preview_collection().objects: if (
log.info('Moving "%s" to Preview Collection', bl_object.name) bl_object.name
preview_collection().objects.link(bl_object) not in (
preview_collection := bl_collection(
# Display Parameters PREVIEW_COLLECTION_NAME, view_layer_exclude=False
if kind == 'EMPTY' and empty_display_type is not None:
log.info(
'Setting Empty Display Type "%s" for "%s"',
empty_display_type,
bl_object.name,
) )
).objects
):
preview_collection.objects.link(bl_object)
if kind == 'EMPTY' and empty_display_type is not None:
bl_object.empty_display_type = empty_display_type bl_object.empty_display_type = empty_display_type
def hide_preview( def hide_preview(
@ -147,23 +147,29 @@ class ManagedBLObject(ct.schemas.ManagedObj):
If it's already removed, do nothing. If it's already removed, do nothing.
""" """
bl_object = self.bl_object(kind) bl_object = self.bl_object(kind)
if bl_object.name not in preview_collection().objects: if (
log.info('Removing "%s" from Preview Collection', bl_object.name) bl_object.name
not in (
preview_collection := bl_collection(
PREVIEW_COLLECTION_NAME, view_layer_exclude=False
)
).objects
):
preview_collection.objects.unlink(bl_object) preview_collection.objects.unlink(bl_object)
def bl_select(self) -> None: def bl_select(self) -> None:
"""Selects the managed Blender object globally, causing it to be ex. """Selects the managed Blender object globally, causing it to be ex.
outlined in the 3D viewport. outlined in the 3D viewport.
""" """
if (bl_object := bpy.data.objects.get(self.name)) is not None: if not (bl_object := bpy.data.objects.get(self.name)):
bpy.ops.object.select_all(action='DESELECT')
bl_object.select_set(True)
msg = 'Managed BLObject does not exist' msg = 'Managed BLObject does not exist'
raise ValueError(msg) raise ValueError(msg)
bpy.ops.object.select_all(action='DESELECT')
bl_object.select_set(True)
#################### ####################
# - BLObject Management # - Managed Object Management
#################### ####################
def bl_object( def bl_object(
self, self,
@ -171,42 +177,33 @@ class ManagedBLObject(ct.schemas.ManagedObj):
): ):
"""Returns the managed blender object. """Returns the managed blender object.
If the requested object data kind is different, then delete the old If the requested object data type is different, then delete the old
object and recreate. object and recreate.
""" """
# Remove Object (if mismatch) # Remove Object (if mismatch)
if (bl_object := bpy.data.objects.get(self.name)) and bl_object.type != kind: if (
log.info( bl_object := bpy.data.objects.get(self.name)
'Removing (recreating) "%s" (existing kind is "%s", but "%s" is requested)', ) and bl_object.type != kind:
bl_object.name,
bl_object.type,
kind,
)
self.free() self.free()
# Create Object w/Appropriate Data Block # Create Object w/Appropriate Data Block
if not (bl_object := bpy.data.objects.get(self.name)): if not (bl_object := bpy.data.objects.get(self.name)):
log.info(
'Creating "%s" with kind "%s"',
self.name,
kind,
)
if kind == 'MESH': if kind == 'MESH':
bl_data = bpy.data.meshes.new(self.name) bl_data = bpy.data.meshes.new(self.bl_mesh_name)
elif kind == 'EMPTY': elif kind == 'EMPTY':
bl_data = None bl_data = None
elif kind == 'VOLUME': elif kind == 'VOLUME':
raise NotImplementedError raise NotImplementedError
else: else:
msg = f'Created BLObject w/invalid kind "{bl_object.type}" for "{self.name}"' msg = (
f'Requested `bl_object` type {bl_object.type} is not valid'
)
raise ValueError(msg) raise ValueError(msg)
bl_object = bpy.data.objects.new(self.name, bl_data) bl_object = bpy.data.objects.new(self.name, bl_data)
log.debug( bl_collection(
'Linking "%s" to Base Collection', MANAGED_COLLECTION_NAME, view_layer_exclude=True
bl_object.name, ).objects.link(bl_object)
)
managed_collection().objects.link(bl_object)
return bl_object return bl_object
@ -214,16 +211,17 @@ class ManagedBLObject(ct.schemas.ManagedObj):
# - Mesh Data Properties # - Mesh Data Properties
#################### ####################
@property @property
def mesh_data(self) -> bpy.types.Mesh: def raw_mesh(self) -> bpy.types.Mesh:
"""Directly loads the Blender mesh data. """Returns the object's raw mesh data.
Raises: Raises an error if the object has no mesh data.
ValueError: If the object has no mesh data.
""" """
if (bl_object := bpy.data.objects.get(self.name)) and bl_object.type == 'MESH': if (
bl_object := bpy.data.objects.get(self.name)
) and bl_object.type == 'MESH':
return bl_object.data return bl_object.data
msg = f'Requested mesh data from {self.name} of type {bl_object.type}' msg = f'Requested MESH data from `bl_object` of type {bl_object.type}'
raise ValueError(msg) raise ValueError(msg)
@contextlib.contextmanager @contextlib.contextmanager
@ -232,7 +230,9 @@ class ManagedBLObject(ct.schemas.ManagedObj):
evaluate: bool = True, evaluate: bool = True,
triangulate: bool = False, triangulate: bool = False,
) -> bpy.types.Mesh: ) -> bpy.types.Mesh:
if (bl_object := bpy.data.objects.get(self.name)) and bl_object.type == 'MESH': if (
bl_object := bpy.data.objects.get(self.name)
) and bl_object.type == 'MESH':
bmesh_mesh = None bmesh_mesh = None
try: try:
bmesh_mesh = bmesh.new() bmesh_mesh = bmesh.new()
@ -254,7 +254,7 @@ class ManagedBLObject(ct.schemas.ManagedObj):
bmesh_mesh.free() bmesh_mesh.free()
else: else:
msg = f'Requested BMesh from "{self.name}" of type "{bl_object.type}"' msg = f'Requested BMesh from `bl_object` of type {bl_object.type}'
raise ValueError(msg) raise ValueError(msg)
@property @property
@ -262,31 +262,27 @@ class ManagedBLObject(ct.schemas.ManagedObj):
## TODO: Cached ## TODO: Cached
# Ensure Updated Geometry # Ensure Updated Geometry
log.debug('Updating View Layer')
bpy.context.view_layer.update() bpy.context.view_layer.update()
## TODO: Must we?
# Compute Evaluted + Triangulated Mesh # Compute Evaluted + Triangulated Mesh
log.debug('Casting BMesh of "%s" to Temporary Mesh', self.name)
_mesh = bpy.data.meshes.new(name='TemporaryMesh') _mesh = bpy.data.meshes.new(name='TemporaryMesh')
with self.mesh_as_bmesh(evaluate=True, triangulate=True) as bmesh_mesh: with self.mesh_as_bmesh(evaluate=True, triangulate=True) as bmesh_mesh:
bmesh_mesh.to_mesh(_mesh) bmesh_mesh.to_mesh(_mesh)
# Optimized Vertex Copy # Optimized Vertex Copy
## See <https://blog.michelanders.nl/2016/02/copying-vertices-to-numpy-arrays-in_4.html> ## See <https://blog.michelanders.nl/2016/02/copying-vertices-to-numpy-arrays-in_4.html>
log.debug('Copying Vertices from "%s"', self.name)
verts = np.zeros(3 * len(_mesh.vertices), dtype=np.float64) verts = np.zeros(3 * len(_mesh.vertices), dtype=np.float64)
_mesh.vertices.foreach_get('co', verts) _mesh.vertices.foreach_get('co', verts)
verts.shape = (-1, 3) verts.shape = (-1, 3)
# Optimized Triangle Copy # Optimized Triangle Copy
## To understand, read it, **carefully**. ## To understand, read it, **carefully**.
log.debug('Copying Faces from "%s"', self.name)
faces = np.zeros(3 * len(_mesh.polygons), dtype=np.uint64) faces = np.zeros(3 * len(_mesh.polygons), dtype=np.uint64)
_mesh.polygons.foreach_get('vertices', faces) _mesh.polygons.foreach_get('vertices', faces)
faces.shape = (-1, 3) faces.shape = (-1, 3)
# Remove Temporary Mesh # Remove Temporary Mesh
log.debug('Removing Temporary Mesh')
bpy.data.meshes.remove(_mesh) bpy.data.meshes.remove(_mesh)
return { return {
@ -295,7 +291,7 @@ class ManagedBLObject(ct.schemas.ManagedObj):
} }
#################### ####################
# - Modifiers # - Modifier Methods
#################### ####################
def bl_modifier( def bl_modifier(
self, self,
@ -303,10 +299,10 @@ class ManagedBLObject(ct.schemas.ManagedObj):
): ):
"""Creates a new modifier for the current `bl_object`. """Creates a new modifier for the current `bl_object`.
- Modifier Type Names: <https://docs.blender.org/api/current/bpy_types_enum_items/object_modifier_type_items.html#rna-enum-object-modifier-type-items> For all Blender modifier type names, see: <https://docs.blender.org/api/current/bpy_types_enum_items/object_modifier_type_items.html#rna-enum-object-modifier-type-items>
""" """
if not (bl_object := bpy.data.objects.get(self.name)): if not (bl_object := bpy.data.objects.get(self.name)):
msg = f'Tried to add modifier to "{self.name}", but it has no bl_object' msg = "Can't add modifier to BL object that doesn't exist"
raise ValueError(msg) raise ValueError(msg)
# (Create and) Return Modifier # (Create and) Return Modifier
@ -380,7 +376,8 @@ class ManagedBLObject(ct.schemas.ManagedObj):
# Quickly Determine if IDPropertyArray is Equal # Quickly Determine if IDPropertyArray is Equal
if ( if (
hasattr(bl_modifier[interface_identifier], 'to_list') hasattr(bl_modifier[interface_identifier], 'to_list')
and tuple(bl_modifier[interface_identifier].to_list()) == value and tuple(bl_modifier[interface_identifier].to_list())
== value
): ):
continue continue
@ -398,3 +395,18 @@ class ManagedBLObject(ct.schemas.ManagedObj):
# Update DepGraph (if anything changed) # Update DepGraph (if anything changed)
if modifier_altered: if modifier_altered:
bl_object.data.update() bl_object.data.update()
# @property
# def volume(self) -> bpy.types.Volume:
# """Returns the object's volume data.
#
# Raises an error if the object has no volume data.
# """
# if (
# (bl_object := bpy.data.objects.get(self.bl_object_name))
# and bl_object.type == "VOLUME"
# ):
# return bl_object.data
#
# msg = f"Requested VOLUME data from `bl_object` of type {bl_object.type}"
# raise ValueError(msg)

View File

@ -2,11 +2,7 @@ import typing as typ
import bpy import bpy
from ...utils import logger
from . import contracts as ct from . import contracts as ct
from .managed_objs.managed_bl_collection import preview_collection
log = logger.get(__name__)
#################### ####################
# - Cache Management # - Cache Management
@ -77,15 +73,6 @@ class MaxwellSimTree(bpy.types.NodeTree):
for bl_socket in [*node.inputs, *node.outputs]: for bl_socket in [*node.inputs, *node.outputs]:
bl_socket.locked = False bl_socket.locked = False
def unpreview_all(self):
log.info('Disabling All 3D Previews')
for node in self.nodes:
if node.preview_active:
node.preview_active = False
for bl_object in preview_collection().objects.values():
preview_collection().objects.unlink(bl_object)
#################### ####################
# - Init Methods # - Init Methods
#################### ####################
@ -138,7 +125,9 @@ class MaxwellSimTree(bpy.types.NodeTree):
'to_add': [], 'to_add': [],
} }
for link_ptr in delta_links['removed']: for link_ptr in delta_links['removed']:
from_socket = self._node_link_cache.link_ptrs_from_sockets[link_ptr] from_socket = self._node_link_cache.link_ptrs_from_sockets[
link_ptr
]
to_socket = self._node_link_cache.link_ptrs_to_sockets[link_ptr] to_socket = self._node_link_cache.link_ptrs_to_sockets[link_ptr]
# Update Socket Caches # Update Socket Caches
@ -147,7 +136,9 @@ class MaxwellSimTree(bpy.types.NodeTree):
# Trigger Report Chain on Socket that Just Lost a Link # Trigger Report Chain on Socket that Just Lost a Link
## Aka. Forward-Refresh Caches Relying on Linkage ## Aka. Forward-Refresh Caches Relying on Linkage
if not (consent_removal := to_socket.sync_link_removed(from_socket)): if not (
consent_removal := to_socket.sync_link_removed(from_socket)
):
# Did Not Consent to Removal: Queue Add Link # Did Not Consent to Removal: Queue Add Link
link_alterations['to_add'].append((from_socket, to_socket)) link_alterations['to_add'].append((from_socket, to_socket))

View File

@ -1,17 +1,16 @@
# from . import kitchen_sink # from . import kitchen_sink
from . import inputs
from . import outputs
from . import sources
from . import mediums
from . import structures
# from . import bounds # from . import bounds
from . import ( from . import monitors
inputs, from . import simulations
mediums, from . import utilities
monitors, from . import viz
outputs,
simulations,
sources,
structures,
utilities,
viz,
)
BL_REGISTER = [ BL_REGISTER = [
# *kitchen_sink.BL_REGISTER, # *kitchen_sink.BL_REGISTER,

View File

@ -1,3 +1,4 @@
import inspect
import json import json
import typing as typ import typing as typ
import uuid import uuid
@ -21,7 +22,7 @@ _DEFAULT_LOOSE_SOCKET_SER = json.dumps(
'socket_def_names': [], 'socket_def_names': [],
'models': [], 'models': [],
} }
) ## TODO: What in the jesus christ is this )
class MaxwellSimNode(bpy.types.Node): class MaxwellSimNode(bpy.types.Node):
@ -42,18 +43,16 @@ class MaxwellSimNode(bpy.types.Node):
# Sockets # Sockets
_output_socket_methods: dict _output_socket_methods: dict
input_sockets: typ.ClassVar[dict[str, ct.schemas.SocketDef]] = {} input_sockets: dict[str, ct.schemas.SocketDef] = {}
output_sockets: typ.ClassVar[dict[str, ct.schemas.SocketDef]] = {} output_sockets: dict[str, ct.schemas.SocketDef] = {}
input_socket_sets: typ.ClassVar[dict[str, dict[str, ct.schemas.SocketDef]]] = {} input_socket_sets: dict[str, dict[str, ct.schemas.SocketDef]] = {}
output_socket_sets: typ.ClassVar[dict[str, dict[str, ct.schemas.SocketDef]]] = {} output_socket_sets: dict[str, dict[str, ct.schemas.SocketDef]] = {}
# Presets # Presets
presets: typ.ClassVar = {} presets = {}
# Managed Objects # Managed Objects
managed_obj_defs: typ.ClassVar[ managed_obj_defs: dict[ct.ManagedObjName, ct.schemas.ManagedObjDef] = {}
dict[ct.ManagedObjName, ct.schemas.ManagedObjDef]
] = {}
#################### ####################
# - Initialization # - Initialization
@ -83,14 +82,6 @@ class MaxwellSimNode(bpy.types.Node):
update=(lambda self, context: self.sync_sim_node_name(context)), update=(lambda self, context: self.sync_sim_node_name(context)),
) )
# Setup "Previewing" Property for Node
cls.__annotations__['preview_active'] = bpy.props.BoolProperty(
name='Preview Active',
description='Whether the preview (if any) is currently active',
default=False,
update=lambda self, context: self.sync_preview_active(context),
)
# Setup Locked Property for Node # Setup Locked Property for Node
cls.__annotations__['locked'] = bpy.props.BoolProperty( cls.__annotations__['locked'] = bpy.props.BoolProperty(
name='Locked State', name='Locked State',
@ -105,30 +96,34 @@ class MaxwellSimNode(bpy.types.Node):
# Setup Callback Methods # Setup Callback Methods
cls._output_socket_methods = { cls._output_socket_methods = {
(method.extra_data['output_socket_name'], method.extra_data['kind']): method method._index_by: method
for attr_name in dir(cls) for attr_name in dir(cls)
if hasattr(method := getattr(cls, attr_name), 'action_type') if hasattr(method := getattr(cls, attr_name), '_callback_type')
and method.action_type == 'computes_output_socket' and method._callback_type == 'computes_output_socket'
and hasattr(method, 'extra_data')
and method.extra_data
} }
cls._on_value_changed_methods = { cls._on_value_changed_methods = {
method method
for attr_name in dir(cls) for attr_name in dir(cls)
if hasattr(method := getattr(cls, attr_name), 'action_type') if hasattr(method := getattr(cls, attr_name), '_callback_type')
and method.action_type == 'on_value_changed' and method._callback_type == 'on_value_changed'
}
cls._on_show_preview = {
method
for attr_name in dir(cls)
if hasattr(method := getattr(cls, attr_name), '_callback_type')
and method._callback_type == 'on_show_preview'
} }
cls._on_show_plot = { cls._on_show_plot = {
method method
for attr_name in dir(cls) for attr_name in dir(cls)
if hasattr(method := getattr(cls, attr_name), 'action_type') if hasattr(method := getattr(cls, attr_name), '_callback_type')
and method.action_type == 'on_show_plot' and method._callback_type == 'on_show_plot'
} }
cls._on_init = { cls._on_init = {
method method
for attr_name in dir(cls) for attr_name in dir(cls)
if hasattr(method := getattr(cls, attr_name), 'action_type') if hasattr(method := getattr(cls, attr_name), '_callback_type')
and method.action_type == 'on_init' and method._callback_type == 'on_init'
} }
# Setup Socket Set Dropdown # Setup Socket Set Dropdown
@ -140,7 +135,7 @@ class MaxwellSimNode(bpy.types.Node):
_input_socket_set_names := list(cls.input_socket_sets.keys()) _input_socket_set_names := list(cls.input_socket_sets.keys())
) + [ ) + [
output_socket_set_name output_socket_set_name
for output_socket_set_name in cls.output_socket_sets for output_socket_set_name in cls.output_socket_sets.keys()
if output_socket_set_name not in _input_socket_set_names if output_socket_set_name not in _input_socket_set_names
] ]
socket_set_ids = [ socket_set_ids = [
@ -162,11 +157,12 @@ class MaxwellSimNode(bpy.types.Node):
for socket_set_id, socket_set_name in zip( for socket_set_id, socket_set_name in zip(
socket_set_ids, socket_set_ids,
socket_set_names, socket_set_names,
strict=False,
) )
], ],
default=socket_set_names[0], default=socket_set_names[0],
update=lambda self, context: self.sync_active_socket_set(context), update=lambda self, context: self.sync_active_socket_set(
context
),
) )
# Setup Preset Dropdown # Setup Preset Dropdown
@ -185,8 +181,8 @@ class MaxwellSimNode(bpy.types.Node):
) )
for preset_name, preset_def in cls.presets.items() for preset_name, preset_def in cls.presets.items()
], ],
default=next(cls.presets.keys()), default=list(cls.presets.keys())[0],
update=lambda self, _: (self.sync_active_preset()()), update=lambda self, context: (self.sync_active_preset()()),
) )
#################### ####################
@ -196,7 +192,7 @@ class MaxwellSimNode(bpy.types.Node):
self.sync_sockets() self.sync_sockets()
self.sync_prop('active_socket_set', context) self.sync_prop('active_socket_set', context)
def sync_sim_node_name(self, _): def sync_sim_node_name(self, context):
if (mobjs := CACHE[self.instance_id].get('managed_objs')) is None: if (mobjs := CACHE[self.instance_id].get('managed_objs')) is None:
return return
@ -211,26 +207,12 @@ class MaxwellSimNode(bpy.types.Node):
## - If altered, set the 'sim_node_name' to the altered name. ## - If altered, set the 'sim_node_name' to the altered name.
## - This will cause recursion, but only once. ## - This will cause recursion, but only once.
def sync_preview_active(self, _: bpy.types.Context):
log.info(
'Changed Preview Active in "%s" to "%s"',
self.name,
self.preview_active,
)
for method in self._on_value_changed_methods:
if 'preview_active' in method.extra_data['changed_props']:
log.info(
'Running Previewer Callback "%s" in "%s")',
method.__name__,
self.name,
)
method(self)
#################### ####################
# - Managed Object Properties # - Managed Object Properties
#################### ####################
@property @property
def managed_objs(self): def managed_objs(self):
global CACHE
if not CACHE.get(self.instance_id): if not CACHE.get(self.instance_id):
CACHE[self.instance_id] = {} CACHE[self.instance_id] = {}
@ -247,7 +229,9 @@ class MaxwellSimNode(bpy.types.Node):
# Fill w/Managed Objects by Name Socket # Fill w/Managed Objects by Name Socket
for mobj_id, mobj_def in self.managed_obj_defs.items(): for mobj_id, mobj_def in self.managed_obj_defs.items():
name = mobj_def.name_prefix + self.sim_node_name name = mobj_def.name_prefix + self.sim_node_name
CACHE[self.instance_id]['managed_objs'][mobj_id] = mobj_def.mk(name) CACHE[self.instance_id]['managed_objs'][mobj_id] = mobj_def.mk(
name
)
return CACHE[self.instance_id]['managed_objs'] return CACHE[self.instance_id]['managed_objs']
@ -269,7 +253,9 @@ class MaxwellSimNode(bpy.types.Node):
# Retrieve Active Socket Set Sockets # Retrieve Active Socket Set Sockets
socket_sets = ( socket_sets = (
self.input_socket_sets if direc == 'input' else self.output_socket_sets self.input_socket_sets
if direc == 'input'
else self.output_socket_sets
) )
active_socket_set_sockets = socket_sets.get(self.active_socket_set) active_socket_set_sockets = socket_sets.get(self.active_socket_set)
@ -279,13 +265,24 @@ class MaxwellSimNode(bpy.types.Node):
return active_socket_set_sockets return active_socket_set_sockets
def active_sockets(self, direc: typx.Literal['input', 'output']): def active_sockets(self, direc: typx.Literal['input', 'output']):
static_sockets = self.input_sockets if direc == 'input' else self.output_sockets static_sockets = (
self.input_sockets if direc == 'input' else self.output_sockets
)
socket_sets = (
self.input_socket_sets
if direc == 'input'
else self.output_socket_sets
)
loose_sockets = ( loose_sockets = (
self.loose_input_sockets if direc == 'input' else self.loose_output_sockets self.loose_input_sockets
if direc == 'input'
else self.loose_output_sockets
) )
return ( return (
static_sockets | self.active_socket_set_sockets(direc=direc) | loose_sockets static_sockets
| self.active_socket_set_sockets(direc=direc)
| loose_sockets
) )
#################### ####################
@ -305,8 +302,12 @@ class MaxwellSimNode(bpy.types.Node):
) )
## Internal Serialization/Deserialization Methods (yuck) ## Internal Serialization/Deserialization Methods (yuck)
def _ser_loose_sockets(self, deser: dict[str, ct.schemas.SocketDef]) -> str: def _ser_loose_sockets(
if not all(isinstance(model, pyd.BaseModel) for model in deser.values()): self, deser: dict[str, ct.schemas.SocketDef]
) -> str:
if not all(
isinstance(model, pyd.BaseModel) for model in deser.values()
):
msg = 'Trying to deserialize loose sockets with invalid SocketDefs (they must be `pydantic` BaseModels).' msg = 'Trying to deserialize loose sockets with invalid SocketDefs (they must be `pydantic` BaseModels).'
raise ValueError(msg) raise ValueError(msg)
@ -324,7 +325,9 @@ class MaxwellSimNode(bpy.types.Node):
} }
) ## Big reliance on order-preservation of dicts here.) ) ## Big reliance on order-preservation of dicts here.)
def _deser_loose_sockets(self, ser: str) -> dict[str, ct.schemas.SocketDef]: def _deser_loose_sockets(
self, ser: str
) -> dict[str, ct.schemas.SocketDef]:
semi_deser = json.loads(ser) semi_deser = json.loads(ser)
return { return {
socket_name: getattr(sockets, socket_def_name)(**model_kwargs) socket_name: getattr(sockets, socket_def_name)(**model_kwargs)
@ -332,7 +335,6 @@ class MaxwellSimNode(bpy.types.Node):
semi_deser['socket_names'], semi_deser['socket_names'],
semi_deser['socket_def_names'], semi_deser['socket_def_names'],
semi_deser['models'], semi_deser['models'],
strict=False,
) )
if hasattr(sockets, socket_def_name) if hasattr(sockets, socket_def_name)
} }
@ -352,11 +354,6 @@ class MaxwellSimNode(bpy.types.Node):
self, self,
value: dict[str, ct.schemas.SocketDef], value: dict[str, ct.schemas.SocketDef],
) -> None: ) -> None:
log.info(
'Setting Loose Input Sockets on "%s" to "%s"',
self.bl_label,
str(value),
)
if not value: if not value:
self.ser_loose_input_sockets = _DEFAULT_LOOSE_SOCKET_SER self.ser_loose_input_sockets = _DEFAULT_LOOSE_SOCKET_SER
else: else:
@ -451,7 +448,9 @@ class MaxwellSimNode(bpy.types.Node):
# - Preset Management # - Preset Management
#################### ####################
def sync_active_preset(self) -> None: def sync_active_preset(self) -> None:
"""Applies the active preset by overwriting the value of preset-defined input sockets.""" """Applies the active preset by overwriting the value of
preset-defined input sockets.
"""
if not (preset_def := self.presets.get(self.active_preset)): if not (preset_def := self.presets.get(self.active_preset)):
msg = f'Tried to apply active preset, but the active preset ({self.active_preset}) is not in presets ({self.presets})' msg = f'Tried to apply active preset, but the active preset ({self.active_preset}) is not in presets ({self.presets})'
raise RuntimeError(msg) raise RuntimeError(msg)
@ -508,7 +507,7 @@ class MaxwellSimNode(bpy.types.Node):
## TODO: Side panel buttons for fanciness. ## TODO: Side panel buttons for fanciness.
def draw_plot_settings(self, _: bpy.types.Context, layout: bpy.types.UILayout): def draw_plot_settings(self, context, layout):
if self.locked: if self.locked:
layout.enabled = False layout.enabled = False
@ -523,14 +522,16 @@ class MaxwellSimNode(bpy.types.Node):
"""Computes the data of an input socket, by socket name and data flow kind, by asking the socket nicely via `bl_socket.compute_data`. """Computes the data of an input socket, by socket name and data flow kind, by asking the socket nicely via `bl_socket.compute_data`.
Args: Args:
input_socket_name: The name of the input socket, as defined in `self.input_sockets`. input_socket_name: The name of the input socket, as defined in
kind: The kind of data flow to compute. `self.input_sockets`.
kind: The data flow kind to compute retrieve.
""" """
if bl_socket := self.inputs.get(input_socket_name): if not (bl_socket := self.inputs.get(input_socket_name)):
return bl_socket.compute_data(kind=kind) return None
# msg = f"Input socket name {input_socket_name} is not an active input sockets."
# raise ValueError(msg)
msg = f'Input socket "{input_socket_name}" on "{self.bl_idname}" is not an active input socket' return bl_socket.compute_data(kind=kind)
raise ValueError(msg)
def compute_output( def compute_output(
self, self,
@ -543,25 +544,27 @@ class MaxwellSimNode(bpy.types.Node):
This method is run to produce the value. This method is run to produce the value.
Args: Args:
output_socket_name: The name declaring the output socket, for which this method computes the output. output_socket_name: The name declaring the output socket,
kind: The DataFlowKind to use when computing the output socket value. for which this method computes the output.
Returns: Returns:
The value of the output socket, as computed by the dedicated method The value of the output socket, as computed by the dedicated method
registered using the `@computes_output_socket` decorator. registered using the `@computes_output_socket` decorator.
""" """
if output_socket_method := self._output_socket_methods.get( if not (
output_socket_method := self._output_socket_methods.get(
(output_socket_name, kind) (output_socket_name, kind)
)
): ):
return output_socket_method(self) msg = f'No output method for ({output_socket_name}, {str(kind.value)}'
msg = f'No output method for ({output_socket_name}, {kind.value!s}'
raise ValueError(msg) raise ValueError(msg)
return output_socket_method(self)
#################### ####################
# - Action Chain # - Action Chain
#################### ####################
def sync_prop(self, prop_name: str, _: bpy.types.Context): def sync_prop(self, prop_name: str, context: bpy.types.Context):
"""Called when a property has been updated.""" """Called when a property has been updated."""
if not hasattr(self, prop_name): if not hasattr(self, prop_name):
msg = f'Property {prop_name} not defined on socket {self}' msg = f'Property {prop_name} not defined on socket {self}'
@ -586,13 +589,6 @@ class MaxwellSimNode(bpy.types.Node):
Invalidates (recursively) the cache of any managed object or Invalidates (recursively) the cache of any managed object or
output socket method that implicitly depends on this input socket. output socket method that implicitly depends on this input socket.
""" """
#log.debug(
# 'Action "%s" Triggered in "%s" (socket_name="%s", prop_name="%s")',
# action,
# self.name,
# socket_name,
# prop_name,
#)
# Forwards Chains # Forwards Chains
if action == 'value_changed': if action == 'value_changed':
# Run User Callbacks # Run User Callbacks
@ -602,20 +598,20 @@ class MaxwellSimNode(bpy.types.Node):
if ( if (
( (
socket_name socket_name
and socket_name in method.extra_data['changed_sockets'] and socket_name
in method._extra_data.get('changed_sockets')
)
or (
prop_name
and prop_name
in method._extra_data.get('changed_props')
) )
or (prop_name and prop_name in method.extra_data['changed_props'])
or ( or (
socket_name socket_name
and method.extra_data['changed_loose_input'] and method._extra_data['changed_loose_input']
and socket_name in self.loose_input_sockets and socket_name in self.loose_input_sockets
) )
): ):
#log.debug(
# 'Running Value-Change Callback "%s" in "%s")',
# method.__name__,
# self.name,
#)
method(self) method(self)
# Propagate via Output Sockets # Propagate via Output Sockets
@ -639,15 +635,8 @@ class MaxwellSimNode(bpy.types.Node):
elif action == 'show_preview': elif action == 'show_preview':
# Run User Callbacks # Run User Callbacks
## "On Show Preview" callbacks are 'on_value_changed' callbacks... for method in self._on_show_preview:
## ...which simply hook into the 'preview_active' property. method(self)
## By (maybe) altering 'preview_active', callbacks run as needed.
if not self.preview_active:
log.info(
'Activating Preview in "%s")',
self.name,
)
self.preview_active = True
## Propagate via Input Sockets ## Propagate via Input Sockets
for bl_socket in self.active_bl_sockets('input'): for bl_socket in self.active_bl_sockets('input'):
@ -659,7 +648,7 @@ class MaxwellSimNode(bpy.types.Node):
## ...because they can stop propagation, they should go first. ## ...because they can stop propagation, they should go first.
for method in self._on_show_plot: for method in self._on_show_plot:
method(self) method(self)
if method.extra_data['stop_propagation']: if method._extra_data['stop_propagation']:
return return
## Propagate via Input Sockets ## Propagate via Input Sockets
@ -675,10 +664,13 @@ class MaxwellSimNode(bpy.types.Node):
Restricted to the MaxwellSimTreeType. Restricted to the MaxwellSimTreeType.
""" """
return node_tree.bl_idname == ct.TreeType.MaxwellSim.value return node_tree.bl_idname == ct.TreeType.MaxwellSim.value
def init(self, context: bpy.types.Context): def init(self, context: bpy.types.Context):
"""Run (by Blender) on node creation.""" """Run (by Blender) on node creation."""
global CACHE
# Initialize Cache and Instance ID # Initialize Cache and Instance ID
self.instance_id = str(uuid.uuid4()) self.instance_id = str(uuid.uuid4())
CACHE[self.instance_id] = {} CACHE[self.instance_id] = {}
@ -703,6 +695,7 @@ class MaxwellSimNode(bpy.types.Node):
def free(self) -> None: def free(self) -> None:
"""Run (by Blender) when deleting the node.""" """Run (by Blender) when deleting the node."""
global CACHE
if not CACHE.get(self.instance_id): if not CACHE.get(self.instance_id):
CACHE[self.instance_id] = {} CACHE[self.instance_id] = {}
node_tree = self.id_data node_tree = self.id_data
@ -732,3 +725,306 @@ class MaxwellSimNode(bpy.types.Node):
# Finally: Free Instance Cache # Finally: Free Instance Cache
if self.instance_id in CACHE: if self.instance_id in CACHE:
del CACHE[self.instance_id] del CACHE[self.instance_id]
def chain_event_decorator(
callback_type: typ.Literal[
'computes_output_socket',
'on_value_changed',
'on_show_preview',
'on_show_plot',
'on_init',
],
index_by: typ.Any | None = None,
extra_data: dict[str, typ.Any] | None = None,
kind: ct.DataFlowKind = ct.DataFlowKind.Value,
input_sockets: set[str] = set(), ## For now, presume
output_sockets: set[str] = set(), ## For now, presume
loose_input_sockets: bool = False,
loose_output_sockets: bool = False,
props: set[str] = set(),
managed_objs: set[str] = set(),
req_params: set[str] = set(),
):
def decorator(method: typ.Callable) -> typ.Callable:
# Check Function Signature Validity
func_sig = set(inspect.signature(method).parameters.keys())
## Too Little
if func_sig != req_params and func_sig.issubset(req_params):
msg = f'Decorated method {method.__name__} is missing arguments {req_params - func_sig}'
## Too Much
if func_sig != req_params and func_sig.issuperset(req_params):
msg = f'Decorated method {method.__name__} has superfluous arguments {func_sig - req_params}'
raise ValueError(msg)
## Just Right :)
# TODO: Check Function Annotation Validity
# - w/pydantic and/or socket capabilities
def decorated(node: MaxwellSimNode):
# Assemble Keyword Arguments
method_kw_args = {}
## Add Input Sockets
if input_sockets:
_input_sockets = {
input_socket_name: node._compute_input(
input_socket_name, kind
)
for input_socket_name in input_sockets
}
method_kw_args |= dict(input_sockets=_input_sockets)
## Add Output Sockets
if output_sockets:
_output_sockets = {
output_socket_name: node.compute_output(
output_socket_name, kind
)
for output_socket_name in output_sockets
}
method_kw_args |= dict(output_sockets=_output_sockets)
## Add Loose Sockets
if loose_input_sockets:
_loose_input_sockets = {
input_socket_name: node._compute_input(
input_socket_name, kind
)
for input_socket_name in node.loose_input_sockets
}
method_kw_args |= dict(
loose_input_sockets=_loose_input_sockets
)
if loose_output_sockets:
_loose_output_sockets = {
output_socket_name: node.compute_output(
output_socket_name, kind
)
for output_socket_name in node.loose_output_sockets
}
method_kw_args |= dict(
loose_output_sockets=_loose_output_sockets
)
## Add Props
if props:
_props = {
prop_name: getattr(node, prop_name) for prop_name in props
}
method_kw_args |= dict(props=_props)
## Add Managed Object
if managed_objs:
_managed_objs = {
managed_obj_name: node.managed_objs[managed_obj_name]
for managed_obj_name in managed_objs
}
method_kw_args |= dict(managed_objs=_managed_objs)
# Call Method
return method(
node,
**method_kw_args,
)
# Set Attributes for Discovery
decorated._callback_type = callback_type
if index_by:
decorated._index_by = index_by
if extra_data:
decorated._extra_data = extra_data
return decorated
return decorator
####################
# - Decorator: Output Socket
####################
def computes_output_socket(
output_socket_name: ct.SocketName,
kind: ct.DataFlowKind = ct.DataFlowKind.Value,
input_sockets: set[str] = set(),
props: set[str] = set(),
managed_objs: set[str] = set(),
cacheable: bool = True,
):
"""Given a socket name, defines a function-that-makes-a-function (aka.
decorator) which has the name of the socket attached.
Must be used as a decorator, ex. `@compute_output_socket("name")`.
Args:
output_socket_name: The name of the output socket to attach the
decorated method to.
input_sockets: The values of these input sockets will be computed
using `_compute_input`, then passed to the decorated function
as `input_sockets: list[Any]`. If the input socket doesn't exist (ex. is contained in an inactive loose socket or socket set), then None is returned.
managed_objs: These managed objects will be passed to the
function as `managed_objs: list[Any]`.
kind: Requests for this `output_socket_name, DataFlowKind` pair will
be returned by the decorated function.
cacheable: The output of th
be returned by the decorated function.
Returns:
The decorator, which takes the output-socket-computing method
and returns a new output-socket-computing method, now annotated
and discoverable by the `MaxwellSimTreeNode`.
"""
req_params = (
{'self'}
| ({'input_sockets'} if input_sockets else set())
| ({'props'} if props else set())
| ({'managed_objs'} if managed_objs else set())
)
return chain_event_decorator(
callback_type='computes_output_socket',
index_by=(output_socket_name, kind),
kind=kind,
input_sockets=input_sockets,
props=props,
managed_objs=managed_objs,
req_params=req_params,
)
####################
# - Decorator: On Show Preview
####################
def on_value_changed(
socket_name: set[ct.SocketName] | ct.SocketName | None = None,
prop_name: set[str] | str | None = None,
any_loose_input_socket: bool = False,
kind: ct.DataFlowKind = ct.DataFlowKind.Value,
input_sockets: set[str] = set(),
props: set[str] = set(),
managed_objs: set[str] = set(),
):
if (
sum(
[
int(socket_name is not None),
int(prop_name is not None),
int(any_loose_input_socket),
]
)
> 1
):
msg = 'Define only one of socket_name, prop_name or any_loose_input_socket'
raise ValueError(msg)
req_params = (
{'self'}
| ({'input_sockets'} if input_sockets else set())
| ({'loose_input_sockets'} if any_loose_input_socket else set())
| ({'props'} if props else set())
| ({'managed_objs'} if managed_objs else set())
)
return chain_event_decorator(
callback_type='on_value_changed',
extra_data={
'changed_sockets': (
socket_name if isinstance(socket_name, set) else {socket_name}
),
'changed_props': (
prop_name if isinstance(prop_name, set) else {prop_name}
),
'changed_loose_input': any_loose_input_socket,
},
kind=kind,
input_sockets=input_sockets,
loose_input_sockets=any_loose_input_socket,
props=props,
managed_objs=managed_objs,
req_params=req_params,
)
def on_show_preview(
kind: ct.DataFlowKind = ct.DataFlowKind.Value,
input_sockets: set[str] = set(), ## For now, presume only same kind
output_sockets: set[str] = set(), ## For now, presume only same kind
props: set[str] = set(),
managed_objs: set[str] = set(),
):
req_params = (
{'self'}
| ({'input_sockets'} if input_sockets else set())
| ({'output_sockets'} if output_sockets else set())
| ({'props'} if props else set())
| ({'managed_objs'} if managed_objs else set())
)
return chain_event_decorator(
callback_type='on_show_preview',
kind=kind,
input_sockets=input_sockets,
output_sockets=output_sockets,
props=props,
managed_objs=managed_objs,
req_params=req_params,
)
def on_show_plot(
kind: ct.DataFlowKind = ct.DataFlowKind.Value,
input_sockets: set[str] = set(),
output_sockets: set[str] = set(),
props: set[str] = set(),
managed_objs: set[str] = set(),
stop_propagation: bool = False,
):
req_params = (
{'self'}
| ({'input_sockets'} if input_sockets else set())
| ({'output_sockets'} if output_sockets else set())
| ({'props'} if props else set())
| ({'managed_objs'} if managed_objs else set())
)
return chain_event_decorator(
callback_type='on_show_plot',
extra_data={
'stop_propagation': stop_propagation,
},
kind=kind,
input_sockets=input_sockets,
output_sockets=output_sockets,
props=props,
managed_objs=managed_objs,
req_params=req_params,
)
def on_init(
kind: ct.DataFlowKind = ct.DataFlowKind.Value,
input_sockets: set[str] = set(),
output_sockets: set[str] = set(),
props: set[str] = set(),
managed_objs: set[str] = set(),
):
req_params = (
{'self'}
| ({'input_sockets'} if input_sockets else set())
| ({'output_sockets'} if output_sockets else set())
| ({'props'} if props else set())
| ({'managed_objs'} if managed_objs else set())
)
return chain_event_decorator(
callback_type='on_init',
kind=kind,
input_sockets=input_sockets,
output_sockets=output_sockets,
props=props,
managed_objs=managed_objs,
req_params=req_params,
)

View File

@ -1,4 +1,5 @@
from . import bound_box, bound_faces from . import bound_box
from . import bound_faces
BL_REGISTER = [ BL_REGISTER = [
*bound_box.BL_REGISTER, *bound_box.BL_REGISTER,

View File

@ -1,8 +1,10 @@
import tidy3d as td import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
from ... import contracts as ct from ... import contracts as ct
from ... import sockets from ... import sockets
from .. import base, events from .. import base
class BoundCondsNode(base.MaxwellSimNode): class BoundCondsNode(base.MaxwellSimNode):
@ -28,7 +30,7 @@ class BoundCondsNode(base.MaxwellSimNode):
#################### ####################
# - Output Socket Computation # - Output Socket Computation
#################### ####################
@events.computes_output_socket( @base.computes_output_socket(
'BCs', input_sockets={'+X', '-X', '+Y', '-Y', '+Z', '-Z'} 'BCs', input_sockets={'+X', '-X', '+Y', '-Y', '+Z', '-Z'}
) )
def compute_simulation(self, input_sockets) -> td.BoundarySpec: def compute_simulation(self, input_sockets) -> td.BoundarySpec:

View File

@ -1,11 +1,10 @@
from . import ( from . import pml_bound_face
absorbing_bound_face, from . import pec_bound_face
bloch_bound_face, from . import pmc_bound_face
pec_bound_face,
periodic_bound_face, from . import bloch_bound_face
pmc_bound_face, from . import periodic_bound_face
pml_bound_face, from . import absorbing_bound_face
)
BL_REGISTER = [ BL_REGISTER = [
*pml_bound_face.BL_REGISTER, *pml_bound_face.BL_REGISTER,

View File

@ -1,294 +0,0 @@
import enum
import inspect
import typing as typ
from types import MappingProxyType
from ....utils import extra_sympy_units as spux
from ....utils import logger
from .. import contracts as ct
from .base import MaxwellSimNode
log = logger.get(__name__)
UnitSystemID = str
UnitSystem = dict[ct.SocketType, typ.Any]
class EventCallbackType(enum.StrEnum):
"""Names of actions that support callbacks."""
computes_output_socket = enum.auto()
on_value_changed = enum.auto()
on_show_plot = enum.auto()
on_init = enum.auto()
####################
# - Event Callback Information
####################
class EventCallbackData_ComputesOutputSocket(typ.TypedDict): # noqa: N801
"""Extra data used to select a method to compute output sockets."""
output_socket_name: ct.SocketName
kind: ct.DataFlowKind
class EventCallbackData_OnValueChanged(typ.TypedDict): # noqa: N801
"""Extra data used to select a method to compute output sockets."""
changed_sockets: set[ct.SocketName]
changed_props: set[str]
changed_loose_input: set[str]
class EventCallbackData_OnShowPlot(typ.TypedDict): # noqa: N801
"""Extra data in the callback, used when showing a plot."""
stop_propagation: bool
class EventCallbackData_OnInit(typ.TypedDict): # noqa: D101, N801
pass
EventCallbackData: typ.TypeAlias = (
EventCallbackData_ComputesOutputSocket
| EventCallbackData_OnValueChanged
| EventCallbackData_OnShowPlot
| EventCallbackData_OnInit
)
####################
# - Event Decorator
####################
ManagedObjName: typ.TypeAlias = str
PropName: typ.TypeAlias = str
def event_decorator(
action_type: EventCallbackType,
extra_data: EventCallbackData,
kind: ct.DataFlowKind = ct.DataFlowKind.Value,
props: set[PropName] = frozenset(),
managed_objs: set[ManagedObjName] = frozenset(),
input_sockets: set[ct.SocketName] = frozenset(),
output_sockets: set[ct.SocketName] = frozenset(),
all_loose_input_sockets: bool = False,
all_loose_output_sockets: bool = False,
unit_systems: dict[UnitSystemID, UnitSystem] = MappingProxyType({}),
scale_input_sockets: dict[ct.SocketName, UnitSystemID] = MappingProxyType({}),
scale_output_sockets: dict[ct.SocketName, UnitSystemID] = MappingProxyType({}),
):
"""Returns a decorator for a method of `MaxwellSimNode`, declaring it as able respond to events passing through a node.
Parameters:
action_type: A name describing which event the decorator should respond to.
Set to `return_method.action_type`
extra_data: A dictionary that provides the caller with additional per-`action_type` information.
This might include parameters to help select the most appropriate method(s) to respond to an event with, or actions to take after running the callback.
kind: The `ct.DataFlowKind` used to compute all input and output socket data for methods with.
Only affects data passed to the decorated method; namely `input_sockets`, `output_sockets`, and their loose variants.
props: Set of `props` to compute, then pass to the decorated method.
managed_objs: Set of `managed_objs` to retrieve, then pass to the decorated method.
input_sockets: Set of `input_sockets` to compute, then pass to the decorated method.
output_sockets: Set of `output_sockets` to compute, then pass to the decorated method.
all_loose_input_sockets: Whether to compute all loose input sockets and pass them to the decorated method.
Used when the names of the loose input sockets are unknown, but all of their values are needed.
all_loose_output_sockets: Whether to compute all loose output sockets and pass them to the decorated method.
Used when the names of the loose output sockets are unknown, but all of their values are needed.
Returns:
A decorator, which can be applied to a method of `MaxwellSimNode`.
When a `MaxwellSimNode` subclass initializes, such a decorated method will be picked up on.
When the `action_type` action passes through the node, then `extra_data` is used to determine
"""
req_params = (
{'self'}
| ({'props'} if props else set())
| ({'managed_objs'} if managed_objs else set())
| ({'input_sockets'} if input_sockets else set())
| ({'output_sockets'} if output_sockets else set())
| ({'loose_input_sockets'} if all_loose_input_sockets else set())
| ({'loose_output_sockets'} if all_loose_output_sockets else set())
| ({'unit_systems'} if unit_systems else set())
)
# TODO: Check that all Unit System IDs referenced are also defined in 'unit_systems'.
## TODO: More ex. introspective checks and such, to make it really hard to write invalid methods.
def decorator(method: typ.Callable) -> typ.Callable:
# Check Function Signature Validity
func_sig = set(inspect.signature(method).parameters.keys())
## Too Few Arguments
if func_sig != req_params and func_sig.issubset(req_params):
msg = f'Decorated method {method.__name__} is missing arguments {req_params - func_sig}'
## Too Many Arguments
if func_sig != req_params and func_sig.issuperset(req_params):
msg = f'Decorated method {method.__name__} has superfluous arguments {func_sig - req_params}'
raise ValueError(msg)
# TODO: Check Function Annotation Validity
## - socket capabilities
def decorated(node: MaxwellSimNode):
method_kw_args = {} ## Keyword Arguments for Decorated Method
# Compute Requested Props
if props:
_props = {prop_name: getattr(node, prop_name) for prop_name in props}
method_kw_args |= {'props': _props}
# Retrieve Requested Managed Objects
if managed_objs:
_managed_objs = {
managed_obj_name: node.managed_objs[managed_obj_name]
for managed_obj_name in managed_objs
}
method_kw_args |= {'managed_objs': _managed_objs}
# Requested Sockets
## Compute Requested Input Sockets
if input_sockets:
_input_sockets = {
input_socket_name: node._compute_input(input_socket_name, kind)
for input_socket_name in input_sockets
}
# Scale Specified Input Sockets to Unit System
## First, scale the input socket value to the given unit system
## Then, convert the symbol-less sympy scalar to a python type.
for input_socket_name, unit_system_id in scale_input_sockets.items():
unit_system = unit_systems[unit_system_id]
_input_sockets[input_socket_name] = spux.sympy_to_python(
spux.scale_to_unit(
_input_sockets[input_socket_name],
unit_system[node.inputs[input_socket_name].socket_type],
)
)
method_kw_args |= {'input_sockets': _input_sockets}
## Compute Requested Output Sockets
if output_sockets:
_output_sockets = {
output_socket_name: node.compute_output(output_socket_name, kind)
for output_socket_name in output_sockets
}
# Scale Specified Output Sockets to Unit System
## First, scale the output socket value to the given unit system
## Then, convert the symbol-less sympy scalar to a python type.
for output_socket_name, unit_system_id in scale_output_sockets.items():
unit_system = unit_systems[unit_system_id]
_output_sockets[output_socket_name] = spux.sympy_to_python(
spux.scale_to_unit(
_output_sockets[output_socket_name],
unit_system[node.outputs[output_socket_name].socket_type],
)
)
method_kw_args |= {'output_sockets': _output_sockets}
# Loose Sockets
## Compute All Loose Input Sockets
if all_loose_input_sockets:
_loose_input_sockets = {
input_socket_name: node._compute_input(input_socket_name, kind)
for input_socket_name in node.loose_input_sockets
}
method_kw_args |= {'loose_input_sockets': _loose_input_sockets}
## Compute All Loose Output Sockets
if all_loose_output_sockets:
_loose_output_sockets = {
output_socket_name: node.compute_output(output_socket_name, kind)
for output_socket_name in node.loose_output_sockets
}
method_kw_args |= {'loose_output_sockets': _loose_output_sockets}
# Unit Systems
if unit_systems:
method_kw_args |= {'unit_systems': unit_systems}
# Call Method
return method(
node,
**method_kw_args,
)
# Set Decorated Attributes and Return
## Fix Introspection + Documentation
decorated.__name__ = method.__name__
decorated.__module__ = method.__module__
decorated.__qualname__ = method.__qualname__
decorated.__doc__ = method.__doc__
## Add Spice
decorated.action_type = action_type
decorated.extra_data = extra_data
return decorated
return decorator
####################
# - Simplified Event Callbacks
####################
def computes_output_socket(
output_socket_name: ct.SocketName,
kind: ct.DataFlowKind = ct.DataFlowKind.Value,
**kwargs,
):
return event_decorator(
action_type='computes_output_socket',
extra_data={
'output_socket_name': output_socket_name,
'kind': kind,
},
**kwargs,
)
## TODO: Consider changing socket_name and prop_name to more obvious names.
def on_value_changed(
socket_name: set[ct.SocketName] | ct.SocketName | None = None,
prop_name: set[str] | str | None = None,
any_loose_input_socket: bool = False,
**kwargs,
):
return event_decorator(
action_type=EventCallbackType.on_value_changed,
extra_data={
'changed_sockets': (
socket_name if isinstance(socket_name, set) else {socket_name}
),
'changed_props': (prop_name if isinstance(prop_name, set) else {prop_name}),
'changed_loose_input': any_loose_input_socket,
},
**kwargs,
)
def on_show_plot(
stop_propagation: bool = False,
**kwargs,
):
return event_decorator(
action_type=EventCallbackType.on_show_plot,
extra_data={
'stop_propagation': stop_propagation,
},
**kwargs,
)
def on_init(**kwargs):
return event_decorator(
action_type=EventCallbackType.on_init,
extra_data={},
**kwargs,
)

View File

@ -1,6 +1,8 @@
# from . import scientific_constant # from . import scientific_constant
from . import number_constant
# from . import physical_constant # from . import physical_constant
from . import blender_constant, number_constant from . import blender_constant
BL_REGISTER = [ BL_REGISTER = [
# *scientific_constant.BL_REGISTER, # *scientific_constant.BL_REGISTER,

View File

@ -2,14 +2,14 @@ import typing as typ
from .... import contracts as ct from .... import contracts as ct
from .... import sockets from .... import sockets
from ... import base, events from ... import base
class BlenderConstantNode(base.MaxwellSimNode): class BlenderConstantNode(base.MaxwellSimNode):
node_type = ct.NodeType.BlenderConstant node_type = ct.NodeType.BlenderConstant
bl_label = 'Blender Constant' bl_label = 'Blender Constant'
input_socket_sets: typ.ClassVar = { input_socket_sets = {
'Object': { 'Object': {
'Value': sockets.BlenderObjectSocketDef(), 'Value': sockets.BlenderObjectSocketDef(),
}, },
@ -31,7 +31,7 @@ class BlenderConstantNode(base.MaxwellSimNode):
#################### ####################
# - Callbacks # - Callbacks
#################### ####################
@events.computes_output_socket('Value', input_sockets={'Value'}) @base.computes_output_socket('Value', input_sockets={'Value'})
def compute_value(self, input_sockets) -> typ.Any: def compute_value(self, input_sockets) -> typ.Any:
return input_sockets['Value'] return input_sockets['Value']
@ -42,4 +42,6 @@ class BlenderConstantNode(base.MaxwellSimNode):
BL_REGISTER = [ BL_REGISTER = [
BlenderConstantNode, BlenderConstantNode,
] ]
BL_NODES = {ct.NodeType.BlenderConstant: (ct.NodeCategory.MAXWELLSIM_INPUTS_CONSTANTS)} BL_NODES = {
ct.NodeType.BlenderConstant: (ct.NodeCategory.MAXWELLSIM_INPUTS_CONSTANTS)
}

View File

@ -1,15 +1,18 @@
import typing as typ import typing as typ
import bpy
import sympy as sp
from .... import contracts as ct from .... import contracts as ct
from .... import sockets from .... import sockets
from ... import base, events from ... import base
class NumberConstantNode(base.MaxwellSimNode): class NumberConstantNode(base.MaxwellSimNode):
node_type = ct.NodeType.NumberConstant node_type = ct.NodeType.NumberConstant
bl_label = 'Numerical Constant' bl_label = 'Numerical Constant'
input_socket_sets: typ.ClassVar = { input_socket_sets = {
'Integer': { 'Integer': {
'Value': sockets.IntegerNumberSocketDef(), 'Value': sockets.IntegerNumberSocketDef(),
}, },
@ -28,7 +31,7 @@ class NumberConstantNode(base.MaxwellSimNode):
#################### ####################
# - Callbacks # - Callbacks
#################### ####################
@events.computes_output_socket('Value', input_sockets={'Value'}) @base.computes_output_socket('Value', input_sockets={'Value'})
def compute_value(self, input_sockets) -> typ.Any: def compute_value(self, input_sockets) -> typ.Any:
return input_sockets['Value'] return input_sockets['Value']
@ -39,4 +42,6 @@ class NumberConstantNode(base.MaxwellSimNode):
BL_REGISTER = [ BL_REGISTER = [
NumberConstantNode, NumberConstantNode,
] ]
BL_NODES = {ct.NodeType.NumberConstant: (ct.NodeCategory.MAXWELLSIM_INPUTS_CONSTANTS)} BL_NODES = {
ct.NodeType.NumberConstant: (ct.NodeCategory.MAXWELLSIM_INPUTS_CONSTANTS)
}

View File

@ -1,16 +1,19 @@
import typing as typ import bpy
import sympy as sp import sympy as sp
from .... import contracts, sockets from .... import contracts
from ... import base, events from .... import sockets
from ... import base
class PhysicalConstantNode(base.MaxwellSimTreeNode): class PhysicalConstantNode(base.MaxwellSimTreeNode):
node_type = contracts.NodeType.PhysicalConstant node_type = contracts.NodeType.PhysicalConstant
bl_label = 'Physical Constant'
input_socket_sets: typ.ClassVar = { bl_label = 'Physical Constant'
# bl_icon = constants.ICON_SIM_INPUT
input_sockets = {}
input_socket_sets = {
'time': { 'time': {
'value': sockets.PhysicalTimeSocketDef( 'value': sockets.PhysicalTimeSocketDef(
label='Time', label='Time',
@ -48,12 +51,13 @@ class PhysicalConstantNode(base.MaxwellSimTreeNode):
}, },
## I got bored so maybe the rest later ## I got bored so maybe the rest later
} }
output_socket_sets: typ.ClassVar = input_socket_sets output_sockets = {}
output_socket_sets = input_socket_sets
#################### ####################
# - Callbacks # - Callbacks
#################### ####################
@events.computes_output_socket('value') @base.computes_output_socket('value')
def compute_value(self: contracts.NodeTypeProtocol) -> sp.Expr: def compute_value(self: contracts.NodeTypeProtocol) -> sp.Expr:
return self.compute_input('value') return self.compute_input('value')

View File

@ -1,4 +1,3 @@
## TODO: Discover dropdown options from sci_constants.py
#################### ####################
# - Blender Registration # - Blender Registration
#################### ####################

View File

@ -1,7 +1,6 @@
from ... import contracts as ct from ... import contracts as ct
from ... import sockets from ... import sockets
from .. import base, events from .. import base
class PhysicalUnitSystemNode(base.MaxwellSimNode): class PhysicalUnitSystemNode(base.MaxwellSimNode):
node_type = ct.NodeType.UnitSystem node_type = ct.NodeType.UnitSystem
@ -19,7 +18,7 @@ class PhysicalUnitSystemNode(base.MaxwellSimNode):
#################### ####################
# - Callbacks # - Callbacks
#################### ####################
@events.computes_output_socket( @base.computes_output_socket(
'Unit System', 'Unit System',
input_sockets={'Unit System'}, input_sockets={'Unit System'},
) )

View File

@ -1,20 +1,22 @@
import typing as typ import bpy
import sympy as sp import sympy as sp
import sympy.physics.units as spu import sympy.physics.units as spu
import scipy as sc
from .....utils import extra_sympy_units as spux from .....utils import extra_sympy_units as spux
from .....utils import sci_constants as constants
from ... import contracts as ct from ... import contracts as ct
from ... import sockets from ... import sockets
from .. import base, events from .. import base
VAC_SPEED_OF_LIGHT = sc.constants.speed_of_light * spu.meter / spu.second
class WaveConstantNode(base.MaxwellSimNode): class WaveConstantNode(base.MaxwellSimNode):
node_type = ct.NodeType.WaveConstant node_type = ct.NodeType.WaveConstant
bl_label = 'Wave Constant' bl_label = 'Wave Constant'
input_socket_sets: typ.ClassVar = { input_socket_sets = {
# Single # Single
'Vacuum WL': { 'Vacuum WL': {
'WL': sockets.PhysicalLengthSocketDef( 'WL': sockets.PhysicalLengthSocketDef(
@ -42,68 +44,85 @@ class WaveConstantNode(base.MaxwellSimNode):
} }
#################### ####################
# - Event Methods: Listy Output # - Callbacks
#################### ####################
@events.computes_output_socket( @base.computes_output_socket(
'WL', 'WL',
input_sockets={'WL', 'Freq'}, input_sockets={'WL', 'Freq'},
) )
def compute_vac_wl(self, input_sockets: dict) -> sp.Expr: def compute_vac_wl(self, input_sockets: dict) -> sp.Expr:
if (vac_wl := input_sockets['WL']) is not None: if (vac_wl := input_sockets['WL']) is not None:
return vac_wl return vac_wl
if (freq := input_sockets['Freq']) is not None:
return constants.vac_speed_of_light / freq
msg = 'Vac WL and Freq are both None' elif (freq := input_sockets['Freq']) is not None:
raise RuntimeError(msg) return spu.convert_to(
VAC_SPEED_OF_LIGHT / freq,
spu.meter,
)
@events.computes_output_socket( raise RuntimeError('Vac WL and Freq are both None')
@base.computes_output_socket(
'Freq', 'Freq',
input_sockets={'WL', 'Freq'}, input_sockets={'WL', 'Freq'},
) )
def compute_freq(self, input_sockets: dict) -> sp.Expr: def compute_freq(self, input_sockets: dict) -> sp.Expr:
if (vac_wl := input_sockets['WL']) is not None: if (vac_wl := input_sockets['WL']) is not None:
return constants.vac_speed_of_light / vac_wl return spu.convert_to(
if (freq := input_sockets['Freq']) is not None: VAC_SPEED_OF_LIGHT / vac_wl,
spu.hertz,
)
elif (freq := input_sockets['Freq']) is not None:
return freq return freq
msg = 'Vac WL and Freq are both None' raise RuntimeError('Vac WL and Freq are both None')
raise RuntimeError(msg)
#################### ####################
# - Event Methods: Listy Output # - Listy Callbacks
#################### ####################
@events.computes_output_socket( @base.computes_output_socket(
'WLs', 'WLs',
input_sockets={'WLs', 'Freqs'}, input_sockets={'WLs', 'Freqs'},
) )
def compute_vac_wls(self, input_sockets: dict) -> sp.Expr: def compute_vac_wls(self, input_sockets: dict) -> sp.Expr:
if (vac_wls := input_sockets['WLs']) is not None: if (vac_wls := input_sockets['WLs']) is not None:
return vac_wls return vac_wls
if (freqs := input_sockets['Freqs']) is not None: elif (freqs := input_sockets['Freqs']) is not None:
return [constants.vac_speed_of_light / freq for freq in freqs][::-1] return [
spu.convert_to(
VAC_SPEED_OF_LIGHT / freq,
spu.meter,
)
for freq in freqs
][::-1]
msg = 'Vac WL and Freq are both None' raise RuntimeError('Vac WLs and Freqs are both None')
raise RuntimeError(msg)
@events.computes_output_socket( @base.computes_output_socket(
'Freqs', 'Freqs',
input_sockets={'WLs', 'Freqs'}, input_sockets={'WLs', 'Freqs'},
) )
def compute_freqs(self, input_sockets: dict) -> sp.Expr: def compute_freqs(self, input_sockets: dict) -> sp.Expr:
if (vac_wls := input_sockets['WLs']) is not None: if (vac_wls := input_sockets['WLs']) is not None:
return [constants.vac_speed_of_light / vac_wl for vac_wl in vac_wls][::-1] return [
if (freqs := input_sockets['Freqs']) is not None: spu.convert_to(
VAC_SPEED_OF_LIGHT / vac_wl,
spu.hertz,
)
for vac_wl in vac_wls
][::-1]
elif (freqs := input_sockets['Freqs']) is not None:
return freqs return freqs
msg = 'Vac WL and Freq are both None' raise RuntimeError('Vac WLs and Freqs are both None')
raise RuntimeError(msg)
#################### ####################
# - Event Methods # - Callbacks
#################### ####################
@events.on_value_changed(prop_name='active_socket_set', props={'active_socket_set'}) @base.on_value_changed(
def on_active_socket_set_changed(self, props: dict): prop_name='active_socket_set', props={'active_socket_set'}
)
def on_value_changed__active_socket_set(self, props: dict):
# Singular: Normal Output Sockets # Singular: Normal Output Sockets
if props['active_socket_set'] in {'Vacuum WL', 'Frequency'}: if props['active_socket_set'] in {'Vacuum WL', 'Frequency'}:
self.loose_output_sockets = {} self.loose_output_sockets = {}
@ -124,9 +143,9 @@ class WaveConstantNode(base.MaxwellSimNode):
msg = f"Active socket set invalid for wave constant: {props['active_socket_set']}" msg = f"Active socket set invalid for wave constant: {props['active_socket_set']}"
raise RuntimeError(msg) raise RuntimeError(msg)
@events.on_init() @base.on_init()
def on_init(self): def on_init(self):
self.on_active_socket_set_changed() self.on_value_changed__active_socket_set()
#################### ####################

View File

@ -1,20 +1,15 @@
import typing as typ import tempfile
from pathlib import Path from pathlib import Path
from ...... import info import tidy3d as td
import tidy3d.web as td_web
from ......services import tdcloud from ......services import tdcloud
from .... import contracts as ct from .... import contracts as ct
from .... import sockets from .... import sockets
from ... import base, events from ... import base
CACHE = {}
def _sim_data_cache_path(task_id: str) -> Path:
"""Compute an appropriate location for caching simulations downloaded from the internet, unique to each task ID.
Arguments:
task_id: The ID of the Tidy3D cloud task.
"""
return info.ADDON_CACHE / task_id / 'sim_data.hdf5'
#################### ####################
@ -22,44 +17,89 @@ def _sim_data_cache_path(task_id: str) -> Path:
#################### ####################
class Tidy3DWebImporterNode(base.MaxwellSimNode): class Tidy3DWebImporterNode(base.MaxwellSimNode):
node_type = ct.NodeType.Tidy3DWebImporter node_type = ct.NodeType.Tidy3DWebImporter
bl_label = 'Tidy3D Web Importer' bl_label = 'Tidy3DWebImporter'
input_sockets: typ.ClassVar = { input_sockets = {
'Cloud Task': sockets.Tidy3DCloudTaskSocketDef( 'Cloud Task': sockets.Tidy3DCloudTaskSocketDef(
should_exist=True, should_exist=True,
), ),
'Cache Path': sockets.FilePathSocketDef(
default_path=Path('loaded_simulation.hdf5')
),
} }
#################### ####################
# - Event Methods # - Output Methods
#################### ####################
@events.computes_output_socket( @base.computes_output_socket(
'FDTD Sim Data', 'FDTD Sim Data',
input_sockets={'Cloud Task', 'Cache Path'},
)
def compute_fdtd_sim_data(self, input_sockets: dict) -> str:
global CACHE
if not CACHE.get(self.instance_id):
CACHE[self.instance_id] = {'fdtd_sim_data': None}
if CACHE[self.instance_id]['fdtd_sim_data'] is not None:
return CACHE[self.instance_id]['fdtd_sim_data']
if not (
(cloud_task := input_sockets['Cloud Task']) is not None
and isinstance(cloud_task, tdcloud.CloudTask)
and cloud_task.status == 'success'
):
msg = "Won't attempt getting SimData"
raise RuntimeError(msg)
# Load the Simulation
cache_path = input_sockets['Cache Path']
if cache_path is None:
print('CACHE PATH IS NONE WHY')
return ## I guess?
if cache_path.is_file():
sim_data = td.SimulationData.from_file(str(cache_path))
else:
sim_data = td_web.api.webapi.load(
cloud_task.task_id,
path=str(cache_path),
)
CACHE[self.instance_id]['fdtd_sim_data'] = sim_data
return sim_data
@base.computes_output_socket(
'FDTD Sim',
input_sockets={'Cloud Task'}, input_sockets={'Cloud Task'},
) )
def compute_sim_data(self, input_sockets: dict) -> str: def compute_fdtd_sim(self, input_sockets: dict) -> str:
# Validate Task Availability if not isinstance(
if (cloud_task := input_sockets['Cloud Task']) is None: cloud_task := input_sockets['Cloud Task'], tdcloud.CloudTask
msg = f'"{self.bl_label}" CloudTask doesn\'t exist' ):
msg = 'Input cloud task does not exist'
raise RuntimeError(msg) raise RuntimeError(msg)
# Validate Task Existence # Load the Simulation
if not isinstance(cloud_task, tdcloud.CloudTask): with tempfile.NamedTemporaryFile(delete=False) as f:
msg = f'"{self.bl_label}" CloudTask input "{cloud_task}" has wrong "should_exists", as it isn\'t an instance of tdcloud.CloudTask' _path_tmp = Path(f.name)
raise TypeError(msg) _path_tmp.rename(f.name + '.json')
path_tmp = Path(f.name + '.json')
# Validate Task Status sim = td_web.api.webapi.load_simulation(
if cloud_task.status != 'success': cloud_task.task_id,
msg = f'"{self.bl_label}" CloudTask is "{cloud_task.status}", not "success"' path=str(path_tmp),
raise RuntimeError(msg) ) ## TODO: Don't use td_web directly. Only through tdcloud
Path(path_tmp).unlink()
# Download and Return SimData return sim
return tdcloud.TidyCloudTasks.download_task_sim_data(
cloud_task, _sim_data_cache_path(cloud_task.task_id) ####################
# - Update
####################
@base.on_value_changed(
socket_name='Cloud Task', input_sockets={'Cloud Task'}
) )
def on_value_changed__cloud_task(self, input_sockets: dict):
@events.on_value_changed(socket_name='Cloud Task', input_sockets={'Cloud Task'})
def on_cloud_task_changed(self, input_sockets: dict):
if ( if (
(cloud_task := input_sockets['Cloud Task']) is not None (cloud_task := input_sockets['Cloud Task']) is not None
and isinstance(cloud_task, tdcloud.CloudTask) and isinstance(cloud_task, tdcloud.CloudTask)
@ -67,13 +107,15 @@ class Tidy3DWebImporterNode(base.MaxwellSimNode):
): ):
self.loose_output_sockets = { self.loose_output_sockets = {
'FDTD Sim Data': sockets.MaxwellFDTDSimDataSocketDef(), 'FDTD Sim Data': sockets.MaxwellFDTDSimDataSocketDef(),
'FDTD Sim': sockets.MaxwellFDTDSimSocketDef(),
} }
else: return
self.loose_output_sockets = {} self.loose_output_sockets = {}
@events.on_init() @base.on_init()
def on_init(self): def on_init(self):
self.on_cloud_task_changed() self.on_value_changed__cloud_task()
#################### ####################
@ -83,5 +125,7 @@ BL_REGISTER = [
Tidy3DWebImporterNode, Tidy3DWebImporterNode,
] ]
BL_NODES = { BL_NODES = {
ct.NodeType.Tidy3DWebImporter: (ct.NodeCategory.MAXWELLSIM_INPUTS_IMPORTERS) ct.NodeType.Tidy3DWebImporter: (
ct.NodeCategory.MAXWELLSIM_INPUTS_IMPORTERS
)
} }

View File

@ -1,4 +1,8 @@
from pathlib import Path
import tidy3d as td
import sympy as sp import sympy as sp
import sympy.physics.units as spu
from .. import contracts as ct from .. import contracts as ct
from .. import sockets from .. import sockets

View File

@ -1,8 +1,10 @@
import sympy.physics.units as spu
import tidy3d as td import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
from ... import contracts, sockets from ... import contracts
from .. import base, events from ... import sockets
from .. import base
class DrudeLorentzMediumNode(base.MaxwellSimTreeNode): class DrudeLorentzMediumNode(base.MaxwellSimTreeNode):
@ -17,7 +19,7 @@ class DrudeLorentzMediumNode(base.MaxwellSimTreeNode):
input_sockets = ( input_sockets = (
{ {
'eps_inf': sockets.RealNumberSocketDef( 'eps_inf': sockets.RealNumberSocketDef(
label='εr_∞', label=f'εr_∞',
), ),
} }
| { | {
@ -46,11 +48,11 @@ class DrudeLorentzMediumNode(base.MaxwellSimTreeNode):
#################### ####################
# - Output Socket Computation # - Output Socket Computation
#################### ####################
@events.computes_output_socket('medium') @base.computes_output_socket('medium')
def compute_medium(self: contracts.NodeTypeProtocol) -> td.Sellmeier: def compute_medium(self: contracts.NodeTypeProtocol) -> td.Sellmeier:
## Retrieval ## Retrieval
return td.Lorentz( return td.Lorentz(
eps_inf=self.compute_input('eps_inf'), eps_inf=self.compute_input(f'eps_inf'),
coeffs=[ coeffs=[
( (
self.compute_input(f'del_eps{i}'), self.compute_input(f'del_eps{i}'),
@ -77,5 +79,7 @@ BL_REGISTER = [
DrudeLorentzMediumNode, DrudeLorentzMediumNode,
] ]
BL_NODES = { BL_NODES = {
contracts.NodeType.DrudeLorentzMedium: (contracts.NodeCategory.MAXWELLSIM_MEDIUMS) contracts.NodeType.DrudeLorentzMedium: (
contracts.NodeCategory.MAXWELLSIM_MEDIUMS
)
} }

View File

@ -1,15 +1,18 @@
import typing as typ import typing as typ
import functools
import bpy import bpy
import scipy as sc import tidy3d as td
import sympy as sp import sympy as sp
import sympy.physics.units as spu import sympy.physics.units as spu
import tidy3d as td import numpy as np
import scipy as sc
from .....utils import extra_sympy_units as spuex from .....utils import extra_sympy_units as spuex
from ... import contracts as ct from ... import contracts as ct
from ... import managed_objs, sockets from ... import sockets
from .. import base, events from ... import managed_objs
from .. import base
VAC_SPEED_OF_LIGHT = sc.constants.speed_of_light * spu.meter / spu.second VAC_SPEED_OF_LIGHT = sc.constants.speed_of_light * spu.meter / spu.second
@ -72,7 +75,9 @@ class LibraryMediumNode(base.MaxwellSimNode):
/ spuex.terahertz / spuex.terahertz
for val in mat.medium.frequency_range for val in mat.medium.frequency_range
] ]
return sp.pretty([freq_range[0].n(4), freq_range[1].n(4)], use_unicode=True) return sp.pretty(
[freq_range[0].n(4), freq_range[1].n(4)], use_unicode=True
)
@property @property
def nm_range_str(self) -> str: def nm_range_str(self) -> str:
@ -86,7 +91,9 @@ class LibraryMediumNode(base.MaxwellSimNode):
/ spu.nanometer / spu.nanometer
for val in reversed(mat.medium.frequency_range) for val in reversed(mat.medium.frequency_range)
] ]
return sp.pretty([nm_range[0].n(4), nm_range[1].n(4)], use_unicode=True) return sp.pretty(
[nm_range[0].n(4), nm_range[1].n(4)], use_unicode=True
)
#################### ####################
# - UI # - UI
@ -111,14 +118,14 @@ class LibraryMediumNode(base.MaxwellSimNode):
#################### ####################
# - Output Sockets # - Output Sockets
#################### ####################
@events.computes_output_socket('Medium') @base.computes_output_socket('Medium')
def compute_vac_wl(self) -> sp.Expr: def compute_vac_wl(self) -> sp.Expr:
return td.material_library[self.material].medium return td.material_library[self.material].medium
#################### ####################
# - Event Callbacks # - Event Callbacks
#################### ####################
@events.on_show_plot( @base.on_show_plot(
managed_objs={'nk_plot'}, managed_objs={'nk_plot'},
props={'material'}, props={'material'},
stop_propagation=True, ## Plot only the first plottable node stop_propagation=True, ## Plot only the first plottable node

View File

@ -1,9 +1,7 @@
from . import ( from . import add_non_linearity
add_non_linearity, from . import chi_3_susceptibility_non_linearity
chi_3_susceptibility_non_linearity, from . import kerr_non_linearity
kerr_non_linearity, from . import two_photon_absorption_non_linearity
two_photon_absorption_non_linearity,
)
BL_REGISTER = [ BL_REGISTER = [
*add_non_linearity.BL_REGISTER, *add_non_linearity.BL_REGISTER,

View File

@ -1,8 +1,10 @@
import sympy.physics.units as spu
import tidy3d as td import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
from ... import contracts, sockets from ... import contracts
from .. import base, events from ... import sockets
from .. import base
class TripleSellmeierMediumNode(base.MaxwellSimTreeNode): class TripleSellmeierMediumNode(base.MaxwellSimTreeNode):
@ -20,7 +22,9 @@ class TripleSellmeierMediumNode(base.MaxwellSimTreeNode):
) )
for i in [1, 2, 3] for i in [1, 2, 3]
} | { } | {
f'C{i}': sockets.PhysicalAreaSocketDef(label=f'C{i}', default_unit=spu.um**2) f'C{i}': sockets.PhysicalAreaSocketDef(
label=f'C{i}', default_unit=spu.um**2
)
for i in [1, 2, 3] for i in [1, 2, 3]
} }
output_sockets = { output_sockets = {
@ -60,7 +64,7 @@ class TripleSellmeierMediumNode(base.MaxwellSimTreeNode):
#################### ####################
# - Output Socket Computation # - Output Socket Computation
#################### ####################
@events.computes_output_socket('medium') @base.computes_output_socket('medium')
def compute_medium(self: contracts.NodeTypeProtocol) -> td.Sellmeier: def compute_medium(self: contracts.NodeTypeProtocol) -> td.Sellmeier:
## Retrieval ## Retrieval
# B1 = self.compute_input("B1") # B1 = self.compute_input("B1")

View File

@ -1,5 +1,5 @@
from . import eh_field_monitor, field_power_flux_monitor from . import eh_field_monitor
from . import field_power_flux_monitor
# from . import epsilon_tensor_monitor # from . import epsilon_tensor_monitor
# from . import diffraction_monitor # from . import diffraction_monitor

View File

@ -1,22 +1,24 @@
import typing as typ import typing as typ
import functools
import bpy
import tidy3d as td
import sympy as sp import sympy as sp
import sympy.physics.units as spu import sympy.physics.units as spu
import tidy3d as td import numpy as np
import scipy as sc
from .....assets.import_geonodes import GeoNodes, import_geonodes from .....utils import analyze_geonodes
from .....utils import extra_sympy_units as spux from .....utils import extra_sympy_units as spux
from .....utils import logger
from ... import contracts as ct from ... import contracts as ct
from ... import managed_objs, sockets from ... import sockets
from .. import base, events from ... import managed_objs
from .. import base
log = logger.get(__name__) GEONODES_MONITOR_BOX = 'monitor_box'
class EHFieldMonitorNode(base.MaxwellSimNode): class EHFieldMonitorNode(base.MaxwellSimNode):
"""Node providing for the monitoring of electromagnetic fields within a given planar region or volume."""
node_type = ct.NodeType.EHFieldMonitor node_type = ct.NodeType.EHFieldMonitor
bl_label = 'E/H Field Monitor' bl_label = 'E/H Field Monitor'
use_sim_node_name = True use_sim_node_name = True
@ -24,14 +26,14 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
#################### ####################
# - Sockets # - Sockets
#################### ####################
input_sockets: typ.ClassVar = { input_sockets = {
'Center': sockets.PhysicalPoint3DSocketDef(), 'Center': sockets.PhysicalPoint3DSocketDef(),
'Size': sockets.PhysicalSize3DSocketDef(), 'Size': sockets.PhysicalSize3DSocketDef(),
'Samples/Space': sockets.Integer3DVectorSocketDef( 'Samples/Space': sockets.Integer3DVectorSocketDef(
default_value=sp.Matrix([10, 10, 10]) default_value=sp.Matrix([10, 10, 10])
), ),
} }
input_socket_sets: typ.ClassVar = { input_socket_sets = {
'Freq Domain': { 'Freq Domain': {
'Freqs': sockets.PhysicalFreqSocketDef( 'Freqs': sockets.PhysicalFreqSocketDef(
is_list=True, is_list=True,
@ -39,31 +41,43 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
}, },
'Time Domain': { 'Time Domain': {
'Rec Start': sockets.PhysicalTimeSocketDef(), 'Rec Start': sockets.PhysicalTimeSocketDef(),
'Rec Stop': sockets.PhysicalTimeSocketDef(default_value=200 * spux.fs), 'Rec Stop': sockets.PhysicalTimeSocketDef(
default_value=200 * spux.fs
),
'Samples/Time': sockets.IntegerNumberSocketDef( 'Samples/Time': sockets.IntegerNumberSocketDef(
default_value=100, default_value=100,
), ),
}, },
} }
output_sockets: typ.ClassVar = { output_sockets = {
'Monitor': sockets.MaxwellMonitorSocketDef(), 'Monitor': sockets.MaxwellMonitorSocketDef(),
} }
managed_obj_defs: typ.ClassVar = { managed_obj_defs = {
'mesh': ct.schemas.ManagedObjDef( 'monitor_box': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLMesh(name), mk=lambda name: managed_objs.ManagedBLObject(name),
), name_prefix='',
'modifier': ct.schemas.ManagedObjDef( )
mk=lambda name: managed_objs.ManagedBLModifier(name),
),
} }
####################
# - Properties
####################
####################
# - UI
####################
def draw_props(self, context, layout):
pass
def draw_info(self, context, col):
pass
#################### ####################
# - Output Sockets # - Output Sockets
#################### ####################
@events.computes_output_socket( @base.computes_output_socket(
'Monitor', 'Monitor',
props={'active_socket_set', 'sim_node_name'},
input_sockets={ input_sockets={
'Rec Start', 'Rec Start',
'Rec Stop', 'Rec Stop',
@ -73,90 +87,107 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
'Samples/Time', 'Samples/Time',
'Freqs', 'Freqs',
}, },
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D}, props={'active_socket_set', 'sim_node_name'},
scale_input_sockets={
'Center': 'Tidy3DUnits',
'Size': 'Tidy3DUnits',
'Samples/Space': 'Tidy3DUnits',
'Rec Start': 'Tidy3DUnits',
'Rec Stop': 'Tidy3DUnits',
'Samples/Time': 'Tidy3DUnits',
},
) )
def compute_monitor( def compute_monitor(
self, input_sockets: dict, props: dict, unit_systems: dict, self, input_sockets: dict, props: dict
) -> td.FieldMonitor | td.FieldTimeMonitor: ) -> td.FieldTimeMonitor:
_center = input_sockets['Center']
_size = input_sockets['Size']
_samples_space = input_sockets['Samples/Space']
center = tuple(spu.convert_to(_center, spu.um) / spu.um)
size = tuple(spu.convert_to(_size, spu.um) / spu.um)
samples_space = tuple(_samples_space)
if props['active_socket_set'] == 'Freq Domain': if props['active_socket_set'] == 'Freq Domain':
freqs = input_sockets['Freqs'] freqs = input_sockets['Freqs']
log.info(
'Computing FieldMonitor (name="%s") with center="%s", size="%s"',
props['sim_node_name'],
input_sockets['Center'],
input_sockets['Size'],
)
return td.FieldMonitor( return td.FieldMonitor(
center=input_sockets['Center'], center=center,
size=input_sockets['Size'], size=size,
name=props['sim_node_name'], name=props['sim_node_name'],
interval_space=input_sockets['Samples/Space'], interval_space=samples_space,
freqs=[ freqs=[
float(spu.convert_to(freq, spu.hertz) / spu.hertz) for freq in freqs float(spu.convert_to(freq, spu.hertz) / spu.hertz)
for freq in freqs
], ],
) )
## Time Domain else: ## Time Domain
log.info( _rec_start = input_sockets['Rec Start']
'Computing FieldTimeMonitor (name=%s) with center=%s, size=%s', _rec_stop = input_sockets['Rec Stop']
props['sim_node_name'], samples_time = input_sockets['Samples/Time']
input_sockets['Center'],
input_sockets['Size'], rec_start = spu.convert_to(_rec_start, spu.second) / spu.second
) rec_stop = spu.convert_to(_rec_stop, spu.second) / spu.second
return td.FieldTimeMonitor( return td.FieldTimeMonitor(
center=input_sockets['Center'], center=center,
size=input_sockets['Size'], size=size,
name=props['sim_node_name'], name=props['sim_node_name'],
start=input_sockets['Rec Start'], start=rec_start,
stop=input_sockets['Rec Stop'], stop=rec_stop,
interval=input_sockets['Samples/Time'], interval=samples_time,
interval_space=input_sockets['Samples/Space'], interval_space=samples_space,
) )
#################### ####################
# - Preview - Changes to Input Sockets # - Preview - Changes to Input Sockets
#################### ####################
@events.on_value_changed( @base.on_value_changed(
socket_name={'Center', 'Size'}, socket_name={'Center', 'Size'},
prop_name='preview_active',
props={'preview_active'},
input_sockets={'Center', 'Size'}, input_sockets={'Center', 'Size'},
managed_objs={'mesh', 'modifier'}, managed_objs={'monitor_box'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={
'Center': 'BlenderUnits',
},
) )
def on_inputs_changed( def on_value_changed__center_size(
self, self,
props: dict,
managed_objs: dict[str, ct.schemas.ManagedObj],
input_sockets: dict, input_sockets: dict,
unit_systems: dict, managed_objs: dict[str, ct.schemas.ManagedObj],
): ):
# Push Input Values to GeoNodes Modifier _center = input_sockets['Center']
managed_objs['modifier'].bl_modifier( center = tuple(
managed_objs['mesh'].bl_object(location=input_sockets['Center']), [float(el) for el in spu.convert_to(_center, spu.um) / spu.um]
'NODES', )
{
'node_group': import_geonodes(GeoNodes.PrimitiveBox, 'link'), _size = input_sockets['Size']
'unit_system': unit_systems['BlenderUnits'], size = tuple(
'inputs': { [float(el) for el in spu.convert_to(_size, spu.um) / spu.um]
'Size': input_sockets['Size'], )
}, ## TODO: Preview unit system?? Presume um for now
# Retrieve Hard-Coded GeoNodes and Analyze Input
geo_nodes = bpy.data.node_groups[GEONODES_MONITOR_BOX]
geonodes_interface = analyze_geonodes.interface(
geo_nodes, direc='INPUT'
)
# Sync Modifier Inputs
managed_objs['monitor_box'].sync_geonodes_modifier(
geonodes_node_group=geo_nodes,
geonodes_identifier_to_value={
geonodes_interface['Size'].identifier: size,
## TODO: Use 'bl_socket_map.value_to_bl`!
## - This accounts for auto-conversion, unit systems, etc. .
## - We could keep it in the node base class...
## - ...But it needs aligning with Blender, too. Hmm.
}, },
) )
# Push Preview State
if props['preview_active']: # Sync Object Position
managed_objs['mesh'].show_preview() managed_objs['monitor_box'].bl_object('MESH').location = center
####################
# - Preview - Show Preview
####################
@base.on_show_preview(
managed_objs={'monitor_box'},
)
def on_show_preview(
self,
managed_objs: dict[str, ct.schemas.ManagedObj],
):
managed_objs['monitor_box'].show_preview('MESH')
self.on_value_changed__center_size()
#################### ####################

View File

@ -1,18 +1,21 @@
import typing as typ import typing as typ
import functools
import bpy import bpy
import tidy3d as td
import sympy as sp import sympy as sp
import sympy.physics.units as spu import sympy.physics.units as spu
import tidy3d as td import numpy as np
import scipy as sc
from .....assets.import_geonodes import GeoNodes, import_geonodes from .....utils import analyze_geonodes
from .....utils import extra_sympy_units as spux from .....utils import extra_sympy_units as spux
from .....utils import logger
from ... import contracts as ct from ... import contracts as ct
from ... import managed_objs, sockets from ... import sockets
from .. import base, events from ... import managed_objs
from .. import base
log = logger.get(__name__) GEONODES_MONITOR_BOX = 'monitor_flux_box'
class FieldPowerFluxMonitorNode(base.MaxwellSimNode): class FieldPowerFluxMonitorNode(base.MaxwellSimNode):
@ -23,7 +26,7 @@ class FieldPowerFluxMonitorNode(base.MaxwellSimNode):
#################### ####################
# - Sockets # - Sockets
#################### ####################
input_sockets: typ.ClassVar = { input_sockets = {
'Center': sockets.PhysicalPoint3DSocketDef(), 'Center': sockets.PhysicalPoint3DSocketDef(),
'Size': sockets.PhysicalSize3DSocketDef(), 'Size': sockets.PhysicalSize3DSocketDef(),
'Samples/Space': sockets.Integer3DVectorSocketDef( 'Samples/Space': sockets.Integer3DVectorSocketDef(
@ -31,7 +34,7 @@ class FieldPowerFluxMonitorNode(base.MaxwellSimNode):
), ),
'Direction': sockets.BoolSocketDef(), 'Direction': sockets.BoolSocketDef(),
} }
input_socket_sets: typ.ClassVar = { input_socket_sets = {
'Freq Domain': { 'Freq Domain': {
'Freqs': sockets.PhysicalFreqSocketDef( 'Freqs': sockets.PhysicalFreqSocketDef(
is_list=True, is_list=True,
@ -39,31 +42,43 @@ class FieldPowerFluxMonitorNode(base.MaxwellSimNode):
}, },
'Time Domain': { 'Time Domain': {
'Rec Start': sockets.PhysicalTimeSocketDef(), 'Rec Start': sockets.PhysicalTimeSocketDef(),
'Rec Stop': sockets.PhysicalTimeSocketDef(default_value=200 * spux.fs), 'Rec Stop': sockets.PhysicalTimeSocketDef(
default_value=200 * spux.fs
),
'Samples/Time': sockets.IntegerNumberSocketDef( 'Samples/Time': sockets.IntegerNumberSocketDef(
default_value=100, default_value=100,
), ),
}, },
} }
output_sockets: typ.ClassVar = { output_sockets = {
'Monitor': sockets.MaxwellMonitorSocketDef(), 'Monitor': sockets.MaxwellMonitorSocketDef(),
} }
managed_obj_defs: typ.ClassVar = { managed_obj_defs = {
'mesh': ct.schemas.ManagedObjDef( 'monitor_box': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLMesh(name), mk=lambda name: managed_objs.ManagedBLObject(name),
), name_prefix='',
'modifier': ct.schemas.ManagedObjDef( )
mk=lambda name: managed_objs.ManagedBLModifier(name),
),
} }
#################### ####################
# - Event Methods: Computation # - Properties
#################### ####################
@events.computes_output_socket(
####################
# - UI
####################
def draw_props(self, context, layout):
pass
def draw_info(self, context, col):
pass
####################
# - Output Sockets
####################
@base.computes_output_socket(
'Monitor', 'Monitor',
props={'active_socket_set', 'sim_node_name'},
input_sockets={ input_sockets={
'Rec Start', 'Rec Start',
'Rec Stop', 'Rec Stop',
@ -74,86 +89,113 @@ class FieldPowerFluxMonitorNode(base.MaxwellSimNode):
'Freqs', 'Freqs',
'Direction', 'Direction',
}, },
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D}, props={'active_socket_set', 'sim_node_name'},
scale_input_sockets={
'Center': 'Tidy3DUnits',
'Size': 'Tidy3DUnits',
'Samples/Space': 'Tidy3DUnits',
'Rec Start': 'Tidy3DUnits',
'Rec Stop': 'Tidy3DUnits',
'Samples/Time': 'Tidy3DUnits',
},
) )
def compute_monitor(self, input_sockets: dict, props: dict) -> td.FieldTimeMonitor: def compute_monitor(
self, input_sockets: dict, props: dict
) -> td.FieldTimeMonitor:
_center = input_sockets['Center']
_size = input_sockets['Size']
_samples_space = input_sockets['Samples/Space']
center = tuple(spu.convert_to(_center, spu.um) / spu.um)
size = tuple(spu.convert_to(_size, spu.um) / spu.um)
samples_space = tuple(_samples_space)
direction = '+' if input_sockets['Direction'] else '-' direction = '+' if input_sockets['Direction'] else '-'
if props['active_socket_set'] == 'Freq Domain': if props['active_socket_set'] == 'Freq Domain':
freqs = input_sockets['Freqs'] freqs = input_sockets['Freqs']
log.info(
'Computing FluxMonitor (name="%s") with center="%s", size="%s"',
props['sim_node_name'],
input_sockets['Center'],
input_sockets['Size'],
)
return td.FluxMonitor( return td.FluxMonitor(
center=input_sockets['Center'], center=center,
size=input_sockets['Size'], size=size,
name=props['sim_node_name'], name=props['sim_node_name'],
interval_space=input_sockets['Samples/Space'], interval_space=samples_space,
freqs=[ freqs=[
float(spu.convert_to(freq, spu.hertz) / spu.hertz) for freq in freqs float(spu.convert_to(freq, spu.hertz) / spu.hertz)
for freq in freqs
], ],
normal_dir=direction, normal_dir=direction,
) )
else: ## Time Domain
_rec_start = input_sockets['Rec Start']
_rec_stop = input_sockets['Rec Stop']
samples_time = input_sockets['Samples/Time']
return td.FluxTimeMonitor( rec_start = spu.convert_to(_rec_start, spu.second) / spu.second
center=input_sockets['Center'], rec_stop = spu.convert_to(_rec_stop, spu.second) / spu.second
size=input_sockets['Size'],
return td.FieldTimeMonitor(
center=center,
size=size,
name=props['sim_node_name'], name=props['sim_node_name'],
start=input_sockets['Rec Start'], start=rec_start,
stop=input_sockets['Rec Stop'], stop=rec_stop,
interval=input_sockets['Samples/Time'], interval=samples_time,
interval_space=input_sockets['Samples/Space'], interval_space=samples_space,
normal_dir=direction,
) )
#################### ####################
# - Preview - Changes to Input Sockets # - Preview - Changes to Input Sockets
#################### ####################
@events.on_value_changed( @base.on_value_changed(
socket_name={'Center', 'Size'}, socket_name={'Center', 'Size'},
prop_name='preview_active', input_sockets={'Center', 'Size', 'Direction'},
props={'preview_active'}, managed_objs={'monitor_box'},
input_sockets={'Center', 'Size'},
managed_objs={'mesh', 'modifier'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={
'Center': 'BlenderUnits',
},
) )
def on_inputs_changed( def on_value_changed__center_size(
self, self,
props: dict,
managed_objs: dict[str, ct.schemas.ManagedObj],
input_sockets: dict, input_sockets: dict,
unit_systems: dict, managed_objs: dict[str, ct.schemas.ManagedObj],
): ):
# Push Input Values to GeoNodes Modifier _center = input_sockets['Center']
managed_objs['modifier'].bl_modifier( center = tuple(
managed_objs['mesh'].bl_object(location=input_sockets['Center']), [float(el) for el in spu.convert_to(_center, spu.um) / spu.um]
'NODES', )
{
'node_group': import_geonodes(GeoNodes.PrimitiveBox, 'link'), _size = input_sockets['Size']
'unit_system': unit_systems['BlenderUnits'], size = tuple(
'inputs': { [float(el) for el in spu.convert_to(_size, spu.um) / spu.um]
'Size': input_sockets['Size'], )
}, ## TODO: Preview unit system?? Presume um for now
# Retrieve Hard-Coded GeoNodes and Analyze Input
geo_nodes = bpy.data.node_groups[GEONODES_MONITOR_BOX]
geonodes_interface = analyze_geonodes.interface(
geo_nodes, direc='INPUT'
)
# Sync Modifier Inputs
managed_objs['monitor_box'].sync_geonodes_modifier(
geonodes_node_group=geo_nodes,
geonodes_identifier_to_value={
geonodes_interface['Size'].identifier: size,
geonodes_interface['Direction'].identifier: input_sockets[
'Direction'
],
## TODO: Use 'bl_socket_map.value_to_bl`!
## - This accounts for auto-conversion, unit systems, etc. .
## - We could keep it in the node base class...
## - ...But it needs aligning with Blender, too. Hmm.
}, },
) )
# Push Preview State
if props['preview_active']: # Sync Object Position
managed_objs['mesh'].show_preview() managed_objs['monitor_box'].bl_object('MESH').location = center
####################
# - Preview - Show Preview
####################
@base.on_show_preview(
managed_objs={'monitor_box'},
)
def on_show_preview(
self,
managed_objs: dict[str, ct.schemas.ManagedObj],
):
managed_objs['monitor_box'].show_preview('MESH')
self.on_value_changed__center_size()
#################### ####################
@ -162,4 +204,6 @@ class FieldPowerFluxMonitorNode(base.MaxwellSimNode):
BL_REGISTER = [ BL_REGISTER = [
FieldPowerFluxMonitorNode, FieldPowerFluxMonitorNode,
] ]
BL_NODES = {ct.NodeType.FieldPowerFluxMonitor: (ct.NodeCategory.MAXWELLSIM_MONITORS)} BL_NODES = {
ct.NodeType.FieldPowerFluxMonitor: (ct.NodeCategory.MAXWELLSIM_MONITORS)
}

View File

@ -1,4 +1,5 @@
from . import exporters, viewer from . import viewer
from . import exporters
BL_REGISTER = [ BL_REGISTER = [
*viewer.BL_REGISTER, *viewer.BL_REGISTER,

View File

@ -1,4 +1,5 @@
from . import json_file_exporter, tidy3d_web_exporter from . import json_file_exporter
from . import tidy3d_web_exporter
BL_REGISTER = [ BL_REGISTER = [
*json_file_exporter.BL_REGISTER, *json_file_exporter.BL_REGISTER,

View File

@ -1,13 +1,15 @@
import json
import typing as typ import typing as typ
import json
from pathlib import Path from pathlib import Path
import bpy import bpy
import sympy as sp
import pydantic as pyd import pydantic as pyd
import tidy3d as td
from .... import contracts as ct from .... import contracts as ct
from .... import sockets from .... import sockets
from ... import base, events from ... import base
#################### ####################
@ -38,7 +40,9 @@ class JSONFileExporterNode(base.MaxwellSimNode):
input_sockets = { input_sockets = {
'Data': sockets.AnySocketDef(), 'Data': sockets.AnySocketDef(),
'JSON Path': sockets.FilePathSocketDef(default_path=Path('simulation.json')), 'JSON Path': sockets.FilePathSocketDef(
default_path=Path('simulation.json')
),
'JSON Indent': sockets.IntegerNumberSocketDef( 'JSON Indent': sockets.IntegerNumberSocketDef(
default_value=4, default_value=4,
), ),
@ -70,11 +74,13 @@ class JSONFileExporterNode(base.MaxwellSimNode):
#################### ####################
# - Output Sockets # - Output Sockets
#################### ####################
@events.computes_output_socket( @base.computes_output_socket(
'JSON String', 'JSON String',
input_sockets={'Data'}, input_sockets={'Data'},
) )
def compute_json_string(self, input_sockets: dict[str, typ.Any]) -> str | None: def compute_json_string(
self, input_sockets: dict[str, typ.Any]
) -> str | None:
if not (data := input_sockets['Data']): if not (data := input_sockets['Data']):
return None return None
@ -98,5 +104,7 @@ BL_REGISTER = [
JSONFileExporterNode, JSONFileExporterNode,
] ]
BL_NODES = { BL_NODES = {
ct.NodeType.JSONFileExporter: (ct.NodeCategory.MAXWELLSIM_OUTPUTS_EXPORTERS) ct.NodeType.JSONFileExporter: (
ct.NodeCategory.MAXWELLSIM_OUTPUTS_EXPORTERS
)
} }

View File

@ -3,7 +3,7 @@ import bpy
from ......services import tdcloud from ......services import tdcloud
from .... import contracts as ct from .... import contracts as ct
from .... import sockets from .... import sockets
from ... import base, events from ... import base
#################### ####################
@ -77,7 +77,9 @@ class ReloadTrackedTask(bpy.types.Operator):
def execute(self, context): def execute(self, context):
node = context.node node = context.node
if (cloud_task := tdcloud.TidyCloudTasks.task(node.tracked_task_id)) is None: if (
cloud_task := tdcloud.TidyCloudTasks.task(node.tracked_task_id)
) is None:
msg = "Tried to reload tracked task, but it doesn't exist" msg = "Tried to reload tracked task, but it doesn't exist"
raise RuntimeError(msg) raise RuntimeError(msg)
@ -103,9 +105,13 @@ class EstCostTrackedTask(bpy.types.Operator):
def execute(self, context): def execute(self, context):
node = context.node node = context.node
if ( if (
task_info := tdcloud.TidyCloudTasks.task_info(context.node.tracked_task_id) task_info := tdcloud.TidyCloudTasks.task_info(
context.node.tracked_task_id
)
) is None: ) is None:
msg = "Tried to estimate cost of tracked task, but it doesn't exist" msg = (
"Tried to estimate cost of tracked task, but it doesn't exist"
)
raise RuntimeError(msg) raise RuntimeError(msg)
node.cache_est_cost = task_info.cost_est() node.cache_est_cost = task_info.cost_est()
@ -229,13 +235,13 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
msg = 'Tried to upload simulation, but none is attached' msg = 'Tried to upload simulation, but none is attached'
raise ValueError(msg) raise ValueError(msg)
if (new_task := self._compute_input('Cloud Task')) is None or isinstance( if (
new_task := self._compute_input('Cloud Task')
) is None or isinstance(
new_task, new_task,
tdcloud.CloudTask, tdcloud.CloudTask,
): ):
msg = ( msg = 'Tried to upload simulation to new task, but existing task was selected'
'Tried to upload simulation to new task, but existing task was selected'
)
raise ValueError(msg) raise ValueError(msg)
# Create Cloud Task # Create Cloud Task
@ -256,7 +262,9 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
self.tracked_task_id = cloud_task.task_id self.tracked_task_id = cloud_task.task_id
def run_tracked_task(self): def run_tracked_task(self):
if (cloud_task := tdcloud.TidyCloudTasks.task(self.tracked_task_id)) is None: if (
cloud_task := tdcloud.TidyCloudTasks.task(self.tracked_task_id)
) is None:
msg = "Tried to run tracked task, but it doesn't exist" msg = "Tried to run tracked task, but it doesn't exist"
raise RuntimeError(msg) raise RuntimeError(msg)
@ -294,7 +302,9 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
def draw_info(self, context, layout): def draw_info(self, context, layout):
# Connection Info # Connection Info
auth_icon = 'CHECKBOX_HLT' if tdcloud.IS_AUTHENTICATED else 'CHECKBOX_DEHLT' auth_icon = (
'CHECKBOX_HLT' if tdcloud.IS_AUTHENTICATED else 'CHECKBOX_DEHLT'
)
conn_icon = 'CHECKBOX_HLT' if tdcloud.IS_ONLINE else 'CHECKBOX_DEHLT' conn_icon = 'CHECKBOX_HLT' if tdcloud.IS_ONLINE else 'CHECKBOX_DEHLT'
row = layout.row() row = layout.row()
@ -328,7 +338,9 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
## Split: Right Column ## Split: Right Column
col = split.column(align=False) col = split.column(align=False)
col.alignment = 'RIGHT' col.alignment = 'RIGHT'
col.label(text=f'{self.cache_total_monitor_data / 1_000_000:.2f}MB') col.label(
text=f'{self.cache_total_monitor_data / 1_000_000:.2f}MB'
)
# Cloud Task Info # Cloud Task Info
if self.tracked_task_id and tdcloud.IS_AUTHENTICATED: if self.tracked_task_id and tdcloud.IS_AUTHENTICATED:
@ -371,7 +383,9 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
## Split: Right Column ## Split: Right Column
cost_est = ( cost_est = (
f'{self.cache_est_cost:.2f}' if self.cache_est_cost >= 0 else 'TBD' f'{self.cache_est_cost:.2f}'
if self.cache_est_cost >= 0
else 'TBD'
) )
cost_real = ( cost_real = (
f'{task_info.cost_real:.2f}' f'{task_info.cost_real:.2f}'
@ -390,12 +404,16 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
#################### ####################
# - Output Methods # - Output Methods
#################### ####################
@events.computes_output_socket( @base.computes_output_socket(
'Cloud Task', 'Cloud Task',
input_sockets={'Cloud Task'}, input_sockets={'Cloud Task'},
) )
def compute_cloud_task(self, input_sockets: dict) -> tdcloud.CloudTask | None: def compute_cloud_task(
if isinstance(cloud_task := input_sockets['Cloud Task'], tdcloud.CloudTask): self, input_sockets: dict
) -> tdcloud.CloudTask | None:
if isinstance(
cloud_task := input_sockets['Cloud Task'], tdcloud.CloudTask
):
return cloud_task return cloud_task
return None return None
@ -403,7 +421,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
#################### ####################
# - Output Methods # - Output Methods
#################### ####################
@events.on_value_changed( @base.on_value_changed(
socket_name='FDTD Sim', socket_name='FDTD Sim',
input_sockets={'FDTD Sim'}, input_sockets={'FDTD Sim'},
) )
@ -428,5 +446,7 @@ BL_REGISTER = [
Tidy3DWebExporterNode, Tidy3DWebExporterNode,
] ]
BL_NODES = { BL_NODES = {
ct.NodeType.Tidy3DWebExporter: (ct.NodeCategory.MAXWELLSIM_OUTPUTS_EXPORTERS) ct.NodeType.Tidy3DWebExporter: (
ct.NodeCategory.MAXWELLSIM_OUTPUTS_EXPORTERS
)
} }

View File

@ -1,16 +1,17 @@
import functools
import typing as typ import typing as typ
import json
from pathlib import Path
import bpy import bpy
import sympy as sp import sympy as sp
import pydantic as pyd
import tidy3d as td
from .....utils import logger
from ... import contracts as ct from ... import contracts as ct
from ... import sockets from ... import sockets
from ...managed_objs.managed_bl_collection import preview_collection from .. import base
from .. import base, events from ...managed_objs import managed_bl_object
log = logger.get(__name__)
console = logger.OUTPUT_CONSOLE
class ConsoleViewOperator(bpy.types.Operator): class ConsoleViewOperator(bpy.types.Operator):
@ -48,7 +49,7 @@ class ViewerNode(base.MaxwellSimNode):
node_type = ct.NodeType.Viewer node_type = ct.NodeType.Viewer
bl_label = 'Viewer' bl_label = 'Viewer'
input_sockets: typ.ClassVar = { input_sockets = {
'Data': sockets.AnySocketDef(), 'Data': sockets.AnySocketDef(),
} }
@ -66,13 +67,9 @@ class ViewerNode(base.MaxwellSimNode):
name='Auto 3D Preview', name='Auto 3D Preview',
description="Whether to auto-preview anything 3D, that's plugged into the viewer node", description="Whether to auto-preview anything 3D, that's plugged into the viewer node",
default=False, default=False,
update=lambda self, context: self.sync_prop('auto_3d_preview', context), update=lambda self, context: self.sync_prop(
) 'auto_3d_preview', context
),
cache__data_was_unlinked: bpy.props.BoolProperty(
name='Data Was Unlinked',
description="Whether the Data input was unlinked last time it was checked.",
default=True,
) )
#################### ####################
@ -114,44 +111,51 @@ class ViewerNode(base.MaxwellSimNode):
return return
if isinstance(data, sp.Basic): if isinstance(data, sp.Basic):
console.print(sp.pretty(data, use_unicode=True)) sp.pprint(data, use_unicode=True)
else:
console.print(data) print(str(data))
#################### ####################
# - Event Methods # - Updates
#################### ####################
@events.on_value_changed( @base.on_value_changed(
socket_name='Data',
props={'auto_plot'},
)
def on_changed_2d_data(self, props):
# Show Plot
## Don't have to un-show other plots.
if self.inputs['Data'].is_linked and props['auto_plot']:
self.trigger_action('show_plot')
@events.on_value_changed(
socket_name='Data', socket_name='Data',
props={'auto_3d_preview'}, props={'auto_3d_preview'},
) )
def on_changed_3d_data(self, props): def on_value_changed__data(self, props):
# Data Not Attached # Show Plot
if not self.inputs['Data'].is_linked: ## Don't have to un-show other plots.
self.cache__data_was_unlinked = True if self.auto_plot:
self.trigger_action('show_plot')
# Data Just Attached # Remove Anything Previewed
elif self.cache__data_was_unlinked: preview_collection = managed_bl_object.bl_collection(
node_tree = self.id_data managed_bl_object.PREVIEW_COLLECTION_NAME,
view_layer_exclude=False,
)
for bl_object in preview_collection.objects.values():
preview_collection.objects.unlink(bl_object)
# Unpreview Everything # Preview Anything that Should be Previewed (maybe)
node_tree.unpreview_all() if props['auto_3d_preview']:
self.trigger_action('show_preview')
# Enable Previews in Tree
@base.on_value_changed(
prop_name='auto_3d_preview',
props={'auto_3d_preview'},
)
def on_value_changed__auto_3d_preview(self, props):
# Remove Anything Previewed
preview_collection = managed_bl_object.bl_collection(
managed_bl_object.PREVIEW_COLLECTION_NAME,
view_layer_exclude=False,
)
for bl_object in preview_collection.objects.values():
preview_collection.objects.unlink(bl_object)
# Preview Anything that Should be Previewed (maybe)
if props['auto_3d_preview']: if props['auto_3d_preview']:
log.info('Enabling 3D Previews from "%s"', self.name)
self.trigger_action('show_preview') self.trigger_action('show_preview')
self.cache__data_was_unlinked = False
#################### ####################

View File

@ -1,6 +1,9 @@
from . import sim_domain
# from . import sim_grid # from . import sim_grid
# from . import sim_grid_axes # from . import sim_grid_axes
from . import fdtd_sim, sim_domain
from . import fdtd_sim
BL_REGISTER = [ BL_REGISTER = [
*sim_domain.BL_REGISTER, *sim_domain.BL_REGISTER,

View File

@ -1,9 +1,10 @@
import sympy as sp
import tidy3d as td import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
from ... import contracts as ct from ... import contracts as ct
from ... import sockets from ... import sockets
from .. import base, events from .. import base
class FDTDSimNode(base.MaxwellSimNode): class FDTDSimNode(base.MaxwellSimNode):
@ -33,7 +34,7 @@ class FDTDSimNode(base.MaxwellSimNode):
#################### ####################
# - Output Socket Computation # - Output Socket Computation
#################### ####################
@events.computes_output_socket( @base.computes_output_socket(
'FDTD Sim', 'FDTD Sim',
kind=ct.DataFlowKind.Value, kind=ct.DataFlowKind.Value,
input_sockets={'Sources', 'Structures', 'Domain', 'BCs', 'Monitors'}, input_sockets={'Sources', 'Structures', 'Domain', 'BCs', 'Monitors'},

View File

@ -1,102 +1,124 @@
import typing as typ import bpy
import sympy as sp import sympy as sp
import sympy.physics.units as spu import sympy.physics.units as spu
import scipy as sc
from .....assets.import_geonodes import GeoNodes, import_geonodes from .....utils import analyze_geonodes
from ... import contracts as ct from ... import contracts as ct
from ... import managed_objs, sockets from ... import sockets
from .. import base, events from .. import base
from ... import managed_objs
GEONODES_DOMAIN_BOX = 'simdomain_box'
class SimDomainNode(base.MaxwellSimNode): class SimDomainNode(base.MaxwellSimNode):
node_type = ct.NodeType.SimDomain node_type = ct.NodeType.SimDomain
bl_label = 'Sim Domain' bl_label = 'Sim Domain'
use_sim_node_name = True
input_sockets: typ.ClassVar = { input_sockets = {
'Duration': sockets.PhysicalTimeSocketDef( 'Duration': sockets.PhysicalTimeSocketDef(
default_value=5 * spu.ps, default_value=5 * spu.ps,
default_unit=spu.ps, default_unit=spu.ps,
), ),
'Center': sockets.PhysicalPoint3DSocketDef(), 'Center': sockets.PhysicalSize3DSocketDef(),
'Size': sockets.PhysicalSize3DSocketDef(), 'Size': sockets.PhysicalSize3DSocketDef(),
'Grid': sockets.MaxwellSimGridSocketDef(), 'Grid': sockets.MaxwellSimGridSocketDef(),
'Ambient Medium': sockets.MaxwellMediumSocketDef(), 'Ambient Medium': sockets.MaxwellMediumSocketDef(),
} }
output_sockets: typ.ClassVar = { output_sockets = {
'Domain': sockets.MaxwellSimDomainSocketDef(), 'Domain': sockets.MaxwellSimDomainSocketDef(),
} }
managed_obj_defs: typ.ClassVar = { managed_obj_defs = {
'mesh': ct.schemas.ManagedObjDef( 'domain_box': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLMesh(name), mk=lambda name: managed_objs.ManagedBLObject(name),
name_prefix='', name_prefix='',
), )
'modifier': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLModifier(name),
),
} }
#################### ####################
# - Event Methods # - Callbacks
#################### ####################
@events.computes_output_socket( @base.computes_output_socket(
'Domain', 'Domain',
input_sockets={'Duration', 'Center', 'Size', 'Grid', 'Ambient Medium'}, input_sockets={'Duration', 'Center', 'Size', 'Grid', 'Ambient Medium'},
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
scale_input_sockets={
'Duration': 'Tidy3DUnits',
'Center': 'Tidy3DUnits',
'Size': 'Tidy3DUnits',
},
) )
def compute_output(self, input_sockets: dict) -> sp.Expr: def compute_sim_domain(self, input_sockets: dict) -> sp.Expr:
return { if all(
'run_time': input_sockets['Duration'], [
'center': input_sockets['Center'], (_duration := input_sockets['Duration']),
'size': input_sockets['Size'], (_center := input_sockets['Center']),
'grid_spec': input_sockets['Grid'], (_size := input_sockets['Size']),
'medium': input_sockets['Ambient Medium'], (grid := input_sockets['Grid']),
} (medium := input_sockets['Ambient Medium']),
]
@events.on_value_changed(
socket_name={'Center', 'Size'},
prop_name='preview_active',
props={'preview_active'},
input_sockets={'Center', 'Size'},
managed_objs={'mesh', 'modifier'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={
'Center': 'BlenderUnits',
},
)
def on_input_changed(
self,
props: dict,
managed_objs: dict[str, ct.schemas.ManagedObj],
input_sockets: dict,
unit_systems: dict,
): ):
# Push Input Values to GeoNodes Modifier duration = spu.convert_to(_duration, spu.second) / spu.second
managed_objs['modifier'].bl_modifier( center = tuple(spu.convert_to(_center, spu.um) / spu.um)
managed_objs['mesh'].bl_object(location=input_sockets['Center']), size = tuple(spu.convert_to(_size, spu.um) / spu.um)
'NODES', return dict(
{ run_time=duration,
'node_group': import_geonodes(GeoNodes.PrimitiveBox, 'link'), center=center,
'unit_system': unit_systems['BlenderUnits'], size=size,
'inputs': { grid_spec=grid,
'Size': input_sockets['Size'], medium=medium,
}, )
####################
# - Preview
####################
@base.on_value_changed(
socket_name={'Center', 'Size'},
input_sockets={'Center', 'Size'},
managed_objs={'domain_box'},
)
def on_value_changed__center_size(
self,
input_sockets: dict,
managed_objs: dict[str, ct.schemas.ManagedObj],
):
_center = input_sockets['Center']
center = tuple(
[float(el) for el in spu.convert_to(_center, spu.um) / spu.um]
)
_size = input_sockets['Size']
size = tuple(
[float(el) for el in spu.convert_to(_size, spu.um) / spu.um]
)
## TODO: Preview unit system?? Presume um for now
# Retrieve Hard-Coded GeoNodes and Analyze Input
geo_nodes = bpy.data.node_groups[GEONODES_DOMAIN_BOX]
geonodes_interface = analyze_geonodes.interface(
geo_nodes, direc='INPUT'
)
# Sync Modifier Inputs
managed_objs['domain_box'].sync_geonodes_modifier(
geonodes_node_group=geo_nodes,
geonodes_identifier_to_value={
geonodes_interface['Size'].identifier: size,
## TODO: Use 'bl_socket_map.value_to_bl`!
## - This accounts for auto-conversion, unit systems, etc. .
## - We could keep it in the node base class...
## - ...But it needs aligning with Blender, too. Hmm.
}, },
) )
# Push Preview State
if props['preview_active']:
managed_objs['mesh'].show_preview()
@events.on_init() # Sync Object Position
def on_init(self): managed_objs['domain_box'].bl_object('MESH').location = center
self.on_input_change()
@base.on_show_preview(
managed_objs={'domain_box'},
)
def on_show_preview(
self,
managed_objs: dict[str, ct.schemas.ManagedObj],
):
managed_objs['domain_box'].show_preview('MESH')
self.on_value_changed__center_size()
#################### ####################

View File

@ -1,9 +1,7 @@
from . import ( from . import automatic_sim_grid_axis
array_sim_grid_axis, from . import manual_sim_grid_axis
automatic_sim_grid_axis, from . import uniform_sim_grid_axis
manual_sim_grid_axis, from . import array_sim_grid_axis
uniform_sim_grid_axis,
)
BL_REGISTER = [ BL_REGISTER = [
*automatic_sim_grid_axis.BL_REGISTER, *automatic_sim_grid_axis.BL_REGISTER,

View File

@ -1,6 +1,9 @@
# from . import uniform_current_source from . import temporal_shapes
from . import plane_wave_source, point_dipole_source, temporal_shapes
from . import point_dipole_source
# from . import uniform_current_source
from . import plane_wave_source
# from . import gaussian_beam_source # from . import gaussian_beam_source
# from . import astigmatic_gaussian_beam_source # from . import astigmatic_gaussian_beam_source
# from . import tfsf_source # from . import tfsf_source

View File

@ -1,12 +1,19 @@
import typing_extensions as typx
import math import math
import typing as typ
import sympy as sp
import tidy3d as td import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
import bpy
from .....utils import analyze_geonodes
from ... import managed_objs
from ... import contracts as ct from ... import contracts as ct
from ... import managed_objs, sockets from ... import sockets
from .. import base, events from .. import base
GEONODES_PLANE_WAVE = 'source_plane_wave'
def convert_vector_to_spherical( def convert_vector_to_spherical(
@ -46,17 +53,19 @@ class PlaneWaveSourceNode(base.MaxwellSimNode):
#################### ####################
# - Sockets # - Sockets
#################### ####################
input_sockets: typ.ClassVar = { input_sockets = {
'Temporal Shape': sockets.MaxwellTemporalShapeSocketDef(), 'Temporal Shape': sockets.MaxwellTemporalShapeSocketDef(),
'Center': sockets.PhysicalPoint3DSocketDef(), 'Center': sockets.PhysicalPoint3DSocketDef(),
'Direction': sockets.Real3DVectorSocketDef(default_value=sp.Matrix([0, 0, -1])), 'Direction': sockets.Real3DVectorSocketDef(
default_value=sp.Matrix([0, 0, -1])
),
'Pol Angle': sockets.PhysicalAngleSocketDef(), 'Pol Angle': sockets.PhysicalAngleSocketDef(),
} }
output_sockets: typ.ClassVar = { output_sockets = {
'Source': sockets.MaxwellSourceSocketDef(), 'Source': sockets.MaxwellSourceSocketDef(),
} }
managed_obj_defs: typ.ClassVar = { managed_obj_defs = {
'plane_wave_source': ct.schemas.ManagedObjDef( 'plane_wave_source': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLObject(name), mk=lambda name: managed_objs.ManagedBLObject(name),
name_prefix='', name_prefix='',
@ -66,20 +75,18 @@ class PlaneWaveSourceNode(base.MaxwellSimNode):
#################### ####################
# - Output Socket Computation # - Output Socket Computation
#################### ####################
@events.computes_output_socket( @base.computes_output_socket(
'Source', 'Source',
input_sockets={'Temporal Shape', 'Center', 'Direction', 'Pol Angle'}, input_sockets={'Temporal Shape', 'Center', 'Direction', 'Pol Angle'},
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
scale_input_sockets={
'Center': 'Tidy3DUnits',
},
) )
def compute_source(self, input_sockets: dict): def compute_source(self, input_sockets: dict):
temporal_shape = input_sockets['Temporal Shape']
_center = input_sockets['Center']
direction = input_sockets['Direction'] direction = input_sockets['Direction']
pol_angle = input_sockets['Pol Angle'] pol_angle = input_sockets['Pol Angle']
injection_axis, dir_sgn, theta, phi = convert_vector_to_spherical( injection_axis, dir_sgn, theta, phi = convert_vector_to_spherical(
input_sockets['Direction'] direction
) )
size = { size = {
@ -87,11 +94,12 @@ class PlaneWaveSourceNode(base.MaxwellSimNode):
'y': (math.inf, 0, math.inf), 'y': (math.inf, 0, math.inf),
'z': (math.inf, math.inf, 0), 'z': (math.inf, math.inf, 0),
}[injection_axis] }[injection_axis]
center = tuple(spu.convert_to(_center, spu.um) / spu.um)
# Display the results # Display the results
return td.PlaneWave( return td.PlaneWave(
center=input_sockets['Center'], center=center,
source_time=input_sockets['Temporal Shape'], source_time=temporal_shape,
size=size, size=size,
direction=dir_sgn, direction=dir_sgn,
angle_theta=theta, angle_theta=theta,
@ -99,54 +107,58 @@ class PlaneWaveSourceNode(base.MaxwellSimNode):
pol_angle=pol_angle, pol_angle=pol_angle,
) )
##################### ####################
## - Preview # - Preview
##################### ####################
# @events.on_value_changed( @base.on_value_changed(
# socket_name={'Center', 'Direction'}, socket_name={'Center', 'Direction'},
# input_sockets={'Center', 'Direction'}, input_sockets={'Center', 'Direction'},
# managed_objs={'plane_wave_source'}, managed_objs={'plane_wave_source'},
# ) )
# def on_value_changed__center_direction( def on_value_changed__center_direction(
# self, self,
# input_sockets: dict, input_sockets: dict,
# managed_objs: dict[str, ct.schemas.ManagedObj], managed_objs: dict[str, ct.schemas.ManagedObj],
# ): ):
# _center = input_sockets['Center'] _center = input_sockets['Center']
# center = tuple([float(el) for el in spu.convert_to(_center, spu.um) / spu.um]) center = tuple(
[float(el) for el in spu.convert_to(_center, spu.um) / spu.um]
)
# _direction = input_sockets['Direction'] _direction = input_sockets['Direction']
# direction = tuple([float(el) for el in _direction]) direction = tuple([float(el) for el in _direction])
# ## TODO: Preview unit system?? Presume um for now ## TODO: Preview unit system?? Presume um for now
# # Retrieve Hard-Coded GeoNodes and Analyze Input # Retrieve Hard-Coded GeoNodes and Analyze Input
# geo_nodes = bpy.data.node_groups[GEONODES_PLANE_WAVE] geo_nodes = bpy.data.node_groups[GEONODES_PLANE_WAVE]
# geonodes_interface = analyze_geonodes.interface(geo_nodes, direc='INPUT') geonodes_interface = analyze_geonodes.interface(
geo_nodes, direc='INPUT'
)
# # Sync Modifier Inputs # Sync Modifier Inputs
# managed_objs['plane_wave_source'].sync_geonodes_modifier( managed_objs['plane_wave_source'].sync_geonodes_modifier(
# geonodes_node_group=geo_nodes, geonodes_node_group=geo_nodes,
# geonodes_identifier_to_value={ geonodes_identifier_to_value={
# geonodes_interface['Direction'].identifier: direction, geonodes_interface['Direction'].identifier: direction,
# ## TODO: Use 'bl_socket_map.value_to_bl`! ## TODO: Use 'bl_socket_map.value_to_bl`!
# ## - This accounts for auto-conversion, unit systems, etc. . ## - This accounts for auto-conversion, unit systems, etc. .
# ## - We could keep it in the node base class... ## - We could keep it in the node base class...
# ## - ...But it needs aligning with Blender, too. Hmm. ## - ...But it needs aligning with Blender, too. Hmm.
# }, },
# ) )
# # Sync Object Position # Sync Object Position
# managed_objs['plane_wave_source'].bl_object('MESH').location = center managed_objs['plane_wave_source'].bl_object('MESH').location = center
# @events.on_show_preview( @base.on_show_preview(
# managed_objs={'plane_wave_source'}, managed_objs={'plane_wave_source'},
# ) )
# def on_show_preview( def on_show_preview(
# self, self,
# managed_objs: dict[str, ct.schemas.ManagedObj], managed_objs: dict[str, ct.schemas.ManagedObj],
# ): ):
# managed_objs['plane_wave_source'].show_preview('MESH') managed_objs['plane_wave_source'].show_preview('MESH')
# self.on_value_changed__center_direction() self.on_value_changed__center_direction()
#################### ####################

View File

@ -1,12 +1,14 @@
import typing as typ import typing as typ
import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
import bpy import bpy
import sympy.physics.units as spu
import tidy3d as td
from ... import contracts as ct from ... import contracts as ct
from ... import managed_objs, sockets from ... import sockets
from .. import base, events from .. import base
from ... import managed_objs
class PointDipoleSourceNode(base.MaxwellSimNode): class PointDipoleSourceNode(base.MaxwellSimNode):
@ -64,14 +66,10 @@ class PointDipoleSourceNode(base.MaxwellSimNode):
#################### ####################
# - Output Socket Computation # - Output Socket Computation
#################### ####################
@events.computes_output_socket( @base.computes_output_socket(
'Source', 'Source',
input_sockets={'Temporal Shape', 'Center', 'Interpolate'}, input_sockets={'Temporal Shape', 'Center', 'Interpolate'},
props={'pol_axis'}, props={'pol_axis'},
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
scale_input_sockets={
'Center': 'Tidy3DUnits',
},
) )
def compute_source( def compute_source(
self, input_sockets: dict[str, typ.Any], props: dict[str, typ.Any] self, input_sockets: dict[str, typ.Any], props: dict[str, typ.Any]
@ -82,46 +80,55 @@ class PointDipoleSourceNode(base.MaxwellSimNode):
'EZ': 'Ez', 'EZ': 'Ez',
}[props['pol_axis']] }[props['pol_axis']]
return td.PointDipole( temporal_shape = input_sockets['Temporal Shape']
center=input_sockets['Center'], _center = input_sockets['Center']
source_time=input_sockets['Temporal Shape'], interpolate = input_sockets['Interpolate']
interpolate=input_sockets['Interpolate'],
center = tuple(spu.convert_to(_center, spu.um) / spu.um)
_res = td.PointDipole(
center=center,
source_time=temporal_shape,
interpolate=interpolate,
polarization=pol_axis, polarization=pol_axis,
) )
return _res
##################### ####################
## - Preview # - Preview
##################### ####################
# @events.on_value_changed( @base.on_value_changed(
# socket_name='Center', socket_name='Center',
# input_sockets={'Center'}, input_sockets={'Center'},
# managed_objs={'sphere_empty'}, managed_objs={'sphere_empty'},
# ) )
# def on_value_changed__center( def on_value_changed__center(
# self, self,
# input_sockets: dict, input_sockets: dict,
# managed_objs: dict[str, ct.schemas.ManagedObj], managed_objs: dict[str, ct.schemas.ManagedObj],
# ): ):
# _center = input_sockets['Center'] _center = input_sockets['Center']
# center = tuple(spu.convert_to(_center, spu.um) / spu.um) center = tuple(spu.convert_to(_center, spu.um) / spu.um)
# ## TODO: Preview unit system?? Presume um for now ## TODO: Preview unit system?? Presume um for now
# mobj = managed_objs['sphere_empty'] mobj = managed_objs['sphere_empty']
# bl_object = mobj.bl_object('EMPTY') bl_object = mobj.bl_object('EMPTY')
# bl_object.location = center # tuple([float(el) for el in center]) bl_object.location = center # tuple([float(el) for el in center])
# @events.on_show_preview( @base.on_show_preview(
# managed_objs={'sphere_empty'}, managed_objs={'sphere_empty'},
# ) )
# def on_show_preview( def on_show_preview(
# self, self,
# managed_objs: dict[str, ct.schemas.ManagedObj], managed_objs: dict[str, ct.schemas.ManagedObj],
# ): ):
# managed_objs['sphere_empty'].show_preview( managed_objs['sphere_empty'].show_preview(
# 'EMPTY', 'EMPTY',
# empty_display_type='SPHERE', empty_display_type='SPHERE',
# ) )
# managed_objs['sphere_empty'].bl_object('EMPTY').empty_display_size = 0.2 managed_objs['sphere_empty'].bl_object(
'EMPTY'
).empty_display_size = 0.2
#################### ####################
@ -130,4 +137,6 @@ class PointDipoleSourceNode(base.MaxwellSimNode):
BL_REGISTER = [ BL_REGISTER = [
PointDipoleSourceNode, PointDipoleSourceNode,
] ]
BL_NODES = {ct.NodeType.PointDipoleSource: (ct.NodeCategory.MAXWELLSIM_SOURCES)} BL_NODES = {
ct.NodeType.PointDipoleSource: (ct.NodeCategory.MAXWELLSIM_SOURCES)
}

View File

@ -1,5 +1,4 @@
from . import gaussian_pulse_temporal_shape from . import gaussian_pulse_temporal_shape
# from . import continuous_wave_temporal_shape # from . import continuous_wave_temporal_shape
# from . import array_temporal_shape # from . import array_temporal_shape

View File

@ -1,8 +1,10 @@
import sympy.physics.units as spu
import tidy3d as td import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
from .... import contracts, sockets from .... import contracts
from ... import base, events from .... import sockets
from ... import base
class ContinuousWaveTemporalShapeNode(base.MaxwellSimTreeNode): class ContinuousWaveTemporalShapeNode(base.MaxwellSimTreeNode):
@ -41,7 +43,7 @@ class ContinuousWaveTemporalShapeNode(base.MaxwellSimTreeNode):
#################### ####################
# - Output Socket Computation # - Output Socket Computation
#################### ####################
@events.computes_output_socket('temporal_shape') @base.computes_output_socket('temporal_shape')
def compute_source(self: contracts.NodeTypeProtocol) -> td.PointDipole: def compute_source(self: contracts.NodeTypeProtocol) -> td.PointDipole:
_phase = self.compute_input('phase') _phase = self.compute_input('phase')
_freq_center = self.compute_input('freq_center') _freq_center = self.compute_input('freq_center')

View File

@ -1,14 +1,17 @@
import typing as typ import typing as typ
import bpy
import numpy as np
import sympy.physics.units as spu
import tidy3d as td import tidy3d as td
import numpy as np
import sympy as sp
import sympy.physics.units as spu
import bpy
from ......utils import extra_sympy_units as spuex from ......utils import extra_sympy_units as spuex
from .... import contracts as ct from .... import contracts as ct
from .... import managed_objs, sockets from .... import sockets
from ... import base, events from .... import managed_objs
from ... import base
class GaussianPulseTemporalShapeNode(base.MaxwellSimNode): class GaussianPulseTemporalShapeNode(base.MaxwellSimNode):
@ -55,13 +58,17 @@ class GaussianPulseTemporalShapeNode(base.MaxwellSimNode):
name='Plot Time Start (ps)', name='Plot Time Start (ps)',
description='The instance ID of a particular MaxwellSimNode instance, used to index caches', description='The instance ID of a particular MaxwellSimNode instance, used to index caches',
default=0.0, default=0.0,
update=(lambda self, context: self.sync_prop('plot_time_start', context)), update=(
lambda self, context: self.sync_prop('plot_time_start', context)
),
) )
plot_time_end: bpy.props.FloatProperty( plot_time_end: bpy.props.FloatProperty(
name='Plot Time End (ps)', name='Plot Time End (ps)',
description='The instance ID of a particular MaxwellSimNode instance, used to index caches', description='The instance ID of a particular MaxwellSimNode instance, used to index caches',
default=5, default=5,
update=(lambda self, context: self.sync_prop('plot_time_start', context)), update=(
lambda self, context: self.sync_prop('plot_time_start', context)
),
) )
#################### ####################
@ -81,7 +88,7 @@ class GaussianPulseTemporalShapeNode(base.MaxwellSimNode):
#################### ####################
# - Output Socket Computation # - Output Socket Computation
#################### ####################
@events.computes_output_socket( @base.computes_output_socket(
'Temporal Shape', 'Temporal Shape',
input_sockets={ input_sockets={
'Freq Center', 'Freq Center',
@ -96,7 +103,8 @@ class GaussianPulseTemporalShapeNode(base.MaxwellSimNode):
(_freq_center := input_sockets['Freq Center']) is None (_freq_center := input_sockets['Freq Center']) is None
or (_freq_std := input_sockets['Freq Std.']) is None or (_freq_std := input_sockets['Freq Std.']) is None
or (_phase := input_sockets['Phase']) is None or (_phase := input_sockets['Phase']) is None
or (time_delay_rel_ang_freq := input_sockets['Delay rel. AngFreq']) is None or (time_delay_rel_ang_freq := input_sockets['Delay rel. AngFreq'])
is None
or (remove_dc_component := input_sockets['Remove DC']) is None or (remove_dc_component := input_sockets['Remove DC']) is None
): ):
raise ValueError('Inputs not defined') raise ValueError('Inputs not defined')
@ -115,7 +123,7 @@ class GaussianPulseTemporalShapeNode(base.MaxwellSimNode):
remove_dc_component=remove_dc_component, remove_dc_component=remove_dc_component,
) )
@events.on_show_plot( @base.on_show_plot(
managed_objs={'amp_time'}, managed_objs={'amp_time'},
props={'plot_time_start', 'plot_time_end'}, props={'plot_time_start', 'plot_time_end'},
output_sockets={'Temporal Shape'}, output_sockets={'Temporal Shape'},

View File

@ -1,5 +1,7 @@
# from . import object_structure # from . import object_structure
from . import geonodes_structure, primitives from . import geonodes_structure
from . import primitives
BL_REGISTER = [ BL_REGISTER = [
# *object_structure.BL_REGISTER, # *object_structure.BL_REGISTER,

View File

@ -1,65 +1,69 @@
import typing as typ import typing as typ
import tidy3d as td import tidy3d as td
import numpy as np
import sympy as sp
import sympy.physics.units as spu
from .....utils import analyze_geonodes, logger import bpy
from ... import bl_socket_map, managed_objs, sockets from bpy_types import bpy_types
import bmesh
from .....utils import analyze_geonodes
from ... import bl_socket_map
from ... import contracts as ct from ... import contracts as ct
from .. import base, events from ... import sockets
from .. import base
log = logger.get(__name__) from ... import managed_objs
class GeoNodesStructureNode(base.MaxwellSimNode): class GeoNodesStructureNode(base.MaxwellSimNode):
node_type = ct.NodeType.GeoNodesStructure node_type = ct.NodeType.GeoNodesStructure
bl_label = 'GeoNodes Structure' bl_label = 'GeoNodes Structure'
use_sim_node_name = True
#################### ####################
# - Sockets # - Sockets
#################### ####################
input_sockets: typ.ClassVar = { input_sockets = {
'Unit System': sockets.PhysicalUnitSystemSocketDef(),
'Medium': sockets.MaxwellMediumSocketDef(), 'Medium': sockets.MaxwellMediumSocketDef(),
'Center': sockets.PhysicalPoint3DSocketDef(),
'GeoNodes': sockets.BlenderGeoNodesSocketDef(), 'GeoNodes': sockets.BlenderGeoNodesSocketDef(),
} }
output_sockets: typ.ClassVar = { output_sockets = {
'Structure': sockets.MaxwellStructureSocketDef(), 'Structure': sockets.MaxwellStructureSocketDef(),
} }
managed_obj_defs: typ.ClassVar = { managed_obj_defs = {
'mesh': ct.schemas.ManagedObjDef( 'geometry': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLMesh(name), mk=lambda name: managed_objs.ManagedBLObject(name),
), name_prefix='',
'modifier': ct.schemas.ManagedObjDef( )
mk=lambda name: managed_objs.ManagedBLModifier(name),
),
} }
#################### ####################
# - Event Methods # - Output Socket Computation
#################### ####################
@events.computes_output_socket( @base.computes_output_socket(
'Structure', 'Structure',
input_sockets={'Medium'}, input_sockets={'Medium'},
managed_objs={'geometry'}, managed_objs={'geometry'},
) )
def compute_output( def compute_structure(
self, self,
input_sockets: dict[str, typ.Any], input_sockets: dict[str, typ.Any],
managed_objs: dict[str, ct.schemas.ManagedObj], managed_objs: dict[str, ct.schemas.ManagedObj],
) -> td.Structure: ) -> td.Structure:
# Simulate Input Value Change # Extract the Managed Blender Object
## This ensures that the mesh has been re-computed. mobj = managed_objs['geometry']
self.on_input_changed()
## TODO: mesh_as_arrays might not take the Center into account. # Extract Geometry as Arrays
## - Alternatively, Tidy3D might have a way to transform? geometry_as_arrays = mobj.mesh_as_arrays
mesh_as_arrays = managed_objs['mesh'].mesh_as_arrays
# Return TriMesh Structure
return td.Structure( return td.Structure(
geometry=td.TriangleMesh.from_vertices_faces( geometry=td.TriangleMesh.from_vertices_faces(
mesh_as_arrays['verts'], geometry_as_arrays['verts'],
mesh_as_arrays['faces'], geometry_as_arrays['faces'],
), ),
medium=input_sockets['Medium'], medium=input_sockets['Medium'],
) )
@ -67,96 +71,112 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
#################### ####################
# - Event Methods # - Event Methods
#################### ####################
@events.on_value_changed( @base.on_value_changed(
socket_name='GeoNodes', socket_name='GeoNodes',
prop_name='preview_active', managed_objs={'geometry'},
any_loose_input_socket=True, input_sockets={'GeoNodes'},
props={'preview_active'},
managed_objs={'mesh', 'modifier'},
input_sockets={'Center', 'GeoNodes'},
all_loose_input_sockets=True,
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
) )
def on_input_changed( def on_value_changed__geonodes(
self, self,
props: dict,
managed_objs: dict[str, ct.schemas.ManagedObj], managed_objs: dict[str, ct.schemas.ManagedObj],
input_sockets: dict, input_sockets: dict[str, typ.Any],
loose_input_sockets: dict,
unit_systems: dict,
) -> None: ) -> None:
# No GeoNodes: Remove Modifier (if any) """Called whenever the GeoNodes socket is changed.
if (geonodes := input_sockets['GeoNodes']) is None:
if (
managed_objs['modifier'].name
in managed_objs['mesh'].bl_object().modifiers
):
log.info(
'Removing Modifier "%s" from BLObject "%s"',
managed_objs['modifier'].name,
managed_objs['mesh'].name,
)
managed_objs['mesh'].bl_object().modifiers.remove(
managed_objs['modifier'].name
)
# Reset Loose Input Sockets Refreshes the Loose Input Sockets, which map directly to the GeoNodes tree input sockets.
"""
if not (geo_nodes := input_sockets['GeoNodes']):
managed_objs['geometry'].free()
self.loose_input_sockets = {} self.loose_input_sockets = {}
return return
# No Loose Input Sockets: Create from GeoNodes Interface # Analyze GeoNodes
## TODO: Other reasons to trigger re-filling loose_input_sockets. ## Extract Valid Inputs (via GeoNodes Tree "Interface")
if not loose_input_sockets:
# Retrieve the GeoNodes Interface
geonodes_interface = analyze_geonodes.interface( geonodes_interface = analyze_geonodes.interface(
input_sockets['GeoNodes'], direc='INPUT' geo_nodes, direc='INPUT'
) )
# Fill the Loose Input Sockets # Set Loose Input Sockets
log.info( ## Retrieve the appropriate SocketDef for the Blender Interface Socket
'Initializing GeoNodes Structure Node "%s" from GeoNodes Group "%s"',
self.bl_label,
str(geonodes),
)
self.loose_input_sockets = { self.loose_input_sockets = {
socket_name: bl_socket_map.socket_def_from_bl_socket(iface_socket)() socket_name: bl_socket_map.socket_def_from_bl_interface_socket(
for socket_name, iface_socket in geonodes_interface.items() bl_interface_socket
)() ## === <SocketType>SocketDef(), but with dynamic SocketDef
for socket_name, bl_interface_socket in geonodes_interface.items()
} }
# Set Loose Input Sockets to Interface (Default) Values ## Set Loose `socket.value` from Interface `default_value`
## Changing socket.value invokes recursion of this function.
## The else: below ensures that only one push occurs.
## (well, one push per .value set, which simplifies to one push)
log.info(
'Setting Loose Input Sockets of "%s" to GeoNodes Defaults',
self.bl_label,
)
for socket_name in self.loose_input_sockets: for socket_name in self.loose_input_sockets:
socket = self.inputs[socket_name] socket = self.inputs[socket_name]
socket.value = bl_socket_map.read_bl_socket_default_value( bl_interface_socket = geonodes_interface[socket_name]
geonodes_interface[socket_name],
unit_systems['BlenderUnits'], socket.value = bl_socket_map.value_from_bl(bl_interface_socket)
allow_unit_not_in_unit_system=True,
## Implicitly triggers the loose-input `on_value_changed` for each.
@base.on_value_changed(
any_loose_input_socket=True,
managed_objs={'geometry'},
input_sockets={'Unit System', 'GeoNodes'},
) )
log.info( def on_value_changed__loose_inputs(
'Set Loose Input Sockets of "%s" to: %s', self,
self.bl_label, managed_objs: dict[str, ct.schemas.ManagedObj],
str(self.loose_input_sockets), input_sockets: dict[str, typ.Any],
loose_input_sockets: dict[str, typ.Any],
):
"""Called whenever a Loose Input Socket is altered.
Synchronizes the change to the actual GeoNodes modifier, so that the change is immediately visible.
"""
# Retrieve Data
unit_system = input_sockets['Unit System']
mobj = managed_objs['geometry']
if not (geo_nodes := input_sockets['GeoNodes']):
return
# Analyze GeoNodes Interface (input direction)
## This retrieves NodeTreeSocketInterface elements
geonodes_interface = analyze_geonodes.interface(
geo_nodes, direc='INPUT'
)
## TODO: Check that Loose Sockets matches the Interface
## - If the user deletes an interface socket, bad things will happen.
## - We will try to set an identifier that doesn't exist!
## - Instead, this should update the loose input sockets.
## Push Values to the GeoNodes Modifier
mobj.sync_geonodes_modifier(
geonodes_node_group=geo_nodes,
geonodes_identifier_to_value={
bl_interface_socket.identifier: bl_socket_map.value_to_bl(
bl_interface_socket,
loose_input_sockets[socket_name],
unit_system,
)
for socket_name, bl_interface_socket in (
geonodes_interface.items()
) )
else:
# Push Loose Input Values to GeoNodes Modifier
managed_objs['modifier'].bl_modifier(
managed_objs['mesh'].bl_object(location=input_sockets['Center']),
'NODES',
{
'node_group': input_sockets['GeoNodes'],
'unit_system': unit_systems['BlenderUnits'],
'inputs': loose_input_sockets,
}, },
) )
# Push Preview State
if props['preview_active']: ####################
managed_objs['mesh'].show_preview() # - Event Methods
####################
@base.on_show_preview(
managed_objs={'geometry'},
)
def on_show_preview(
self,
managed_objs: dict[str, ct.schemas.ManagedObj],
):
"""Called whenever a Loose Input Socket is altered.
Synchronizes the change to the actual GeoNodes modifier, so that the change is immediately visible.
"""
managed_objs['geometry'].show_preview('MESH')
#################### ####################
@ -165,4 +185,6 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
BL_REGISTER = [ BL_REGISTER = [
GeoNodesStructureNode, GeoNodesStructureNode,
] ]
BL_NODES = {ct.NodeType.GeoNodesStructure: (ct.NodeCategory.MAXWELLSIM_STRUCTURES)} BL_NODES = {
ct.NodeType.GeoNodesStructure: (ct.NodeCategory.MAXWELLSIM_STRUCTURES)
}

View File

@ -1,10 +1,14 @@
import bmesh
import bpy
import numpy as np
import tidy3d as td import tidy3d as td
import numpy as np
import sympy as sp
import sympy.physics.units as spu
from ... import contracts, sockets import bpy
from .. import base, events import bmesh
from ... import contracts
from ... import sockets
from .. import base
class ObjectStructureNode(base.MaxwellSimTreeNode): class ObjectStructureNode(base.MaxwellSimTreeNode):
@ -32,7 +36,7 @@ class ObjectStructureNode(base.MaxwellSimTreeNode):
#################### ####################
# - Output Socket Computation # - Output Socket Computation
#################### ####################
@events.computes_output_socket('structure') @base.computes_output_socket('structure')
def compute_structure(self: contracts.NodeTypeProtocol) -> td.Structure: def compute_structure(self: contracts.NodeTypeProtocol) -> td.Structure:
# Extract the Blender Object # Extract the Blender Object
bl_object = self.compute_input('object') bl_object = self.compute_input('object')
@ -54,7 +58,9 @@ class ObjectStructureNode(base.MaxwellSimTreeNode):
# Extract Vertices and Faces # Extract Vertices and Faces
vertices = np.array([vert.co for vert in mesh.vertices]) vertices = np.array([vert.co for vert in mesh.vertices])
faces = np.array([[vert for vert in poly.vertices] for poly in mesh.polygons]) faces = np.array(
[[vert for vert in poly.vertices] for poly in mesh.polygons]
)
# Remove Temporary Mesh # Remove Temporary Mesh
bpy.data.meshes.remove(mesh) bpy.data.meshes.remove(mesh)
@ -72,5 +78,7 @@ BL_REGISTER = [
ObjectStructureNode, ObjectStructureNode,
] ]
BL_NODES = { BL_NODES = {
contracts.NodeType.ObjectStructure: (contracts.NodeCategory.MAXWELLSIM_STRUCTURES) contracts.NodeType.ObjectStructure: (
contracts.NodeCategory.MAXWELLSIM_STRUCTURES
)
} }

View File

@ -1,5 +1,7 @@
from . import box_structure
# from . import cylinder_structure # from . import cylinder_structure
from . import box_structure, sphere_structure from . import sphere_structure
BL_REGISTER = [ BL_REGISTER = [
*box_structure.BL_REGISTER, *box_structure.BL_REGISTER,

View File

@ -1,101 +1,123 @@
import typing as typ import tidy3d as td
import sympy as sp import sympy as sp
import sympy.physics.units as spu import sympy.physics.units as spu
import tidy3d as td
from ......assets.import_geonodes import GeoNodes, import_geonodes import bpy
from ......utils import analyze_geonodes
from .... import contracts as ct from .... import contracts as ct
from .... import managed_objs, sockets from .... import sockets
from ... import base, events from .... import managed_objs
from ... import base
GEONODES_STRUCTURE_BOX = 'structure_box'
class BoxStructureNode(base.MaxwellSimNode): class BoxStructureNode(base.MaxwellSimNode):
node_type = ct.NodeType.BoxStructure node_type = ct.NodeType.BoxStructure
bl_label = 'Box Structure' bl_label = 'Box Structure'
use_sim_node_name = True
#################### ####################
# - Sockets # - Sockets
#################### ####################
input_sockets: typ.ClassVar = { input_sockets = {
'Medium': sockets.MaxwellMediumSocketDef(), 'Medium': sockets.MaxwellMediumSocketDef(),
'Center': sockets.PhysicalPoint3DSocketDef(), 'Center': sockets.PhysicalPoint3DSocketDef(),
'Size': sockets.PhysicalSize3DSocketDef( 'Size': sockets.PhysicalSize3DSocketDef(
default_value=sp.Matrix([500, 500, 500]) * spu.nm default_value=sp.Matrix([500, 500, 500]) * spu.nm
), ),
} }
output_sockets: typ.ClassVar = { output_sockets = {
'Structure': sockets.MaxwellStructureSocketDef(), 'Structure': sockets.MaxwellStructureSocketDef(),
} }
managed_obj_defs: typ.ClassVar = { managed_obj_defs = {
'mesh': ct.schemas.ManagedObjDef( 'structure_box': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLMesh(name), mk=lambda name: managed_objs.ManagedBLObject(name),
), name_prefix='',
'modifier': ct.schemas.ManagedObjDef( )
mk=lambda name: managed_objs.ManagedBLModifier(name),
),
} }
#################### ####################
# - Event Methods # - Output Socket Computation
#################### ####################
@events.computes_output_socket( @base.computes_output_socket(
'Structure', 'Structure',
input_sockets={'Medium', 'Center', 'Size'}, input_sockets={'Medium', 'Center', 'Size'},
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
scale_input_sockets={
'Center': 'Tidy3DUnits',
'Size': 'Tidy3DUnits',
},
) )
def compute_output(self, input_sockets: dict, unit_systems: dict) -> td.Box: def compute_simulation(self, input_sockets: dict) -> td.Box:
medium = input_sockets['Medium']
_center = input_sockets['Center']
_size = input_sockets['Size']
center = tuple(spu.convert_to(_center, spu.um) / spu.um)
size = tuple(spu.convert_to(_size, spu.um) / spu.um)
return td.Structure( return td.Structure(
geometry=td.Box( geometry=td.Box(
center=input_sockets['Center'], center=center,
size=input_sockets['Size'], size=size,
), ),
medium=input_sockets['Medium'], medium=medium,
) )
@events.on_value_changed( ####################
# - Preview - Changes to Input Sockets
####################
@base.on_value_changed(
socket_name={'Center', 'Size'}, socket_name={'Center', 'Size'},
prop_name='preview_active',
props={'preview_active'},
input_sockets={'Center', 'Size'}, input_sockets={'Center', 'Size'},
managed_objs={'mesh', 'modifier'}, managed_objs={'structure_box'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={
'Center': 'BlenderUnits',
},
) )
def on_inputs_changed( def on_value_changed__center_size(
self, self,
props: dict,
managed_objs: dict[str, ct.schemas.ManagedObj],
input_sockets: dict, input_sockets: dict,
unit_systems: dict, managed_objs: dict[str, ct.schemas.ManagedObj],
): ):
# Push Input Values to GeoNodes Modifier _center = input_sockets['Center']
managed_objs['modifier'].bl_modifier( center = tuple(
managed_objs['mesh'].bl_object(location=input_sockets['Center']), [float(el) for el in spu.convert_to(_center, spu.um) / spu.um]
'NODES', )
{
'node_group': import_geonodes(GeoNodes.PrimitiveBox, 'link'), _size = input_sockets['Size']
'unit_system': unit_systems['BlenderUnits'], size = tuple(
'inputs': { [float(el) for el in spu.convert_to(_size, spu.um) / spu.um]
'Size': input_sockets['Size'], )
}, ## TODO: Preview unit system?? Presume um for now
# Retrieve Hard-Coded GeoNodes and Analyze Input
geo_nodes = bpy.data.node_groups[GEONODES_STRUCTURE_BOX]
geonodes_interface = analyze_geonodes.interface(
geo_nodes, direc='INPUT'
)
# Sync Modifier Inputs
managed_objs['structure_box'].sync_geonodes_modifier(
geonodes_node_group=geo_nodes,
geonodes_identifier_to_value={
geonodes_interface['Size'].identifier: size,
## TODO: Use 'bl_socket_map.value_to_bl`!
## - This accounts for auto-conversion, unit systems, etc. .
## - We could keep it in the node base class...
## - ...But it needs aligning with Blender, too. Hmm.
}, },
) )
# Push Preview State
if props['preview_active']:
managed_objs['mesh'].show_preview()
@events.on_init() # Sync Object Position
def on_init(self): managed_objs['structure_box'].bl_object('MESH').location = center
self.on_inputs_changed()
####################
# - Preview - Show Preview
####################
@base.on_show_preview(
managed_objs={'structure_box'},
)
def on_show_preview(
self,
managed_objs: dict[str, ct.schemas.ManagedObj],
):
managed_objs['structure_box'].show_preview('MESH')
self.on_value_changed__center_size()
#################### ####################
@ -105,5 +127,7 @@ BL_REGISTER = [
BoxStructureNode, BoxStructureNode,
] ]
BL_NODES = { BL_NODES = {
ct.NodeType.BoxStructure: (ct.NodeCategory.MAXWELLSIM_STRUCTURES_PRIMITIVES) ct.NodeType.BoxStructure: (
ct.NodeCategory.MAXWELLSIM_STRUCTURES_PRIMITIVES
)
} }

View File

@ -1,8 +1,10 @@
import sympy.physics.units as spu
import tidy3d as td import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
from .... import contracts, sockets from .... import contracts
from ... import base, events from .... import sockets
from ... import base
class CylinderStructureNode(base.MaxwellSimTreeNode): class CylinderStructureNode(base.MaxwellSimTreeNode):
@ -36,7 +38,7 @@ class CylinderStructureNode(base.MaxwellSimTreeNode):
#################### ####################
# - Output Socket Computation # - Output Socket Computation
#################### ####################
@events.computes_output_socket('structure') @base.computes_output_socket('structure')
def compute_simulation(self: contracts.NodeTypeProtocol) -> td.Box: def compute_simulation(self: contracts.NodeTypeProtocol) -> td.Box:
medium = self.compute_input('medium') medium = self.compute_input('medium')
_center = self.compute_input('center') _center = self.compute_input('center')

View File

@ -1,104 +1,121 @@
import typing as typ
import sympy.physics.units as spu
import tidy3d as td import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
from ......assets.import_geonodes import GeoNodes, import_geonodes import bpy
from ......utils import analyze_geonodes
from .... import contracts as ct from .... import contracts as ct
from .... import managed_objs, sockets from .... import sockets
from ... import base, events from .... import managed_objs
from ... import base
GEONODES_STRUCTURE_SPHERE = 'structure_sphere'
class SphereStructureNode(base.MaxwellSimNode): class SphereStructureNode(base.MaxwellSimNode):
node_type = ct.NodeType.SphereStructure node_type = ct.NodeType.SphereStructure
bl_label = 'Sphere Structure' bl_label = 'Sphere Structure'
use_sim_node_name = True
#################### ####################
# - Sockets # - Sockets
#################### ####################
input_sockets: typ.ClassVar = { input_sockets = {
'Center': sockets.PhysicalPoint3DSocketDef(), 'Center': sockets.PhysicalPoint3DSocketDef(),
'Radius': sockets.PhysicalLengthSocketDef( 'Radius': sockets.PhysicalLengthSocketDef(
default_value=150 * spu.nm, default_value=150 * spu.nm,
), ),
'Medium': sockets.MaxwellMediumSocketDef(), 'Medium': sockets.MaxwellMediumSocketDef(),
} }
output_sockets: typ.ClassVar = { output_sockets = {
'Structure': sockets.MaxwellStructureSocketDef(), 'Structure': sockets.MaxwellStructureSocketDef(),
} }
managed_obj_defs: typ.ClassVar = { managed_obj_defs = {
'mesh': ct.schemas.ManagedObjDef( 'structure_sphere': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLMesh(name), mk=lambda name: managed_objs.ManagedBLObject(name),
), name_prefix='',
'modifier': ct.schemas.ManagedObjDef( )
mk=lambda name: managed_objs.ManagedBLModifier(name),
),
} }
#################### ####################
# - Output Socket Computation # - Output Socket Computation
#################### ####################
@events.computes_output_socket( @base.computes_output_socket(
'Structure', 'Structure',
input_sockets={'Center', 'Radius', 'Medium'}, input_sockets={'Center', 'Radius', 'Medium'},
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
scale_input_sockets={
'Center': 'Tidy3DUnits',
'Radius': 'Tidy3DUnits',
},
) )
def compute_structure(self, input_sockets: dict) -> td.Box: def compute_structure(self, input_sockets: dict) -> td.Box:
medium = input_sockets['Medium']
_center = input_sockets['Center']
_radius = input_sockets['Radius']
center = tuple(spu.convert_to(_center, spu.um) / spu.um)
radius = spu.convert_to(_radius, spu.um) / spu.um
return td.Structure( return td.Structure(
geometry=td.Sphere( geometry=td.Sphere(
radius=input_sockets['Radius'], radius=radius,
center=input_sockets['Center'], center=center,
), ),
medium=input_sockets['Medium'], medium=medium,
) )
#################### ####################
# - Preview - Changes to Input Sockets # - Preview - Changes to Input Sockets
#################### ####################
@events.on_value_changed( @base.on_value_changed(
socket_name={'Center', 'Radius'}, socket_name={'Center', 'Radius'},
prop_name='preview_active',
props={'preview_active'},
input_sockets={'Center', 'Radius'}, input_sockets={'Center', 'Radius'},
managed_objs={'mesh', 'modifier'}, managed_objs={'structure_sphere'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={
'Center': 'Tidy3DUnits',
'Radius': 'Tidy3DUnits',
},
) )
def on_inputs_changed( def on_value_changed__center_radius(
self, self,
props: dict,
managed_objs: dict[str, ct.schemas.ManagedObj],
input_sockets: dict, input_sockets: dict,
unit_systems: dict, managed_objs: dict[str, ct.schemas.ManagedObj],
): ):
# Push Input Values to GeoNodes Modifier _center = input_sockets['Center']
managed_objs['modifier'].bl_modifier( center = tuple(
managed_objs['mesh'].bl_object(location=input_sockets['Center']), [float(el) for el in spu.convert_to(_center, spu.um) / spu.um]
'NODES', )
{
'node_group': import_geonodes(GeoNodes.PrimitiveSphere, 'link'), _radius = input_sockets['Radius']
'unit_system': unit_systems['BlenderUnits'], radius = float(spu.convert_to(_radius, spu.um) / spu.um)
'inputs': { ## TODO: Preview unit system?? Presume um for now
'Radius': input_sockets['Radius'],
}, # Retrieve Hard-Coded GeoNodes and Analyze Input
geo_nodes = bpy.data.node_groups[GEONODES_STRUCTURE_SPHERE]
geonodes_interface = analyze_geonodes.interface(
geo_nodes, direc='INPUT'
)
# Sync Modifier Inputs
managed_objs['structure_sphere'].sync_geonodes_modifier(
geonodes_node_group=geo_nodes,
geonodes_identifier_to_value={
geonodes_interface['Radius'].identifier: radius,
## TODO: Use 'bl_socket_map.value_to_bl`!
## - This accounts for auto-conversion, unit systems, etc. .
## - We could keep it in the node base class...
## - ...But it needs aligning with Blender, too. Hmm.
}, },
) )
# Push Preview State
if props['preview_active']:
managed_objs['mesh'].show_preview()
@events.on_init() # Sync Object Position
def on_init(self): managed_objs['structure_sphere'].bl_object('MESH').location = center
self.on_inputs_changed()
####################
# - Preview - Show Preview
####################
@base.on_show_preview(
managed_objs={'structure_sphere'},
)
def on_show_preview(
self,
managed_objs: dict[str, ct.schemas.ManagedObj],
):
managed_objs['structure_sphere'].show_preview('MESH')
self.on_value_changed__center_radius()
#################### ####################
@ -108,5 +125,7 @@ BL_REGISTER = [
SphereStructureNode, SphereStructureNode,
] ]
BL_NODES = { BL_NODES = {
ct.NodeType.SphereStructure: (ct.NodeCategory.MAXWELLSIM_STRUCTURES_PRIMITIVES) ct.NodeType.SphereStructure: (
ct.NodeCategory.MAXWELLSIM_STRUCTURES_PRIMITIVES
)
} }

View File

@ -1,6 +1,5 @@
# from . import math # from . import math
from . import combine from . import combine
# from . import separate # from . import separate
BL_REGISTER = [ BL_REGISTER = [

View File

@ -1,9 +1,12 @@
import bpy
import sympy as sp import sympy as sp
import sympy.physics.units as spu
import scipy as sc
import bpy
from ... import contracts as ct from ... import contracts as ct
from ... import sockets from ... import sockets
from .. import base, events from .. import base
MAX_AMOUNT = 20 MAX_AMOUNT = 20
@ -20,7 +23,9 @@ class CombineNode(base.MaxwellSimNode):
'Maxwell Sources': {}, 'Maxwell Sources': {},
'Maxwell Structures': {}, 'Maxwell Structures': {},
'Maxwell Monitors': {}, 'Maxwell Monitors': {},
'Real 3D Vector': {f'x_{i}': sockets.RealNumberSocketDef() for i in range(3)}, 'Real 3D Vector': {
f'x_{i}': sockets.RealNumberSocketDef() for i in range(3)
},
# "Point 3D": { # "Point 3D": {
# axis: sockets.PhysicalLengthSocketDef() # axis: sockets.PhysicalLengthSocketDef()
# for i, axis in zip( # for i, axis in zip(
@ -82,13 +87,13 @@ class CombineNode(base.MaxwellSimNode):
#################### ####################
# - Output Socket Computation # - Output Socket Computation
#################### ####################
@events.computes_output_socket( @base.computes_output_socket(
'Real 3D Vector', input_sockets={'x_0', 'x_1', 'x_2'} 'Real 3D Vector', input_sockets={'x_0', 'x_1', 'x_2'}
) )
def compute_real_3d_vector(self, input_sockets) -> sp.Expr: def compute_real_3d_vector(self, input_sockets) -> sp.Expr:
return sp.Matrix([input_sockets[f'x_{i}'] for i in range(3)]) return sp.Matrix([input_sockets[f'x_{i}'] for i in range(3)])
@events.computes_output_socket( @base.computes_output_socket(
'Sources', 'Sources',
input_sockets={f'Source #{i}' for i in range(MAX_AMOUNT)}, input_sockets={f'Source #{i}' for i in range(MAX_AMOUNT)},
props={'amount'}, props={'amount'},
@ -96,15 +101,17 @@ class CombineNode(base.MaxwellSimNode):
def compute_sources(self, input_sockets, props) -> sp.Expr: def compute_sources(self, input_sockets, props) -> sp.Expr:
return [input_sockets[f'Source #{i}'] for i in range(props['amount'])] return [input_sockets[f'Source #{i}'] for i in range(props['amount'])]
@events.computes_output_socket( @base.computes_output_socket(
'Structures', 'Structures',
input_sockets={f'Structure #{i}' for i in range(MAX_AMOUNT)}, input_sockets={f'Structure #{i}' for i in range(MAX_AMOUNT)},
props={'amount'}, props={'amount'},
) )
def compute_structures(self, input_sockets, props) -> sp.Expr: def compute_structures(self, input_sockets, props) -> sp.Expr:
return [input_sockets[f'Structure #{i}'] for i in range(props['amount'])] return [
input_sockets[f'Structure #{i}'] for i in range(props['amount'])
]
@events.computes_output_socket( @base.computes_output_socket(
'Monitors', 'Monitors',
input_sockets={f'Monitor #{i}' for i in range(MAX_AMOUNT)}, input_sockets={f'Monitor #{i}' for i in range(MAX_AMOUNT)},
props={'amount'}, props={'amount'},
@ -115,7 +122,7 @@ class CombineNode(base.MaxwellSimNode):
#################### ####################
# - Input Socket Compilation # - Input Socket Compilation
#################### ####################
@events.on_value_changed( @base.on_value_changed(
prop_name='active_socket_set', prop_name='active_socket_set',
props={'active_socket_set', 'amount'}, props={'active_socket_set', 'amount'},
) )
@ -138,13 +145,13 @@ class CombineNode(base.MaxwellSimNode):
else: else:
self.loose_input_sockets = {} self.loose_input_sockets = {}
@events.on_value_changed( @base.on_value_changed(
prop_name='amount', prop_name='amount',
) )
def on_value_changed__amount(self): def on_value_changed__amount(self):
self.on_value_changed__active_socket_set() self.on_value_changed__active_socket_set()
@events.on_init() @base.on_init()
def on_init(self): def on_init(self):
self.on_value_changed__active_socket_set() self.on_value_changed__active_socket_set()

View File

@ -1,9 +1,11 @@
import scipy as sc import tidy3d as td
import sympy as sp import sympy as sp
import sympy.physics.units as spu import sympy.physics.units as spu
import scipy as sc
from .... import contracts, sockets from .... import contracts
from ... import base, events from .... import sockets
from ... import base
class WaveConverterNode(base.MaxwellSimTreeNode): class WaveConverterNode(base.MaxwellSimTreeNode):
@ -44,9 +46,11 @@ class WaveConverterNode(base.MaxwellSimTreeNode):
#################### ####################
# - Output Socket Computation # - Output Socket Computation
#################### ####################
@events.computes_output_socket('freq') @base.computes_output_socket('freq')
def compute_freq(self: contracts.NodeTypeProtocol) -> sp.Expr: def compute_freq(self: contracts.NodeTypeProtocol) -> sp.Expr:
vac_speed_of_light = sc.constants.speed_of_light * spu.meter / spu.second vac_speed_of_light = (
sc.constants.speed_of_light * spu.meter / spu.second
)
vacwl = self.compute_input('vacwl') vacwl = self.compute_input('vacwl')
@ -55,9 +59,11 @@ class WaveConverterNode(base.MaxwellSimTreeNode):
spu.hertz, spu.hertz,
) )
@events.computes_output_socket('vacwl') @base.computes_output_socket('vacwl')
def compute_vacwl(self: contracts.NodeTypeProtocol) -> sp.Expr: def compute_vacwl(self: contracts.NodeTypeProtocol) -> sp.Expr:
vac_speed_of_light = sc.constants.speed_of_light * spu.meter / spu.second vac_speed_of_light = (
sc.constants.speed_of_light * spu.meter / spu.second
)
freq = self.compute_input('freq') freq = self.compute_input('freq')

View File

@ -1,10 +1,18 @@
import typing as typ import typing as typ
import tidy3d as td
import numpy as np
import sympy as sp
import sympy.physics.units as spu
import bpy import bpy
from .....utils import analyze_geonodes
from ... import bl_socket_map
from ... import contracts as ct from ... import contracts as ct
from ... import managed_objs, sockets from ... import sockets
from .. import base, events from .. import base
from ... import managed_objs
CACHE = {} CACHE = {}
@ -16,12 +24,12 @@ class FDTDSimDataVizNode(base.MaxwellSimNode):
#################### ####################
# - Sockets # - Sockets
#################### ####################
input_sockets: typ.ClassVar = { input_sockets = {
'FDTD Sim Data': sockets.MaxwellFDTDSimDataSocketDef(), 'FDTD Sim Data': sockets.MaxwellFDTDSimDataSocketDef(),
} }
output_sockets: typ.ClassVar = {'Preview': sockets.AnySocketDef()} output_sockets = {'Preview': sockets.AnySocketDef()}
managed_obj_defs: typ.ClassVar = { managed_obj_defs = {
'viz_plot': ct.schemas.ManagedObjDef( 'viz_plot': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLImage(name), mk=lambda name: managed_objs.ManagedBLImage(name),
name_prefix='', name_prefix='',
@ -63,7 +71,9 @@ class FDTDSimDataVizNode(base.MaxwellSimNode):
# ("Hz", "Hz", "Hz"), # ("Hz", "Hz", "Hz"),
], ],
default='E', default='E',
update=lambda self, context: self.sync_prop('field_viz_component', context), update=lambda self, context: self.sync_prop(
'field_viz_component', context
),
) )
field_viz_part: bpy.props.EnumProperty( field_viz_part: bpy.props.EnumProperty(
name='Field Part', name='Field Part',
@ -86,7 +96,9 @@ class FDTDSimDataVizNode(base.MaxwellSimNode):
('dB', 'Log (dB)', 'Logarithmic (dB) Scale'), ('dB', 'Log (dB)', 'Logarithmic (dB) Scale'),
], ],
default='lin', default='lin',
update=lambda self, context: self.sync_prop('field_viz_scale', context), update=lambda self, context: self.sync_prop(
'field_viz_scale', context
),
) )
field_viz_structure_visibility: bpy.props.FloatProperty( field_viz_structure_visibility: bpy.props.FloatProperty(
name='Field Viz Plot: Structure Visibility', name='Field Viz Plot: Structure Visibility',
@ -94,57 +106,75 @@ class FDTDSimDataVizNode(base.MaxwellSimNode):
default=0.2, default=0.2,
min=0.0, min=0.0,
max=1.0, max=1.0,
update=lambda self, context: self.sync_prop('field_viz_plot_fixed_f', context), update=lambda self, context: self.sync_prop(
'field_viz_plot_fixed_f', context
),
) )
field_viz_plot_fix_x: bpy.props.BoolProperty( field_viz_plot_fix_x: bpy.props.BoolProperty(
name='Field Viz Plot: Fix X', name='Field Viz Plot: Fix X',
description='Fix the x-coordinate on the plot', description='Fix the x-coordinate on the plot',
default=False, default=False,
update=lambda self, context: self.sync_prop('field_viz_plot_fix_x', context), update=lambda self, context: self.sync_prop(
'field_viz_plot_fix_x', context
),
) )
field_viz_plot_fix_y: bpy.props.BoolProperty( field_viz_plot_fix_y: bpy.props.BoolProperty(
name='Field Viz Plot: Fix Y', name='Field Viz Plot: Fix Y',
description='Fix the y coordinate on the plot', description='Fix the y coordinate on the plot',
default=False, default=False,
update=lambda self, context: self.sync_prop('field_viz_plot_fix_y', context), update=lambda self, context: self.sync_prop(
'field_viz_plot_fix_y', context
),
) )
field_viz_plot_fix_z: bpy.props.BoolProperty( field_viz_plot_fix_z: bpy.props.BoolProperty(
name='Field Viz Plot: Fix Z', name='Field Viz Plot: Fix Z',
description='Fix the z coordinate on the plot', description='Fix the z coordinate on the plot',
default=False, default=False,
update=lambda self, context: self.sync_prop('field_viz_plot_fix_z', context), update=lambda self, context: self.sync_prop(
'field_viz_plot_fix_z', context
),
) )
field_viz_plot_fix_f: bpy.props.BoolProperty( field_viz_plot_fix_f: bpy.props.BoolProperty(
name='Field Viz Plot: Fix Freq', name='Field Viz Plot: Fix Freq',
description='Fix the frequency coordinate on the plot', description='Fix the frequency coordinate on the plot',
default=False, default=False,
update=lambda self, context: self.sync_prop('field_viz_plot_fix_f', context), update=lambda self, context: self.sync_prop(
'field_viz_plot_fix_f', context
),
) )
field_viz_plot_fixed_x: bpy.props.FloatProperty( field_viz_plot_fixed_x: bpy.props.FloatProperty(
name='Field Viz Plot: Fix X', name='Field Viz Plot: Fix X',
description='Fix the x-coordinate on the plot', description='Fix the x-coordinate on the plot',
default=0.0, default=0.0,
update=lambda self, context: self.sync_prop('field_viz_plot_fixed_x', context), update=lambda self, context: self.sync_prop(
'field_viz_plot_fixed_x', context
),
) )
field_viz_plot_fixed_y: bpy.props.FloatProperty( field_viz_plot_fixed_y: bpy.props.FloatProperty(
name='Field Viz Plot: Fixed Y', name='Field Viz Plot: Fixed Y',
description='Fix the y coordinate on the plot', description='Fix the y coordinate on the plot',
default=0.0, default=0.0,
update=lambda self, context: self.sync_prop('field_viz_plot_fixed_y', context), update=lambda self, context: self.sync_prop(
'field_viz_plot_fixed_y', context
),
) )
field_viz_plot_fixed_z: bpy.props.FloatProperty( field_viz_plot_fixed_z: bpy.props.FloatProperty(
name='Field Viz Plot: Fixed Z', name='Field Viz Plot: Fixed Z',
description='Fix the z coordinate on the plot', description='Fix the z coordinate on the plot',
default=0.0, default=0.0,
update=lambda self, context: self.sync_prop('field_viz_plot_fixed_z', context), update=lambda self, context: self.sync_prop(
'field_viz_plot_fixed_z', context
),
) )
field_viz_plot_fixed_f: bpy.props.FloatProperty( field_viz_plot_fixed_f: bpy.props.FloatProperty(
name='Field Viz Plot: Fixed Freq (Thz)', name='Field Viz Plot: Fixed Freq (Thz)',
description='Fix the frequency coordinate on the plot', description='Fix the frequency coordinate on the plot',
default=0.0, default=0.0,
update=lambda self, context: self.sync_prop('field_viz_plot_fixed_f', context), update=lambda self, context: self.sync_prop(
'field_viz_plot_fixed_f', context
),
) )
#################### ####################
@ -154,7 +184,9 @@ class FDTDSimDataVizNode(base.MaxwellSimNode):
if (sim_data := self._compute_input('FDTD Sim Data')) is None: if (sim_data := self._compute_input('FDTD Sim Data')) is None:
return return
self.cache_viz_monitor_type = sim_data.monitor_data[self.viz_monitor_name].type self.cache_viz_monitor_type = sim_data.monitor_data[
self.viz_monitor_name
].type
self.sync_prop('viz_monitor_name', context) self.sync_prop('viz_monitor_name', context)
def retrieve_monitors(self, context) -> list[tuple]: def retrieve_monitors(self, context) -> list[tuple]:
@ -220,7 +252,7 @@ class FDTDSimDataVizNode(base.MaxwellSimNode):
#################### ####################
# - On Value Changed Methods # - On Value Changed Methods
#################### ####################
@events.on_value_changed( @base.on_value_changed(
socket_name='FDTD Sim Data', socket_name='FDTD Sim Data',
managed_objs={'viz_object'}, managed_objs={'viz_object'},
input_sockets={'FDTD Sim Data'}, input_sockets={'FDTD Sim Data'},
@ -236,12 +268,14 @@ class FDTDSimDataVizNode(base.MaxwellSimNode):
CACHE.pop(self.instance_id, None) CACHE.pop(self.instance_id, None)
return return
CACHE[self.instance_id] = {'monitors': list(sim_data.monitor_data.keys())} CACHE[self.instance_id] = {
'monitors': list(sim_data.monitor_data.keys())
}
#################### ####################
# - Plotting # - Plotting
#################### ####################
@events.on_show_plot( @base.on_show_plot(
managed_objs={'viz_plot'}, managed_objs={'viz_plot'},
props={ props={
'viz_monitor_name', 'viz_monitor_name',
@ -296,7 +330,7 @@ class FDTDSimDataVizNode(base.MaxwellSimNode):
bl_select=True, bl_select=True,
) )
# @events.on_show_preview( # @base.on_show_preview(
# managed_objs={"viz_object"}, # managed_objs={"viz_object"},
# ) # )
# def on_show_preview( # def on_show_preview(

View File

@ -1,35 +1,72 @@
from ....utils import logger from . import base
from .. import contracts as ct
from . import basic, blender, maxwell, number, physical, tidy3d, vector
from .scan_socket_defs import scan_for_socket_defs
log = logger.get(__name__) from . import basic
sockets_modules = [basic, number, vector, physical, blender, maxwell, tidy3d]
#################### AnySocketDef = basic.AnySocketDef
# - Scan for SocketDefs BoolSocketDef = basic.BoolSocketDef
#################### StringSocketDef = basic.StringSocketDef
SOCKET_DEFS = {} FilePathSocketDef = basic.FilePathSocketDef
for sockets_module in sockets_modules:
SOCKET_DEFS |= scan_for_socket_defs(sockets_module)
# Set Global Names from SOCKET_DEFS from . import number
## SOCKET_DEFS values are the classes themselves, which always have a __name__.
for socket_def_type in SOCKET_DEFS.values():
globals()[socket_def_type.__name__] = socket_def_type
# Validate SocketType -> SocketDef IntegerNumberSocketDef = number.IntegerNumberSocketDef
## All SocketTypes should have a SocketDef RationalNumberSocketDef = number.RationalNumberSocketDef
for socket_type in ct.SocketType: RealNumberSocketDef = number.RealNumberSocketDef
if ( ComplexNumberSocketDef = number.ComplexNumberSocketDef
globals().get(socket_type.value.removesuffix('SocketType') + 'SocketDef')
is None from . import vector
):
log.warning('Missing SocketDef for %s', socket_type.value) Real2DVectorSocketDef = vector.Real2DVectorSocketDef
Complex2DVectorSocketDef = vector.Complex2DVectorSocketDef
Integer3DVectorSocketDef = vector.Integer3DVectorSocketDef
Real3DVectorSocketDef = vector.Real3DVectorSocketDef
Complex3DVectorSocketDef = vector.Complex3DVectorSocketDef
from . import physical
PhysicalUnitSystemSocketDef = physical.PhysicalUnitSystemSocketDef
PhysicalTimeSocketDef = physical.PhysicalTimeSocketDef
PhysicalAngleSocketDef = physical.PhysicalAngleSocketDef
PhysicalLengthSocketDef = physical.PhysicalLengthSocketDef
PhysicalAreaSocketDef = physical.PhysicalAreaSocketDef
PhysicalVolumeSocketDef = physical.PhysicalVolumeSocketDef
PhysicalPoint3DSocketDef = physical.PhysicalPoint3DSocketDef
PhysicalSize3DSocketDef = physical.PhysicalSize3DSocketDef
PhysicalMassSocketDef = physical.PhysicalMassSocketDef
PhysicalSpeedSocketDef = physical.PhysicalSpeedSocketDef
PhysicalAccelScalarSocketDef = physical.PhysicalAccelScalarSocketDef
PhysicalForceScalarSocketDef = physical.PhysicalForceScalarSocketDef
PhysicalPolSocketDef = physical.PhysicalPolSocketDef
PhysicalFreqSocketDef = physical.PhysicalFreqSocketDef
from . import blender
BlenderObjectSocketDef = blender.BlenderObjectSocketDef
BlenderCollectionSocketDef = blender.BlenderCollectionSocketDef
BlenderImageSocketDef = blender.BlenderImageSocketDef
BlenderGeoNodesSocketDef = blender.BlenderGeoNodesSocketDef
BlenderTextSocketDef = blender.BlenderTextSocketDef
from . import maxwell
MaxwellBoundCondSocketDef = maxwell.MaxwellBoundCondSocketDef
MaxwellBoundCondsSocketDef = maxwell.MaxwellBoundCondsSocketDef
MaxwellMediumSocketDef = maxwell.MaxwellMediumSocketDef
MaxwellMediumNonLinearitySocketDef = maxwell.MaxwellMediumNonLinearitySocketDef
MaxwellSourceSocketDef = maxwell.MaxwellSourceSocketDef
MaxwellTemporalShapeSocketDef = maxwell.MaxwellTemporalShapeSocketDef
MaxwellStructureSocketDef = maxwell.MaxwellStructureSocketDef
MaxwellMonitorSocketDef = maxwell.MaxwellMonitorSocketDef
MaxwellFDTDSimSocketDef = maxwell.MaxwellFDTDSimSocketDef
MaxwellFDTDSimDataSocketDef = maxwell.MaxwellFDTDSimDataSocketDef
MaxwellSimGridSocketDef = maxwell.MaxwellSimGridSocketDef
MaxwellSimGridAxisSocketDef = maxwell.MaxwellSimGridAxisSocketDef
MaxwellSimDomainSocketDef = maxwell.MaxwellSimDomainSocketDef
from . import tidy3d
Tidy3DCloudTaskSocketDef = tidy3d.Tidy3DCloudTaskSocketDef
####################
# - Exports
####################
BL_REGISTER = [ BL_REGISTER = [
*basic.BL_REGISTER, *basic.BL_REGISTER,
*number.BL_REGISTER, *number.BL_REGISTER,
@ -39,13 +76,3 @@ BL_REGISTER = [
*maxwell.BL_REGISTER, *maxwell.BL_REGISTER,
*tidy3d.BL_REGISTER, *tidy3d.BL_REGISTER,
] ]
__all__ = [
'basic',
'number',
'vector',
'physical',
'blender',
'maxwell',
'tidy3d',
] + [socket_def_type.__name__ for socket_def_type in SOCKET_DEFS.values()]

View File

@ -1,11 +1,12 @@
import functools
import typing as typ import typing as typ
import typing_extensions as typx
import functools
import bpy import bpy
import pydantic as pyd
import sympy as sp import sympy as sp
import sympy.physics.units as spu import sympy.physics.units as spu
import typing_extensions as typx
from .. import contracts as ct from .. import contracts as ct
@ -42,7 +43,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
# - Initialization # - Initialization
#################### ####################
def __init_subclass__(cls, **kwargs: typ.Any): def __init_subclass__(cls, **kwargs: typ.Any):
super().__init_subclass__(**kwargs) super().__init_subclass__(**kwargs) ## Yucky superclass setup.
# Setup Blender ID for Node # Setup Blender ID for Node
if not hasattr(cls, 'socket_type'): if not hasattr(cls, 'socket_type'):
@ -180,7 +181,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
if self.locked: if self.locked:
return False return False
if self.is_output: if self.is_output:
msg = "Tried to sync 'link add' on output socket" msg = f"Tried to sync 'link add' on output socket"
raise RuntimeError(msg) raise RuntimeError(msg)
self.trigger_action('value_changed') self.trigger_action('value_changed')
@ -195,7 +196,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
if self.locked: if self.locked:
return False return False
if self.is_output: if self.is_output:
msg = "Tried to sync 'link add' on output socket" msg = f"Tried to sync 'link add' on output socket"
raise RuntimeError(msg) raise RuntimeError(msg)
self.trigger_action('value_changed') self.trigger_action('value_changed')
@ -266,12 +267,15 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
if kind == ct.DataFlowKind.Value: if kind == ct.DataFlowKind.Value:
if self.is_list: if self.is_list:
return self.value_list return self.value_list
else:
return self.value return self.value
if kind == ct.DataFlowKind.LazyValue: elif kind == ct.DataFlowKind.LazyValue:
if self.is_list: if self.is_list:
return self.lazy_value_list return self.lazy_value_list
else:
return self.lazy_value return self.lazy_value
if kind == ct.DataFlowKind.Capabilities: return self.lazy_value
elif kind == ct.DataFlowKind.Capabilities:
return self.capabilities return self.capabilities
return None return None
@ -299,7 +303,9 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
return self._compute_data(kind) return self._compute_data(kind)
## Linked: Compute Output of Linked Sockets ## Linked: Compute Output of Linked Sockets
linked_values = [link.from_socket.compute_data(kind) for link in self.links] linked_values = [
link.from_socket.compute_data(kind) for link in self.links
]
## Return Single Value / List of Values ## Return Single Value / List of Values
if len(linked_values) == 1: if len(linked_values) == 1:
@ -356,6 +362,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
Can be overridden if more specific logic is required. Can be overridden if more specific logic is required.
""" """
prev_value = self.value / self.unit * self.prev_unit prev_value = self.value / self.unit * self.prev_unit
## After changing units, self.value is expressed in the wrong unit. ## After changing units, self.value is expressed in the wrong unit.
## - Therefore, we removing the new unit, and re-add the prev unit. ## - Therefore, we removing the new unit, and re-add the prev unit.
@ -394,6 +401,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
text: str, text: str,
) -> None: ) -> None:
"""Called by Blender to draw the socket UI.""" """Called by Blender to draw the socket UI."""
if self.is_output: if self.is_output:
self.draw_output(context, layout, node, text) self.draw_output(context, layout, node, text)
else: else:
@ -450,7 +458,8 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
if self.locked: if self.locked:
row = col.row(align=False) row = col.row(align=False)
row.enabled = False row.enabled = False
elif self.locked: else:
if self.locked:
row.enabled = False row.enabled = False
# Value Column(s) # Value Column(s)
@ -489,9 +498,11 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
Can be overridden. Can be overridden.
""" """
pass
def draw_value_list(self, col: bpy.types.UILayout) -> None: def draw_value_list(self, col: bpy.types.UILayout) -> None:
"""Called to draw the value list column in unlinked input sockets. """Called to draw the value list column in unlinked input sockets.
Can be overridden. Can be overridden.
""" """
pass

View File

@ -1,12 +1,19 @@
from . import any as any_socket from . import any as any_socket
from . import bool as bool_socket
from . import file_path, string
AnySocketDef = any_socket.AnySocketDef AnySocketDef = any_socket.AnySocketDef
from . import bool as bool_socket
BoolSocketDef = bool_socket.BoolSocketDef BoolSocketDef = bool_socket.BoolSocketDef
FilePathSocketDef = file_path.FilePathSocketDef
from . import string
StringSocketDef = string.StringSocketDef StringSocketDef = string.StringSocketDef
from . import file_path
FilePathSocketDef = file_path.FilePathSocketDef
BL_REGISTER = [ BL_REGISTER = [
*any_socket.BL_REGISTER, *any_socket.BL_REGISTER,

View File

@ -1,7 +1,11 @@
import typing as typ
import bpy
import sympy as sp
import pydantic as pyd import pydantic as pyd
from ... import contracts as ct
from .. import base from .. import base
from ... import contracts as ct
#################### ####################

View File

@ -1,8 +1,11 @@
import typing as typ
import bpy import bpy
import sympy as sp
import pydantic as pyd import pydantic as pyd
from ... import contracts as ct
from .. import base from .. import base
from ... import contracts as ct
#################### ####################
@ -25,7 +28,9 @@ class BoolBLSocket(base.MaxwellSimSocket):
#################### ####################
# - Socket UI # - Socket UI
#################### ####################
def draw_label_row(self, label_col_row: bpy.types.UILayout, text: str) -> None: def draw_label_row(
self, label_col_row: bpy.types.UILayout, text: str
) -> None:
label_col_row.label(text=text) label_col_row.label(text=text)
label_col_row.prop(self, 'raw_value', text='') label_col_row.prop(self, 'raw_value', text='')

View File

@ -1,10 +1,12 @@
import typing as typ
from pathlib import Path from pathlib import Path
import bpy import bpy
import sympy as sp
import pydantic as pyd import pydantic as pyd
from ... import contracts as ct
from .. import base from .. import base
from ... import contracts as ct
#################### ####################
@ -49,7 +51,7 @@ class FilePathBLSocket(base.MaxwellSimSocket):
class FilePathSocketDef(pyd.BaseModel): class FilePathSocketDef(pyd.BaseModel):
socket_type: ct.SocketType = ct.SocketType.FilePath socket_type: ct.SocketType = ct.SocketType.FilePath
default_path: Path = Path() default_path: Path = Path('')
def init(self, bl_socket: FilePathBLSocket) -> None: def init(self, bl_socket: FilePathBLSocket) -> None:
bl_socket.value = self.default_path bl_socket.value = self.default_path

View File

@ -1,8 +1,11 @@
import typing as typ
import bpy import bpy
import sympy as sp
import pydantic as pyd import pydantic as pyd
from ... import contracts as ct
from .. import base from .. import base
from ... import contracts as ct
#################### ####################
@ -25,7 +28,9 @@ class StringBLSocket(base.MaxwellSimSocket):
#################### ####################
# - Socket UI # - Socket UI
#################### ####################
def draw_label_row(self, label_col_row: bpy.types.UILayout, text: str) -> None: def draw_label_row(
self, label_col_row: bpy.types.UILayout, text: str
) -> None:
label_col_row.prop(self, 'raw_value', text=text) label_col_row.prop(self, 'raw_value', text=text)
#################### ####################

View File

@ -1,7 +1,6 @@
from . import collection, material
from . import object as object_socket from . import object as object_socket
from . import collection
BlenderMaterialSocketDef = material.BlenderMaterialSocketDef
BlenderObjectSocketDef = object_socket.BlenderObjectSocketDef BlenderObjectSocketDef = object_socket.BlenderObjectSocketDef
BlenderCollectionSocketDef = collection.BlenderCollectionSocketDef BlenderCollectionSocketDef = collection.BlenderCollectionSocketDef
@ -9,13 +8,13 @@ from . import image
BlenderImageSocketDef = image.BlenderImageSocketDef BlenderImageSocketDef = image.BlenderImageSocketDef
from . import geonodes, text from . import geonodes
from . import text
BlenderGeoNodesSocketDef = geonodes.BlenderGeoNodesSocketDef BlenderGeoNodesSocketDef = geonodes.BlenderGeoNodesSocketDef
BlenderTextSocketDef = text.BlenderTextSocketDef BlenderTextSocketDef = text.BlenderTextSocketDef
BL_REGISTER = [ BL_REGISTER = [
*material.BL_REGISTER,
*object_socket.BL_REGISTER, *object_socket.BL_REGISTER,
*collection.BL_REGISTER, *collection.BL_REGISTER,
*text.BL_REGISTER, *text.BL_REGISTER,

View File

@ -1,8 +1,10 @@
import typing as typ
import bpy import bpy
import pydantic as pyd import pydantic as pyd
from ... import contracts as ct
from .. import base from .. import base
from ... import contracts as ct
#################### ####################

View File

@ -1,8 +1,10 @@
import typing as typ
import bpy import bpy
import pydantic as pyd import pydantic as pyd
from ... import contracts as ct
from .. import base from .. import base
from ... import contracts as ct
#################### ####################

View File

@ -1,8 +1,10 @@
import typing as typ
import bpy import bpy
import pydantic as pyd import pydantic as pyd
from ... import contracts as ct
from .. import base from .. import base
from ... import contracts as ct
#################### ####################

View File

@ -1,55 +0,0 @@
import bpy
import pydantic as pyd
from ... import contracts as ct
from .. import base
class BlenderMaterialBLSocket(base.MaxwellSimSocket):
socket_type = ct.SocketType.BlenderMaterial
bl_label = 'Blender Material'
####################
# - Properties
####################
raw_value: bpy.props.PointerProperty(
name='Blender Material',
description='Represents a Blender material',
type=bpy.types.Material,
update=(lambda self, context: self.sync_prop('raw_value', context)),
)
####################
# - UI
####################
def draw_value(self, col: bpy.types.UILayout) -> None:
col.prop(self, 'raw_value', text='')
####################
# - Default Value
####################
@property
def value(self) -> bpy.types.Material | None:
return self.raw_value
@value.setter
def value(self, value: bpy.types.Material) -> None:
self.raw_value = value
####################
# - Socket Configuration
####################
class BlenderMaterialSocketDef(pyd.BaseModel):
socket_type: ct.SocketType = ct.SocketType.BlenderMaterial
def init(self, bl_socket: BlenderMaterialBLSocket) -> None:
pass
####################
# - Blender Registration
####################
BL_REGISTER = [
BlenderMaterialBLSocket,
]

View File

@ -1,8 +1,10 @@
import typing as typ
import bpy import bpy
import pydantic as pyd import pydantic as pyd
from ... import contracts as ct
from .. import base from .. import base
from ... import contracts as ct
#################### ####################

View File

@ -1,8 +1,10 @@
import typing as typ
import bpy import bpy
import pydantic as pyd import pydantic as pyd
from ... import contracts as ct
from .. import base from .. import base
from ... import contracts as ct
#################### ####################

View File

@ -1,16 +1,19 @@
from . import bound_cond, bound_conds from . import bound_cond
from . import bound_conds
MaxwellBoundCondSocketDef = bound_cond.MaxwellBoundCondSocketDef MaxwellBoundCondSocketDef = bound_cond.MaxwellBoundCondSocketDef
MaxwellBoundCondsSocketDef = bound_conds.MaxwellBoundCondsSocketDef MaxwellBoundCondsSocketDef = bound_conds.MaxwellBoundCondsSocketDef
from . import medium, medium_non_linearity from . import medium
from . import medium_non_linearity
MaxwellMediumSocketDef = medium.MaxwellMediumSocketDef MaxwellMediumSocketDef = medium.MaxwellMediumSocketDef
MaxwellMediumNonLinearitySocketDef = ( MaxwellMediumNonLinearitySocketDef = (
medium_non_linearity.MaxwellMediumNonLinearitySocketDef medium_non_linearity.MaxwellMediumNonLinearitySocketDef
) )
from . import source, temporal_shape from . import source
from . import temporal_shape
MaxwellSourceSocketDef = source.MaxwellSourceSocketDef MaxwellSourceSocketDef = source.MaxwellSourceSocketDef
MaxwellTemporalShapeSocketDef = temporal_shape.MaxwellTemporalShapeSocketDef MaxwellTemporalShapeSocketDef = temporal_shape.MaxwellTemporalShapeSocketDef
@ -23,7 +26,11 @@ from . import monitor
MaxwellMonitorSocketDef = monitor.MaxwellMonitorSocketDef MaxwellMonitorSocketDef = monitor.MaxwellMonitorSocketDef
from . import fdtd_sim, fdtd_sim_data, sim_domain, sim_grid, sim_grid_axis from . import fdtd_sim
from . import fdtd_sim_data
from . import sim_grid
from . import sim_grid_axis
from . import sim_domain
MaxwellFDTDSimSocketDef = fdtd_sim.MaxwellFDTDSimSocketDef MaxwellFDTDSimSocketDef = fdtd_sim.MaxwellFDTDSimSocketDef
MaxwellFDTDSimDataSocketDef = fdtd_sim_data.MaxwellFDTDSimDataSocketDef MaxwellFDTDSimDataSocketDef = fdtd_sim_data.MaxwellFDTDSimDataSocketDef

View File

@ -1,10 +1,12 @@
import typing as typ
import typing_extensions as typx
import bpy import bpy
import pydantic as pyd import pydantic as pyd
import tidy3d as td import tidy3d as td
import typing_extensions as typx
from ... import contracts as ct
from .. import base from .. import base
from ... import contracts as ct
class MaxwellBoundCondBLSocket(base.MaxwellSimSocket): class MaxwellBoundCondBLSocket(base.MaxwellSimSocket):
@ -24,7 +26,9 @@ class MaxwellBoundCondBLSocket(base.MaxwellSimSocket):
('PERIODIC', 'Periodic', 'Infinitely periodic layer'), ('PERIODIC', 'Periodic', 'Infinitely periodic layer'),
], ],
default='PML', default='PML',
update=(lambda self, context: self.sync_prop('default_choice', context)), update=(
lambda self, context: self.sync_prop('default_choice', context)
),
) )
#################### ####################
@ -46,7 +50,9 @@ class MaxwellBoundCondBLSocket(base.MaxwellSimSocket):
}[self.default_choice] }[self.default_choice]
@value.setter @value.setter
def value(self, value: typx.Literal['PML', 'PEC', 'PMC', 'PERIODIC']) -> None: def value(
self, value: typx.Literal['PML', 'PEC', 'PMC', 'PERIODIC']
) -> None:
self.default_choice = value self.default_choice = value

View File

@ -1,9 +1,11 @@
import typing as typ
import bpy import bpy
import pydantic as pyd import pydantic as pyd
import tidy3d as td import tidy3d as td
from ... import contracts as ct
from .. import base from .. import base
from ... import contracts as ct
BOUND_FACE_ITEMS = [ BOUND_FACE_ITEMS = [
('PML', 'PML', 'Perfectly matched layer'), ('PML', 'PML', 'Perfectly matched layer'),
@ -30,7 +32,9 @@ class MaxwellBoundCondsBLSocket(base.MaxwellSimSocket):
name='Show Bounds Definition', name='Show Bounds Definition',
description='Toggle to show bound faces', description='Toggle to show bound faces',
default=False, default=False,
update=(lambda self, context: self.sync_prop('show_definition', context)), update=(
lambda self, context: self.sync_prop('show_definition', context)
),
) )
x_pos: bpy.props.EnumProperty( x_pos: bpy.props.EnumProperty(
@ -81,7 +85,9 @@ class MaxwellBoundCondsBLSocket(base.MaxwellSimSocket):
#################### ####################
def draw_label_row(self, row: bpy.types.UILayout, text) -> None: def draw_label_row(self, row: bpy.types.UILayout, text) -> None:
row.label(text=text) row.label(text=text)
row.prop(self, 'show_definition', toggle=True, text='', icon='MOD_LENGTH') row.prop(
self, 'show_definition', toggle=True, text='', icon='MOD_LENGTH'
)
def draw_value(self, col: bpy.types.UILayout) -> None: def draw_value(self, col: bpy.types.UILayout) -> None:
if not self.show_definition: if not self.show_definition:

Some files were not shown because too many files have changed in this diff Show More