Compare commits

...

6 Commits

153 changed files with 2953 additions and 2455 deletions

33
TODO.md
View File

@ -33,6 +33,7 @@
## Outputs
[x] Viewer
- [ ] **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.
- [ ] A setting that live-previews just a value.
- [ ] Pop-up multiline string print as alternative to console print.
@ -174,13 +175,24 @@
[ ] Tests / Monkey (suzanne deserves to be simulated, she may need manifolding up though :))
[ ] Tests / Wood Pile
[ ] Primitives / Plane
[ ] Primitives / Box
[ ] Primitives / Sphere
[ ] Primitives / Cylinder
[ ] Primitives / Ring
[ ] Primitives / Capsule
[ ] Primitives / Cone
[ ] Structures / Primitives / Plane
[x] Structures / Primitives / Box
[x] Structures / Primitives / Sphere
[ ] Structures / Primitives / Cylinder
[x] Structures / Primitives / Ring
[ ] Structures / Primitives / Capsule
[ ] Structures / 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 / Hex Array **NOTE: Ring and cylinder**
@ -337,12 +349,15 @@
# Architecture
## CRITICAL
With these things in place
With these things in place, we're in tip top shape:
[ ] 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.
- 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.
[ ] 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
[x] Finish the contract code converting from Blender sockets to our sockets based on dimensionality and the property description.

View File

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

View File

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

View File

@ -0,0 +1,14 @@
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) 100644

Binary file not shown.

View File

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

View File

@ -0,0 +1,10 @@
# 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.

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

Binary file not shown.

View File

@ -0,0 +1,320 @@
"""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,38 +3,54 @@ from pathlib import Path
import bpy
PATH_ADDON_ROOT = Path(__file__).resolve().parent
####################
# - 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:
PROJ_SPEC = tomllib.load(f)
## bl_info is filled with PROJ_SPEC when packing the .zip.
ADDON_NAME = PROJ_SPEC['project']['name']
ADDON_VERSION = PROJ_SPEC['project']['version']
# PyDeps Path Info
## requirements.lock is written when packing the .zip.
## By default, the addon pydeps are kept in the addon dir.
####################
# - 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.
## By default, the addon pydeps are kept in the addon dir.
# 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.
## The initial .log_level contents are written when packing the .zip.
## 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'
with PATH_BOOTSTRAP_LOG_LEVEL.open('r') as f:
BOOTSTRAP_LOG_LEVEL = int(f.read().strip())
####################
# - Addon Getters
# - Addon Prefs Info
####################
def addon_prefs() -> bpy.types.AddonPreferences | None:
if (addon := bpy.context.preferences.addons.get(ADDON_NAME)) is None:

View File

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

View File

@ -1,44 +1,29 @@
"""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 bpy
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 . import contracts as ct
from . import sockets as sck
from .contracts import SocketType as ST
from . import sockets
log = _logger.get(__name__)
# TODO: Caching?
# TODO: Move the manual labor stuff to contracts
BLSocketType = str ## A Blender-Defined Socket Type
BLSocketSize = int
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)
BLSocketType: typ.TypeAlias = str ## A Blender-Defined Socket Type
BLSocketValue: typ.TypeAlias = typ.Any ## A Blender Socket Value
BLSocketSize: typ.TypeAlias = int
DescType: typ.TypeAlias = str
Unit: typ.TypeAlias = typ.Any ## Type of a valid unit
## TODO: Move this kind of thing to contracts
####################
@ -53,9 +38,11 @@ BL_SOCKET_4D_TYPE_PREFIXES = {
}
def size_from_bl_interface_socket(
bl_interface_socket: bpy.types.NodeTreeInterfaceSocket,
) -> typx.Literal[1, 2, 3, 4]:
@functools.lru_cache(maxsize=4096)
def _size_from_bl_socket(
description: str,
bl_socket_type: BLSocketType,
):
"""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.
@ -65,15 +52,15 @@ def size_from_bl_interface_socket(
- For 3D sockets, a hard-coded list of Blender node socket types is used.
- Else, it is a 1D socket type.
"""
if bl_interface_socket.description.startswith('2D'):
if description.startswith('2D'):
return 2
if any(
bl_interface_socket.socket_type.startswith(bl_socket_3d_type_prefix)
bl_socket_type.startswith(bl_socket_3d_type_prefix)
for bl_socket_3d_type_prefix in BL_SOCKET_3D_TYPE_PREFIXES
):
return 3
if any(
bl_interface_socket.socket_type.startswith(bl_socket_4d_type_prefix)
bl_socket_type.startswith(bl_socket_4d_type_prefix)
for bl_socket_4d_type_prefix in BL_SOCKET_4D_TYPE_PREFIXES
):
return 4
@ -84,176 +71,185 @@ def size_from_bl_interface_socket(
####################
# - BL Socket Type / Unit Parser
####################
def parse_bl_interface_socket(
bl_interface_socket: bpy.types.NodeTreeInterfaceSocket,
) -> tuple[ST, sp.Expr | None]:
"""Parse a Blender interface socket by parsing its description, falling back to any direct type links.
@functools.lru_cache(maxsize=4096)
def _socket_type_from_bl_socket(
description: str,
bl_socket_type: BLSocketType,
) -> 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:
bl_interface_socket: An interface socket associated with the global input to a node tree.
description: The description from Blender socket, aka. `bl_socket.description`.
bl_socket_type: The Blender socket type, aka. `bl_socket.socket_type`.
Returns:
The type of a corresponding MaxwellSimSocket, as well as a unit (if a particular unit was requested by the Blender interface socket).
The type of a MaxwellSimSocket that corresponds to the Blender socket.
"""
size = size_from_bl_interface_socket(bl_interface_socket)
size = _size_from_bl_socket(description, bl_socket_type)
# Determine Direct Socket Type
# Determine Socket Type Directly
## The naive mapping from BL socket -> Maxwell socket may be good enough.
if (
direct_socket_type := ct.BL_SOCKET_DIRECT_TYPE_MAP.get(
(bl_interface_socket.socket_type, size)
)
direct_socket_type := ct.BL_SOCKET_DIRECT_TYPE_MAP.get((bl_socket_type, size))
) is None:
msg = "Blender interface socket has no mapping among 'MaxwellSimSocket's."
raise ValueError(msg)
# (Maybe) Return Direct Socket Type
## When there's no description, that's it; return.
if not ct.BL_SOCKET_DESCR_ANNOT_STRING in bl_interface_socket.description:
return (direct_socket_type, None)
# (No Description) Return Direct Socket Type
if ct.BL_SOCKET_DESCR_ANNOT_STRING not in description:
return direct_socket_type
# Parse Description for Socket Type
tokens = (
_tokens
if (_tokens := bl_interface_socket.description.split(' '))[0] != '2D'
else _tokens[1:]
) ## Don't include the "2D" token, if defined.
## The "2D" token is special; don't include it if it's there.
descr_params = description.split(ct.BL_SOCKET_DESCR_ANNOT_STRING)[0]
directive = (
_tokens[0] if (_tokens := descr_params.split(' '))[0] != '2D' else _tokens[1]
)
if directive == 'Preview':
return direct_socket_type ## TODO: Preview element handling
if (
socket_type := ct.BL_SOCKET_DESCR_TYPE_MAP.get(
(tokens[0], bl_interface_socket.socket_type, size)
(directive, bl_socket_type, size)
)
) is None:
return (
direct_socket_type,
None,
) ## Description doesn't map to anything
msg = f'Socket description "{(directive, bl_socket_type, size)}" doesn\'t map to a socket type + unit'
raise ValueError(msg)
# 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)
return socket_type
####################
# - BL Socket Interface Definition
####################
def socket_def_from_bl_interface_socket(
@functools.lru_cache(maxsize=4096)
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,
):
) -> ct.schemas.SocketDef:
"""Computes an appropriate (no-arg) SocketDef from the given `bl_interface_socket`, by parsing it."""
return SOCKET_DEFS[parse_bl_interface_socket(bl_interface_socket)[0]]
return _socket_def_from_bl_socket(
bl_interface_socket.description, bl_interface_socket.bl_socket_idname
)
####################
# - Extract Default Interface Socket Value
####################
def value_from_bl(
def _read_bl_socket_default_value(
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,
unit_system: dict | None = None,
allow_unit_not_in_unit_system: bool = False,
) -> typ.Any:
"""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`.
- 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`.
"""Reads the `default_value` of a Blender socket, guaranteeing a well-formed value consistent with the passed unit system.
Arguments:
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.
"""
## TODO: Consider sympy.S()'ing the default_value
parsed_bl_socket_value = {
1: lambda: bl_interface_socket.default_value,
2: lambda: sp.Matrix(tuple(bl_interface_socket.default_value)[:2]),
3: lambda: sp.Matrix(tuple(bl_interface_socket.default_value)),
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
return _read_bl_socket_default_value(
bl_interface_socket.description,
bl_interface_socket.bl_socket_idname,
bl_interface_socket.default_value,
unit_system=unit_system,
allow_unit_not_in_unit_system=allow_unit_not_in_unit_system,
)
####################
# - Convert to Blender-Compatible Value
####################
def make_scalar_bl_compat(scalar: typ.Any) -> typ.Any:
"""Blender doesn't accept ex. Sympy numbers as values.
Therefore, we need to do some conforming.
Currently hard-coded; this is probably best.
"""
if isinstance(scalar, sp.Integer):
return int(scalar)
elif isinstance(scalar, sp.Float):
return float(scalar)
elif isinstance(scalar, sp.Rational):
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,
def _writable_bl_socket_value(
description: str,
bl_socket_type: BLSocketType,
value: typ.Any,
unit_system: dict | None = None,
allow_unit_not_in_unit_system: bool = False,
) -> typ.Any:
socket_type, unit = parse_bl_interface_socket(bl_interface_socket)
socket_type = _socket_type_from_bl_socket(description, bl_socket_type)
# 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]
)
# Retrieve Unit-System Unit
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
_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
# 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(
bl_interface_socket.description,
bl_interface_socket.bl_socket_idname,
value,
unit_system=unit_system,
allow_unit_not_in_unit_system=allow_unit_not_in_unit_system,
)

View File

@ -2,6 +2,7 @@
import bpy
import nodeitems_utils
from . import contracts as ct
from .nodes import BL_NODES
@ -81,9 +82,7 @@ BL_NODE_CATEGORIES = mk_node_categories(
syllable_prefix=['MAXWELLSIM'],
)
## 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

View File

@ -30,6 +30,8 @@ from .socket_units import SOCKET_UNITS
from .socket_colors import SOCKET_COLORS
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_direct import BL_SOCKET_DIRECT_TYPE_MAP
@ -73,6 +75,8 @@ __all__ = [
'SOCKET_UNITS',
'SOCKET_COLORS',
'SOCKET_SHAPES',
'UNITS_BLENDER',
'UNITS_TIDY3D',
'BL_SOCKET_DESCR_TYPE_MAP',
'BL_SOCKET_DIRECT_TYPE_MAP',
'BL_SOCKET_DESCR_ANNOT_STRING',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,58 @@
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,2 +1,19 @@
#from .managed_bl_empty import ManagedBLEmpty
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

@ -0,0 +1,42 @@
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,12 +1,10 @@
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 typing as typ
import bpy
import matplotlib.axis as mpl_ax
import numpy as np
import typing_extensions as typx
from .. import contracts as ct
@ -109,9 +107,7 @@ class ManagedBLImage(ct.schemas.ManagedObj):
"""
if preview_area := self.preview_area:
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
)
####################
@ -161,7 +157,7 @@ class ManagedBLImage(ct.schemas.ManagedObj):
height_px = int(_height_inches * _dpi)
else:
msg = f'There must either be a preview area, or defined `width_inches`, `height_inches`, and `dpi`'
msg = 'There must either be a preview area, or defined `width_inches`, `height_inches`, and `dpi`'
raise ValueError(msg)
# Compute Plot Dimensions

View File

@ -0,0 +1,237 @@
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

@ -0,0 +1,212 @@
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 typing as typ
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
import bpy
import numpy as np
import typing_extensions as typx
from ....utils import logger
from .. import contracts as ct
from .managed_bl_collection import managed_collection, preview_collection
log = logger.get(__name__)
ModifierType = typx.Literal['NODES', 'ARRAY']
MODIFIER_NAMES = {
'NODES': 'BLMaxwell_GeoNodes',
'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):
managed_obj_type = ct.ManagedObjType.ManagedBLObject
_bl_object_name: str
_bl_object_name: str | None = None
def __init__(self, name: str):
self._bl_object_name = name
# Object Name
####################
# - BL Object Name
####################
@property
def name(self):
return self._bl_object_name
@name.setter
def set_name(self, value: str) -> None:
# Object Doesn't Exist
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):
self._bl_object_name = value
return
def name(self, value: str) -> None:
log.info(
'Changing BLObject w/Name "%s" to Name "%s"', self._bl_object_name, value
)
# ...AND Desired Object Name is Taken
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'Desired name {value} for BL object is taken'
raise ValueError(msg)
msg = f'ManagedBLObject with name "{self._bl_object_name}" was deleted'
raise RuntimeError(msg)
# Object DOES Exist
bl_object.name = value
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.
# Set Internal Name
self._bl_object_name = value
else:
log.info(
'Desired BLObject Name "%s" is Taken. Using Blender Rename',
value,
)
# Object Datablock Name
@property
def bl_mesh_name(self):
return self.name
# 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
@property
def bl_volume_name(self):
return self.name
log.info(
'Changed BLObject Name to "%s"',
bl_object.name,
)
# Deallocation
####################
# - Allocation
####################
def __init__(self, name: str):
self.name = name
####################
# - Deallocation
####################
def free(self):
if not (bl_object := bpy.data.objects.get(self.name)):
return ## Nothing to do
if (bl_object := bpy.data.objects.get(self.name)) is None:
return
# Delete the Underlying Datablock
## This automatically deletes the object too
if bl_object.type == 'MESH':
bpy.data.meshes.remove(bl_object.data)
elif bl_object.type == 'EMPTY':
log.info('Removing "%s" BLObject', bl_object.type)
if bl_object.type in {'MESH', 'EMPTY'}:
bpy.data.meshes.remove(bl_object.data)
elif bl_object.type == 'VOLUME':
bpy.data.volumes.remove(bl_object.data)
else:
msg = f'Type of to-delete `bl_object`, {bl_object.type}, is not valid'
raise ValueError(msg)
msg = f'BLObject "{bl_object.name}" has invalid kind "{bl_object.type}"'
raise RuntimeError(msg)
####################
# - Actions
@ -125,17 +125,17 @@ class ManagedBLObject(ct.schemas.ManagedObj):
If it's already included, do nothing.
"""
bl_object = self.bl_object(kind)
if (
bl_object.name
not in (
preview_collection := bl_collection(
PREVIEW_COLLECTION_NAME, view_layer_exclude=False
)
).objects
):
preview_collection.objects.link(bl_object)
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)
# Display Parameters
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,
)
bl_object.empty_display_type = empty_display_type
def hide_preview(
@ -147,29 +147,23 @@ class ManagedBLObject(ct.schemas.ManagedObj):
If it's already removed, do nothing.
"""
bl_object = self.bl_object(kind)
if (
bl_object.name
not in (
preview_collection := bl_collection(
PREVIEW_COLLECTION_NAME, view_layer_exclude=False
)
).objects
):
if bl_object.name not in preview_collection().objects:
log.info('Removing "%s" from Preview Collection', bl_object.name)
preview_collection.objects.unlink(bl_object)
def bl_select(self) -> None:
"""Selects the managed Blender object globally, causing it to be ex.
outlined in the 3D viewport.
"""
if not (bl_object := bpy.data.objects.get(self.name)):
msg = 'Managed BLObject does not exist'
raise ValueError(msg)
if (bl_object := bpy.data.objects.get(self.name)) is not None:
bpy.ops.object.select_all(action='DESELECT')
bl_object.select_set(True)
bpy.ops.object.select_all(action='DESELECT')
bl_object.select_set(True)
msg = 'Managed BLObject does not exist'
raise ValueError(msg)
####################
# - Managed Object Management
# - BLObject Management
####################
def bl_object(
self,
@ -177,33 +171,42 @@ class ManagedBLObject(ct.schemas.ManagedObj):
):
"""Returns the managed blender object.
If the requested object data type is different, then delete the old
If the requested object data kind is different, then delete the old
object and recreate.
"""
# Remove Object (if mismatch)
if (
bl_object := bpy.data.objects.get(self.name)
) and bl_object.type != kind:
if (bl_object := bpy.data.objects.get(self.name)) and bl_object.type != kind:
log.info(
'Removing (recreating) "%s" (existing kind is "%s", but "%s" is requested)',
bl_object.name,
bl_object.type,
kind,
)
self.free()
# Create Object w/Appropriate Data Block
if not (bl_object := bpy.data.objects.get(self.name)):
log.info(
'Creating "%s" with kind "%s"',
self.name,
kind,
)
if kind == 'MESH':
bl_data = bpy.data.meshes.new(self.bl_mesh_name)
bl_data = bpy.data.meshes.new(self.name)
elif kind == 'EMPTY':
bl_data = None
elif kind == 'VOLUME':
raise NotImplementedError
else:
msg = (
f'Requested `bl_object` type {bl_object.type} is not valid'
)
msg = f'Created BLObject w/invalid kind "{bl_object.type}" for "{self.name}"'
raise ValueError(msg)
bl_object = bpy.data.objects.new(self.name, bl_data)
bl_collection(
MANAGED_COLLECTION_NAME, view_layer_exclude=True
).objects.link(bl_object)
log.debug(
'Linking "%s" to Base Collection',
bl_object.name,
)
managed_collection().objects.link(bl_object)
return bl_object
@ -211,17 +214,16 @@ class ManagedBLObject(ct.schemas.ManagedObj):
# - Mesh Data Properties
####################
@property
def raw_mesh(self) -> bpy.types.Mesh:
"""Returns the object's raw mesh data.
def mesh_data(self) -> bpy.types.Mesh:
"""Directly loads the Blender mesh data.
Raises an error if the object has no mesh data.
Raises:
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
msg = f'Requested MESH data from `bl_object` of type {bl_object.type}'
msg = f'Requested mesh data from {self.name} of type {bl_object.type}'
raise ValueError(msg)
@contextlib.contextmanager
@ -230,9 +232,7 @@ class ManagedBLObject(ct.schemas.ManagedObj):
evaluate: bool = True,
triangulate: bool = False,
) -> 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
try:
bmesh_mesh = bmesh.new()
@ -254,7 +254,7 @@ class ManagedBLObject(ct.schemas.ManagedObj):
bmesh_mesh.free()
else:
msg = f'Requested BMesh from `bl_object` of type {bl_object.type}'
msg = f'Requested BMesh from "{self.name}" of type "{bl_object.type}"'
raise ValueError(msg)
@property
@ -262,27 +262,31 @@ class ManagedBLObject(ct.schemas.ManagedObj):
## TODO: Cached
# Ensure Updated Geometry
log.debug('Updating View Layer')
bpy.context.view_layer.update()
## TODO: Must we?
# 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 {
@ -291,7 +295,7 @@ class ManagedBLObject(ct.schemas.ManagedObj):
}
####################
# - Modifier Methods
# - Modifiers
####################
def bl_modifier(
self,
@ -299,10 +303,10 @@ class ManagedBLObject(ct.schemas.ManagedObj):
):
"""Creates a new modifier for the current `bl_object`.
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>
- Modifier Type Names: <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)):
msg = "Can't add modifier to BL object that doesn't exist"
msg = f'Tried to add modifier to "{self.name}", but it has no bl_object'
raise ValueError(msg)
# (Create and) Return Modifier
@ -376,8 +380,7 @@ class ManagedBLObject(ct.schemas.ManagedObj):
# Quickly Determine if IDPropertyArray is Equal
if (
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
@ -395,18 +398,3 @@ class ManagedBLObject(ct.schemas.ManagedObj):
# Update DepGraph (if anything changed)
if modifier_altered:
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,7 +2,11 @@ import typing as typ
import bpy
from ...utils import logger
from . import contracts as ct
from .managed_objs.managed_bl_collection import preview_collection
log = logger.get(__name__)
####################
# - Cache Management
@ -73,6 +77,15 @@ class MaxwellSimTree(bpy.types.NodeTree):
for bl_socket in [*node.inputs, *node.outputs]:
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
####################
@ -125,9 +138,7 @@ class MaxwellSimTree(bpy.types.NodeTree):
'to_add': [],
}
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]
# Update Socket Caches
@ -136,9 +147,7 @@ class MaxwellSimTree(bpy.types.NodeTree):
# Trigger Report Chain on Socket that Just Lost a Link
## 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
link_alterations['to_add'].append((from_socket, to_socket))

View File

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

View File

@ -1,4 +1,3 @@
import inspect
import json
import typing as typ
import uuid
@ -22,7 +21,7 @@ _DEFAULT_LOOSE_SOCKET_SER = json.dumps(
'socket_def_names': [],
'models': [],
}
)
) ## TODO: What in the jesus christ is this
class MaxwellSimNode(bpy.types.Node):
@ -43,16 +42,18 @@ class MaxwellSimNode(bpy.types.Node):
# Sockets
_output_socket_methods: dict
input_sockets: dict[str, ct.schemas.SocketDef] = {}
output_sockets: dict[str, ct.schemas.SocketDef] = {}
input_socket_sets: dict[str, dict[str, ct.schemas.SocketDef]] = {}
output_socket_sets: dict[str, dict[str, ct.schemas.SocketDef]] = {}
input_sockets: typ.ClassVar[dict[str, ct.schemas.SocketDef]] = {}
output_sockets: typ.ClassVar[dict[str, ct.schemas.SocketDef]] = {}
input_socket_sets: typ.ClassVar[dict[str, dict[str, ct.schemas.SocketDef]]] = {}
output_socket_sets: typ.ClassVar[dict[str, dict[str, ct.schemas.SocketDef]]] = {}
# Presets
presets = {}
presets: typ.ClassVar = {}
# Managed Objects
managed_obj_defs: dict[ct.ManagedObjName, ct.schemas.ManagedObjDef] = {}
managed_obj_defs: typ.ClassVar[
dict[ct.ManagedObjName, ct.schemas.ManagedObjDef]
] = {}
####################
# - Initialization
@ -82,6 +83,14 @@ class MaxwellSimNode(bpy.types.Node):
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
cls.__annotations__['locked'] = bpy.props.BoolProperty(
name='Locked State',
@ -96,34 +105,30 @@ class MaxwellSimNode(bpy.types.Node):
# Setup Callback Methods
cls._output_socket_methods = {
method._index_by: method
(method.extra_data['output_socket_name'], method.extra_data['kind']): method
for attr_name in dir(cls)
if hasattr(method := getattr(cls, attr_name), '_callback_type')
and method._callback_type == 'computes_output_socket'
if hasattr(method := getattr(cls, attr_name), 'action_type')
and method.action_type == 'computes_output_socket'
and hasattr(method, 'extra_data')
and method.extra_data
}
cls._on_value_changed_methods = {
method
for attr_name in dir(cls)
if hasattr(method := getattr(cls, attr_name), '_callback_type')
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'
if hasattr(method := getattr(cls, attr_name), 'action_type')
and method.action_type == 'on_value_changed'
}
cls._on_show_plot = {
method
for attr_name in dir(cls)
if hasattr(method := getattr(cls, attr_name), '_callback_type')
and method._callback_type == 'on_show_plot'
if hasattr(method := getattr(cls, attr_name), 'action_type')
and method.action_type == 'on_show_plot'
}
cls._on_init = {
method
for attr_name in dir(cls)
if hasattr(method := getattr(cls, attr_name), '_callback_type')
and method._callback_type == 'on_init'
if hasattr(method := getattr(cls, attr_name), 'action_type')
and method.action_type == 'on_init'
}
# Setup Socket Set Dropdown
@ -135,7 +140,7 @@ class MaxwellSimNode(bpy.types.Node):
_input_socket_set_names := list(cls.input_socket_sets.keys())
) + [
output_socket_set_name
for output_socket_set_name in cls.output_socket_sets.keys()
for output_socket_set_name in cls.output_socket_sets
if output_socket_set_name not in _input_socket_set_names
]
socket_set_ids = [
@ -157,12 +162,11 @@ class MaxwellSimNode(bpy.types.Node):
for socket_set_id, socket_set_name in zip(
socket_set_ids,
socket_set_names,
strict=False,
)
],
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
@ -181,8 +185,8 @@ class MaxwellSimNode(bpy.types.Node):
)
for preset_name, preset_def in cls.presets.items()
],
default=list(cls.presets.keys())[0],
update=lambda self, context: (self.sync_active_preset()()),
default=next(cls.presets.keys()),
update=lambda self, _: (self.sync_active_preset()()),
)
####################
@ -192,7 +196,7 @@ class MaxwellSimNode(bpy.types.Node):
self.sync_sockets()
self.sync_prop('active_socket_set', context)
def sync_sim_node_name(self, context):
def sync_sim_node_name(self, _):
if (mobjs := CACHE[self.instance_id].get('managed_objs')) is None:
return
@ -207,12 +211,26 @@ class MaxwellSimNode(bpy.types.Node):
## - If altered, set the 'sim_node_name' to the altered name.
## - 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
####################
@property
def managed_objs(self):
global CACHE
if not CACHE.get(self.instance_id):
CACHE[self.instance_id] = {}
@ -229,9 +247,7 @@ class MaxwellSimNode(bpy.types.Node):
# Fill w/Managed Objects by Name Socket
for mobj_id, mobj_def in self.managed_obj_defs.items():
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']
@ -253,9 +269,7 @@ class MaxwellSimNode(bpy.types.Node):
# Retrieve Active Socket Set Sockets
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)
@ -265,24 +279,13 @@ class MaxwellSimNode(bpy.types.Node):
return active_socket_set_sockets
def active_sockets(self, direc: typx.Literal['input', 'output']):
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
)
static_sockets = self.input_sockets if direc == 'input' else self.output_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 (
static_sockets
| self.active_socket_set_sockets(direc=direc)
| loose_sockets
static_sockets | self.active_socket_set_sockets(direc=direc) | loose_sockets
)
####################
@ -302,12 +305,8 @@ class MaxwellSimNode(bpy.types.Node):
)
## Internal Serialization/Deserialization Methods (yuck)
def _ser_loose_sockets(
self, deser: dict[str, ct.schemas.SocketDef]
) -> str:
if not all(
isinstance(model, pyd.BaseModel) for model in deser.values()
):
def _ser_loose_sockets(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).'
raise ValueError(msg)
@ -325,9 +324,7 @@ class MaxwellSimNode(bpy.types.Node):
}
) ## 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)
return {
socket_name: getattr(sockets, socket_def_name)(**model_kwargs)
@ -335,6 +332,7 @@ class MaxwellSimNode(bpy.types.Node):
semi_deser['socket_names'],
semi_deser['socket_def_names'],
semi_deser['models'],
strict=False,
)
if hasattr(sockets, socket_def_name)
}
@ -354,6 +352,11 @@ class MaxwellSimNode(bpy.types.Node):
self,
value: dict[str, ct.schemas.SocketDef],
) -> None:
log.info(
'Setting Loose Input Sockets on "%s" to "%s"',
self.bl_label,
str(value),
)
if not value:
self.ser_loose_input_sockets = _DEFAULT_LOOSE_SOCKET_SER
else:
@ -448,9 +451,7 @@ class MaxwellSimNode(bpy.types.Node):
# - Preset Management
####################
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)):
msg = f'Tried to apply active preset, but the active preset ({self.active_preset}) is not in presets ({self.presets})'
raise RuntimeError(msg)
@ -507,7 +508,7 @@ class MaxwellSimNode(bpy.types.Node):
## TODO: Side panel buttons for fanciness.
def draw_plot_settings(self, context, layout):
def draw_plot_settings(self, _: bpy.types.Context, layout: bpy.types.UILayout):
if self.locked:
layout.enabled = False
@ -522,16 +523,14 @@ 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`.
Args:
input_socket_name: The name of the input socket, as defined in
`self.input_sockets`.
kind: The data flow kind to compute retrieve.
input_socket_name: The name of the input socket, as defined in `self.input_sockets`.
kind: The kind of data flow to compute.
"""
if not (bl_socket := self.inputs.get(input_socket_name)):
return None
# msg = f"Input socket name {input_socket_name} is not an active input sockets."
# raise ValueError(msg)
if bl_socket := self.inputs.get(input_socket_name):
return bl_socket.compute_data(kind=kind)
return bl_socket.compute_data(kind=kind)
msg = f'Input socket "{input_socket_name}" on "{self.bl_idname}" is not an active input socket'
raise ValueError(msg)
def compute_output(
self,
@ -544,27 +543,25 @@ class MaxwellSimNode(bpy.types.Node):
This method is run to produce the value.
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, for which this method computes the output.
kind: The DataFlowKind to use when computing the output socket value.
Returns:
The value of the output socket, as computed by the dedicated method
registered using the `@computes_output_socket` decorator.
"""
if not (
output_socket_method := self._output_socket_methods.get(
(output_socket_name, kind)
)
if output_socket_method := self._output_socket_methods.get(
(output_socket_name, kind)
):
msg = f'No output method for ({output_socket_name}, {str(kind.value)}'
raise ValueError(msg)
return output_socket_method(self)
return output_socket_method(self)
msg = f'No output method for ({output_socket_name}, {kind.value!s}'
raise ValueError(msg)
####################
# - Action Chain
####################
def sync_prop(self, prop_name: str, context: bpy.types.Context):
def sync_prop(self, prop_name: str, _: bpy.types.Context):
"""Called when a property has been updated."""
if not hasattr(self, prop_name):
msg = f'Property {prop_name} not defined on socket {self}'
@ -589,6 +586,13 @@ class MaxwellSimNode(bpy.types.Node):
Invalidates (recursively) the cache of any managed object or
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
if action == 'value_changed':
# Run User Callbacks
@ -598,20 +602,20 @@ class MaxwellSimNode(bpy.types.Node):
if (
(
socket_name
and socket_name
in method._extra_data.get('changed_sockets')
)
or (
prop_name
and prop_name
in method._extra_data.get('changed_props')
and socket_name in method.extra_data['changed_sockets']
)
or (prop_name and prop_name in method.extra_data['changed_props'])
or (
socket_name
and method._extra_data['changed_loose_input']
and method.extra_data['changed_loose_input']
and socket_name in self.loose_input_sockets
)
):
#log.debug(
# 'Running Value-Change Callback "%s" in "%s")',
# method.__name__,
# self.name,
#)
method(self)
# Propagate via Output Sockets
@ -635,8 +639,15 @@ class MaxwellSimNode(bpy.types.Node):
elif action == 'show_preview':
# Run User Callbacks
for method in self._on_show_preview:
method(self)
## "On Show Preview" callbacks are 'on_value_changed' callbacks...
## ...which simply hook into the 'preview_active' property.
## 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
for bl_socket in self.active_bl_sockets('input'):
@ -648,7 +659,7 @@ class MaxwellSimNode(bpy.types.Node):
## ...because they can stop propagation, they should go first.
for method in self._on_show_plot:
method(self)
if method._extra_data['stop_propagation']:
if method.extra_data['stop_propagation']:
return
## Propagate via Input Sockets
@ -664,13 +675,10 @@ class MaxwellSimNode(bpy.types.Node):
Restricted to the MaxwellSimTreeType.
"""
return node_tree.bl_idname == ct.TreeType.MaxwellSim.value
def init(self, context: bpy.types.Context):
"""Run (by Blender) on node creation."""
global CACHE
# Initialize Cache and Instance ID
self.instance_id = str(uuid.uuid4())
CACHE[self.instance_id] = {}
@ -695,7 +703,6 @@ class MaxwellSimNode(bpy.types.Node):
def free(self) -> None:
"""Run (by Blender) when deleting the node."""
global CACHE
if not CACHE.get(self.instance_id):
CACHE[self.instance_id] = {}
node_tree = self.id_data
@ -725,306 +732,3 @@ class MaxwellSimNode(bpy.types.Node):
# Finally: Free Instance Cache
if self.instance_id in CACHE:
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,5 +1,4 @@
from . import bound_box
from . import bound_faces
from . import bound_box, bound_faces
BL_REGISTER = [
*bound_box.BL_REGISTER,

View File

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

View File

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

View File

@ -0,0 +1,294 @@
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,8 +1,8 @@
from . import (
constants,
unit_system,
wave_constant,
web_importers,
constants,
unit_system,
wave_constant,
web_importers,
)
# from . import file_importers

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,15 +1,20 @@
import tempfile
import typing as typ
from pathlib import Path
import tidy3d as td
import tidy3d.web as td_web
from ...... import info
from ......services import tdcloud
from .... import contracts as ct
from .... import sockets
from ... import base
from ... import base, events
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'
####################
@ -17,89 +22,44 @@ CACHE = {}
####################
class Tidy3DWebImporterNode(base.MaxwellSimNode):
node_type = ct.NodeType.Tidy3DWebImporter
bl_label = 'Tidy3DWebImporter'
bl_label = 'Tidy3D Web Importer'
input_sockets = {
input_sockets: typ.ClassVar = {
'Cloud Task': sockets.Tidy3DCloudTaskSocketDef(
should_exist=True,
),
'Cache Path': sockets.FilePathSocketDef(
default_path=Path('loaded_simulation.hdf5')
),
}
####################
# - Output Methods
# - Event Methods
####################
@base.computes_output_socket(
@events.computes_output_socket(
'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'},
)
def compute_fdtd_sim(self, input_sockets: dict) -> str:
if not isinstance(
cloud_task := input_sockets['Cloud Task'], tdcloud.CloudTask
):
msg = 'Input cloud task does not exist'
def compute_sim_data(self, input_sockets: dict) -> str:
# Validate Task Availability
if (cloud_task := input_sockets['Cloud Task']) is None:
msg = f'"{self.bl_label}" CloudTask doesn\'t exist'
raise RuntimeError(msg)
# Load the Simulation
with tempfile.NamedTemporaryFile(delete=False) as f:
_path_tmp = Path(f.name)
_path_tmp.rename(f.name + '.json')
path_tmp = Path(f.name + '.json')
# Validate Task Existence
if not isinstance(cloud_task, tdcloud.CloudTask):
msg = f'"{self.bl_label}" CloudTask input "{cloud_task}" has wrong "should_exists", as it isn\'t an instance of tdcloud.CloudTask'
raise TypeError(msg)
sim = td_web.api.webapi.load_simulation(
cloud_task.task_id,
path=str(path_tmp),
) ## TODO: Don't use td_web directly. Only through tdcloud
Path(path_tmp).unlink()
# Validate Task Status
if cloud_task.status != 'success':
msg = f'"{self.bl_label}" CloudTask is "{cloud_task.status}", not "success"'
raise RuntimeError(msg)
return sim
# Download and Return SimData
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 (
(cloud_task := input_sockets['Cloud Task']) is not None
and isinstance(cloud_task, tdcloud.CloudTask)
@ -107,15 +67,13 @@ class Tidy3DWebImporterNode(base.MaxwellSimNode):
):
self.loose_output_sockets = {
'FDTD Sim Data': sockets.MaxwellFDTDSimDataSocketDef(),
'FDTD Sim': sockets.MaxwellFDTDSimSocketDef(),
}
return
else:
self.loose_output_sockets = {}
self.loose_output_sockets = {}
@base.on_init()
@events.on_init()
def on_init(self):
self.on_value_changed__cloud_task()
self.on_cloud_task_changed()
####################
@ -125,7 +83,5 @@ BL_REGISTER = [
Tidy3DWebImporterNode,
]
BL_NODES = {
ct.NodeType.Tidy3DWebImporter: (
ct.NodeCategory.MAXWELLSIM_INPUTS_IMPORTERS
)
ct.NodeType.Tidy3DWebImporter: (ct.NodeCategory.MAXWELLSIM_INPUTS_IMPORTERS)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,8 @@
import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
import tidy3d as td
from ... import contracts
from ... import sockets
from .. import base
from ... import contracts, sockets
from .. import base, events
class TripleSellmeierMediumNode(base.MaxwellSimTreeNode):
@ -22,9 +20,7 @@ class TripleSellmeierMediumNode(base.MaxwellSimTreeNode):
)
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]
}
output_sockets = {
@ -64,7 +60,7 @@ class TripleSellmeierMediumNode(base.MaxwellSimTreeNode):
####################
# - Output Socket Computation
####################
@base.computes_output_socket('medium')
@events.computes_output_socket('medium')
def compute_medium(self: contracts.NodeTypeProtocol) -> td.Sellmeier:
## Retrieval
# B1 = self.compute_input("B1")

View File

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

View File

@ -1,24 +1,22 @@
import typing as typ
import functools
import bpy
import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
import numpy as np
import scipy as sc
import tidy3d as td
from .....utils import analyze_geonodes
from .....assets.import_geonodes import GeoNodes, import_geonodes
from .....utils import extra_sympy_units as spux
from .....utils import logger
from ... import contracts as ct
from ... import sockets
from ... import managed_objs
from .. import base
from ... import managed_objs, sockets
from .. import base, events
GEONODES_MONITOR_BOX = 'monitor_box'
log = logger.get(__name__)
class EHFieldMonitorNode(base.MaxwellSimNode):
"""Node providing for the monitoring of electromagnetic fields within a given planar region or volume."""
node_type = ct.NodeType.EHFieldMonitor
bl_label = 'E/H Field Monitor'
use_sim_node_name = True
@ -26,14 +24,14 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
####################
# - Sockets
####################
input_sockets = {
input_sockets: typ.ClassVar = {
'Center': sockets.PhysicalPoint3DSocketDef(),
'Size': sockets.PhysicalSize3DSocketDef(),
'Samples/Space': sockets.Integer3DVectorSocketDef(
default_value=sp.Matrix([10, 10, 10])
),
}
input_socket_sets = {
input_socket_sets: typ.ClassVar = {
'Freq Domain': {
'Freqs': sockets.PhysicalFreqSocketDef(
is_list=True,
@ -41,43 +39,31 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
},
'Time Domain': {
'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(
default_value=100,
),
},
}
output_sockets = {
output_sockets: typ.ClassVar = {
'Monitor': sockets.MaxwellMonitorSocketDef(),
}
managed_obj_defs = {
'monitor_box': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLObject(name),
name_prefix='',
)
managed_obj_defs: typ.ClassVar = {
'mesh': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLMesh(name),
),
'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
####################
@base.computes_output_socket(
@events.computes_output_socket(
'Monitor',
props={'active_socket_set', 'sim_node_name'},
input_sockets={
'Rec Start',
'Rec Stop',
@ -87,107 +73,90 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
'Samples/Time',
'Freqs',
},
props={'active_socket_set', 'sim_node_name'},
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
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:
_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)
self, input_sockets: dict, props: dict, unit_systems: dict,
) -> td.FieldMonitor | td.FieldTimeMonitor:
if props['active_socket_set'] == 'Freq Domain':
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(
center=center,
size=size,
center=input_sockets['Center'],
size=input_sockets['Size'],
name=props['sim_node_name'],
interval_space=samples_space,
interval_space=input_sockets['Samples/Space'],
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
],
)
else: ## Time Domain
_rec_start = input_sockets['Rec Start']
_rec_stop = input_sockets['Rec Stop']
samples_time = input_sockets['Samples/Time']
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(
center=center,
size=size,
name=props['sim_node_name'],
start=rec_start,
stop=rec_stop,
interval=samples_time,
interval_space=samples_space,
)
## Time Domain
log.info(
'Computing FieldTimeMonitor (name=%s) with center=%s, size=%s',
props['sim_node_name'],
input_sockets['Center'],
input_sockets['Size'],
)
return td.FieldTimeMonitor(
center=input_sockets['Center'],
size=input_sockets['Size'],
name=props['sim_node_name'],
start=input_sockets['Rec Start'],
stop=input_sockets['Rec Stop'],
interval=input_sockets['Samples/Time'],
interval_space=input_sockets['Samples/Space'],
)
####################
# - Preview - Changes to Input Sockets
####################
@base.on_value_changed(
@events.on_value_changed(
socket_name={'Center', 'Size'},
prop_name='preview_active',
props={'preview_active'},
input_sockets={'Center', 'Size'},
managed_objs={'monitor_box'},
managed_objs={'mesh', 'modifier'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={
'Center': 'BlenderUnits',
},
)
def on_value_changed__center_size(
def on_inputs_changed(
self,
input_sockets: dict,
props: dict,
managed_objs: dict[str, ct.schemas.ManagedObj],
input_sockets: dict,
unit_systems: dict,
):
_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_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 Input Values to GeoNodes Modifier
managed_objs['modifier'].bl_modifier(
managed_objs['mesh'].bl_object(location=input_sockets['Center']),
'NODES',
{
'node_group': import_geonodes(GeoNodes.PrimitiveBox, 'link'),
'unit_system': unit_systems['BlenderUnits'],
'inputs': {
'Size': input_sockets['Size'],
},
},
)
# Sync Object Position
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()
# Push Preview State
if props['preview_active']:
managed_objs['mesh'].show_preview()
####################

View File

@ -1,21 +1,18 @@
import typing as typ
import functools
import bpy
import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
import numpy as np
import scipy as sc
import tidy3d as td
from .....utils import analyze_geonodes
from .....assets.import_geonodes import GeoNodes, import_geonodes
from .....utils import extra_sympy_units as spux
from .....utils import logger
from ... import contracts as ct
from ... import sockets
from ... import managed_objs
from .. import base
from ... import managed_objs, sockets
from .. import base, events
GEONODES_MONITOR_BOX = 'monitor_flux_box'
log = logger.get(__name__)
class FieldPowerFluxMonitorNode(base.MaxwellSimNode):
@ -26,7 +23,7 @@ class FieldPowerFluxMonitorNode(base.MaxwellSimNode):
####################
# - Sockets
####################
input_sockets = {
input_sockets: typ.ClassVar = {
'Center': sockets.PhysicalPoint3DSocketDef(),
'Size': sockets.PhysicalSize3DSocketDef(),
'Samples/Space': sockets.Integer3DVectorSocketDef(
@ -34,7 +31,7 @@ class FieldPowerFluxMonitorNode(base.MaxwellSimNode):
),
'Direction': sockets.BoolSocketDef(),
}
input_socket_sets = {
input_socket_sets: typ.ClassVar = {
'Freq Domain': {
'Freqs': sockets.PhysicalFreqSocketDef(
is_list=True,
@ -42,43 +39,31 @@ class FieldPowerFluxMonitorNode(base.MaxwellSimNode):
},
'Time Domain': {
'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(
default_value=100,
),
},
}
output_sockets = {
output_sockets: typ.ClassVar = {
'Monitor': sockets.MaxwellMonitorSocketDef(),
}
managed_obj_defs = {
'monitor_box': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLObject(name),
name_prefix='',
)
managed_obj_defs: typ.ClassVar = {
'mesh': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLMesh(name),
),
'modifier': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLModifier(name),
),
}
####################
# - Properties
# - Event Methods: Computation
####################
####################
# - UI
####################
def draw_props(self, context, layout):
pass
def draw_info(self, context, col):
pass
####################
# - Output Sockets
####################
@base.computes_output_socket(
@events.computes_output_socket(
'Monitor',
props={'active_socket_set', 'sim_node_name'},
input_sockets={
'Rec Start',
'Rec Stop',
@ -89,113 +74,86 @@ class FieldPowerFluxMonitorNode(base.MaxwellSimNode):
'Freqs',
'Direction',
},
props={'active_socket_set', 'sim_node_name'},
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
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:
_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)
def compute_monitor(self, input_sockets: dict, props: dict) -> td.FieldTimeMonitor:
direction = '+' if input_sockets['Direction'] else '-'
if props['active_socket_set'] == 'Freq Domain':
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(
center=center,
size=size,
center=input_sockets['Center'],
size=input_sockets['Size'],
name=props['sim_node_name'],
interval_space=samples_space,
interval_space=input_sockets['Samples/Space'],
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,
)
else: ## Time Domain
_rec_start = input_sockets['Rec Start']
_rec_stop = input_sockets['Rec Stop']
samples_time = input_sockets['Samples/Time']
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(
center=center,
size=size,
name=props['sim_node_name'],
start=rec_start,
stop=rec_stop,
interval=samples_time,
interval_space=samples_space,
)
return td.FluxTimeMonitor(
center=input_sockets['Center'],
size=input_sockets['Size'],
name=props['sim_node_name'],
start=input_sockets['Rec Start'],
stop=input_sockets['Rec Stop'],
interval=input_sockets['Samples/Time'],
interval_space=input_sockets['Samples/Space'],
normal_dir=direction,
)
####################
# - Preview - Changes to Input Sockets
####################
@base.on_value_changed(
@events.on_value_changed(
socket_name={'Center', 'Size'},
input_sockets={'Center', 'Size', 'Direction'},
managed_objs={'monitor_box'},
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_value_changed__center_size(
def on_inputs_changed(
self,
input_sockets: dict,
props: dict,
managed_objs: dict[str, ct.schemas.ManagedObj],
input_sockets: dict,
unit_systems: dict,
):
_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_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 Input Values to GeoNodes Modifier
managed_objs['modifier'].bl_modifier(
managed_objs['mesh'].bl_object(location=input_sockets['Center']),
'NODES',
{
'node_group': import_geonodes(GeoNodes.PrimitiveBox, 'link'),
'unit_system': unit_systems['BlenderUnits'],
'inputs': {
'Size': input_sockets['Size'],
},
},
)
# Sync Object Position
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()
# Push Preview State
if props['preview_active']:
managed_objs['mesh'].show_preview()
####################
@ -204,6 +162,4 @@ class FieldPowerFluxMonitorNode(base.MaxwellSimNode):
BL_REGISTER = [
FieldPowerFluxMonitorNode,
]
BL_NODES = {
ct.NodeType.FieldPowerFluxMonitor: (ct.NodeCategory.MAXWELLSIM_MONITORS)
}
BL_NODES = {ct.NodeType.FieldPowerFluxMonitor: (ct.NodeCategory.MAXWELLSIM_MONITORS)}

View File

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

View File

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

View File

@ -1,15 +1,13 @@
import typing as typ
import json
import typing as typ
from pathlib import Path
import bpy
import sympy as sp
import pydantic as pyd
import tidy3d as td
from .... import contracts as ct
from .... import sockets
from ... import base
from ... import base, events
####################
@ -40,9 +38,7 @@ class JSONFileExporterNode(base.MaxwellSimNode):
input_sockets = {
'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(
default_value=4,
),
@ -74,13 +70,11 @@ class JSONFileExporterNode(base.MaxwellSimNode):
####################
# - Output Sockets
####################
@base.computes_output_socket(
@events.computes_output_socket(
'JSON String',
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']):
return None
@ -104,7 +98,5 @@ BL_REGISTER = [
JSONFileExporterNode,
]
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 .... import contracts as ct
from .... import sockets
from ... import base
from ... import base, events
####################
@ -77,9 +77,7 @@ class ReloadTrackedTask(bpy.types.Operator):
def execute(self, context):
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"
raise RuntimeError(msg)
@ -105,13 +103,9 @@ class EstCostTrackedTask(bpy.types.Operator):
def execute(self, context):
node = context.node
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:
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)
node.cache_est_cost = task_info.cost_est()
@ -235,13 +229,13 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
msg = 'Tried to upload simulation, but none is attached'
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,
tdcloud.CloudTask,
):
msg = 'Tried to upload simulation to new task, but existing task was selected'
msg = (
'Tried to upload simulation to new task, but existing task was selected'
)
raise ValueError(msg)
# Create Cloud Task
@ -262,9 +256,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
self.tracked_task_id = cloud_task.task_id
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"
raise RuntimeError(msg)
@ -302,9 +294,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
def draw_info(self, context, layout):
# 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'
row = layout.row()
@ -338,9 +328,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
## Split: Right Column
col = split.column(align=False)
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
if self.tracked_task_id and tdcloud.IS_AUTHENTICATED:
@ -383,9 +371,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
## Split: Right Column
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 = (
f'{task_info.cost_real:.2f}'
@ -404,16 +390,12 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
####################
# - Output Methods
####################
@base.computes_output_socket(
@events.computes_output_socket(
'Cloud Task',
input_sockets={'Cloud Task'},
)
def compute_cloud_task(
self, input_sockets: dict
) -> tdcloud.CloudTask | None:
if isinstance(
cloud_task := input_sockets['Cloud Task'], tdcloud.CloudTask
):
def compute_cloud_task(self, input_sockets: dict) -> tdcloud.CloudTask | None:
if isinstance(cloud_task := input_sockets['Cloud Task'], tdcloud.CloudTask):
return cloud_task
return None
@ -421,7 +403,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
####################
# - Output Methods
####################
@base.on_value_changed(
@events.on_value_changed(
socket_name='FDTD Sim',
input_sockets={'FDTD Sim'},
)
@ -446,7 +428,5 @@ BL_REGISTER = [
Tidy3DWebExporterNode,
]
BL_NODES = {
ct.NodeType.Tidy3DWebExporter: (
ct.NodeCategory.MAXWELLSIM_OUTPUTS_EXPORTERS
)
ct.NodeType.Tidy3DWebExporter: (ct.NodeCategory.MAXWELLSIM_OUTPUTS_EXPORTERS)
}

View File

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

View File

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

View File

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

View File

@ -1,124 +1,102 @@
import bpy
import typing as typ
import sympy as sp
import sympy.physics.units as spu
import scipy as sc
from .....utils import analyze_geonodes
from .....assets.import_geonodes import GeoNodes, import_geonodes
from ... import contracts as ct
from ... import sockets
from .. import base
from ... import managed_objs
GEONODES_DOMAIN_BOX = 'simdomain_box'
from ... import managed_objs, sockets
from .. import base, events
class SimDomainNode(base.MaxwellSimNode):
node_type = ct.NodeType.SimDomain
bl_label = 'Sim Domain'
use_sim_node_name = True
input_sockets = {
input_sockets: typ.ClassVar = {
'Duration': sockets.PhysicalTimeSocketDef(
default_value=5 * spu.ps,
default_unit=spu.ps,
),
'Center': sockets.PhysicalSize3DSocketDef(),
'Center': sockets.PhysicalPoint3DSocketDef(),
'Size': sockets.PhysicalSize3DSocketDef(),
'Grid': sockets.MaxwellSimGridSocketDef(),
'Ambient Medium': sockets.MaxwellMediumSocketDef(),
}
output_sockets = {
output_sockets: typ.ClassVar = {
'Domain': sockets.MaxwellSimDomainSocketDef(),
}
managed_obj_defs = {
'domain_box': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLObject(name),
managed_obj_defs: typ.ClassVar = {
'mesh': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLMesh(name),
name_prefix='',
)
),
'modifier': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLModifier(name),
),
}
####################
# - Callbacks
# - Event Methods
####################
@base.computes_output_socket(
@events.computes_output_socket(
'Domain',
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_sim_domain(self, input_sockets: dict) -> sp.Expr:
if all(
[
(_duration := input_sockets['Duration']),
(_center := input_sockets['Center']),
(_size := input_sockets['Size']),
(grid := input_sockets['Grid']),
(medium := input_sockets['Ambient Medium']),
]
):
duration = spu.convert_to(_duration, spu.second) / spu.second
center = tuple(spu.convert_to(_center, spu.um) / spu.um)
size = tuple(spu.convert_to(_size, spu.um) / spu.um)
return dict(
run_time=duration,
center=center,
size=size,
grid_spec=grid,
medium=medium,
)
def compute_output(self, input_sockets: dict) -> sp.Expr:
return {
'run_time': input_sockets['Duration'],
'center': input_sockets['Center'],
'size': input_sockets['Size'],
'grid_spec': input_sockets['Grid'],
'medium': input_sockets['Ambient Medium'],
}
####################
# - Preview
####################
@base.on_value_changed(
@events.on_value_changed(
socket_name={'Center', 'Size'},
prop_name='preview_active',
props={'preview_active'},
input_sockets={'Center', 'Size'},
managed_objs={'domain_box'},
managed_objs={'mesh', 'modifier'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={
'Center': 'BlenderUnits',
},
)
def on_value_changed__center_size(
def on_input_changed(
self,
input_sockets: dict,
props: dict,
managed_objs: dict[str, ct.schemas.ManagedObj],
input_sockets: dict,
unit_systems: dict,
):
_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 Input Values to GeoNodes Modifier
managed_objs['modifier'].bl_modifier(
managed_objs['mesh'].bl_object(location=input_sockets['Center']),
'NODES',
{
'node_group': import_geonodes(GeoNodes.PrimitiveBox, 'link'),
'unit_system': unit_systems['BlenderUnits'],
'inputs': {
'Size': input_sockets['Size'],
},
},
)
# Push Preview State
if props['preview_active']:
managed_objs['mesh'].show_preview()
# Sync Object Position
managed_objs['domain_box'].bl_object('MESH').location = center
@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()
@events.on_init()
def on_init(self):
self.on_input_change()
####################

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,69 +1,65 @@
import typing as typ
import tidy3d as td
import numpy as np
import sympy as sp
import sympy.physics.units as spu
import bpy
from bpy_types import bpy_types
import bmesh
from .....utils import analyze_geonodes
from ... import bl_socket_map
from .....utils import analyze_geonodes, logger
from ... import bl_socket_map, managed_objs, sockets
from ... import contracts as ct
from ... import sockets
from .. import base
from ... import managed_objs
from .. import base, events
log = logger.get(__name__)
class GeoNodesStructureNode(base.MaxwellSimNode):
node_type = ct.NodeType.GeoNodesStructure
bl_label = 'GeoNodes Structure'
use_sim_node_name = True
####################
# - Sockets
####################
input_sockets = {
'Unit System': sockets.PhysicalUnitSystemSocketDef(),
input_sockets: typ.ClassVar = {
'Medium': sockets.MaxwellMediumSocketDef(),
'Center': sockets.PhysicalPoint3DSocketDef(),
'GeoNodes': sockets.BlenderGeoNodesSocketDef(),
}
output_sockets = {
output_sockets: typ.ClassVar = {
'Structure': sockets.MaxwellStructureSocketDef(),
}
managed_obj_defs = {
'geometry': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLObject(name),
name_prefix='',
)
managed_obj_defs: typ.ClassVar = {
'mesh': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLMesh(name),
),
'modifier': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLModifier(name),
),
}
####################
# - Output Socket Computation
# - Event Methods
####################
@base.computes_output_socket(
@events.computes_output_socket(
'Structure',
input_sockets={'Medium'},
managed_objs={'geometry'},
)
def compute_structure(
def compute_output(
self,
input_sockets: dict[str, typ.Any],
managed_objs: dict[str, ct.schemas.ManagedObj],
) -> td.Structure:
# Extract the Managed Blender Object
mobj = managed_objs['geometry']
# Simulate Input Value Change
## This ensures that the mesh has been re-computed.
self.on_input_changed()
# Extract Geometry as Arrays
geometry_as_arrays = mobj.mesh_as_arrays
# Return TriMesh Structure
## TODO: mesh_as_arrays might not take the Center into account.
## - Alternatively, Tidy3D might have a way to transform?
mesh_as_arrays = managed_objs['mesh'].mesh_as_arrays
return td.Structure(
geometry=td.TriangleMesh.from_vertices_faces(
geometry_as_arrays['verts'],
geometry_as_arrays['faces'],
mesh_as_arrays['verts'],
mesh_as_arrays['faces'],
),
medium=input_sockets['Medium'],
)
@ -71,112 +67,96 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
####################
# - Event Methods
####################
@base.on_value_changed(
@events.on_value_changed(
socket_name='GeoNodes',
managed_objs={'geometry'},
input_sockets={'GeoNodes'},
)
def on_value_changed__geonodes(
self,
managed_objs: dict[str, ct.schemas.ManagedObj],
input_sockets: dict[str, typ.Any],
) -> None:
"""Called whenever the GeoNodes socket is changed.
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 = {}
return
# Analyze GeoNodes
## Extract Valid Inputs (via GeoNodes Tree "Interface")
geonodes_interface = analyze_geonodes.interface(
geo_nodes, direc='INPUT'
)
# Set Loose Input Sockets
## Retrieve the appropriate SocketDef for the Blender Interface Socket
self.loose_input_sockets = {
socket_name: bl_socket_map.socket_def_from_bl_interface_socket(
bl_interface_socket
)() ## === <SocketType>SocketDef(), but with dynamic SocketDef
for socket_name, bl_interface_socket in geonodes_interface.items()
}
## Set Loose `socket.value` from Interface `default_value`
for socket_name in self.loose_input_sockets:
socket = self.inputs[socket_name]
bl_interface_socket = geonodes_interface[socket_name]
socket.value = bl_socket_map.value_from_bl(bl_interface_socket)
## Implicitly triggers the loose-input `on_value_changed` for each.
@base.on_value_changed(
prop_name='preview_active',
any_loose_input_socket=True,
managed_objs={'geometry'},
input_sockets={'Unit System', '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_value_changed__loose_inputs(
def on_input_changed(
self,
props: dict,
managed_objs: dict[str, ct.schemas.ManagedObj],
input_sockets: dict[str, typ.Any],
loose_input_sockets: dict[str, typ.Any],
):
"""Called whenever a Loose Input Socket is altered.
input_sockets: dict,
loose_input_sockets: dict,
unit_systems: dict,
) -> None:
# No GeoNodes: Remove Modifier (if any)
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
)
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']):
# Reset Loose Input Sockets
self.loose_input_sockets = {}
return
# Analyze GeoNodes Interface (input direction)
## This retrieves NodeTreeSocketInterface elements
geonodes_interface = analyze_geonodes.interface(
geo_nodes, direc='INPUT'
)
# No Loose Input Sockets: Create from GeoNodes Interface
## TODO: Other reasons to trigger re-filling loose_input_sockets.
if not loose_input_sockets:
# Retrieve the GeoNodes Interface
geonodes_interface = analyze_geonodes.interface(
input_sockets['GeoNodes'], 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.
# Fill the Loose Input Sockets
log.info(
'Initializing GeoNodes Structure Node "%s" from GeoNodes Group "%s"',
self.bl_label,
str(geonodes),
)
self.loose_input_sockets = {
socket_name: bl_socket_map.socket_def_from_bl_socket(iface_socket)()
for socket_name, iface_socket in geonodes_interface.items()
}
## 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,
# Set Loose Input Sockets to Interface (Default) Values
## 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:
socket = self.inputs[socket_name]
socket.value = bl_socket_map.read_bl_socket_default_value(
geonodes_interface[socket_name],
unit_systems['BlenderUnits'],
allow_unit_not_in_unit_system=True,
)
for socket_name, bl_interface_socket in (
geonodes_interface.items()
)
},
)
####################
# - 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')
log.info(
'Set Loose Input Sockets of "%s" to: %s',
self.bl_label,
str(self.loose_input_sockets),
)
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()
####################
@ -185,6 +165,4 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
BL_REGISTER = [
GeoNodesStructureNode,
]
BL_NODES = {
ct.NodeType.GeoNodesStructure: (ct.NodeCategory.MAXWELLSIM_STRUCTURES)
}
BL_NODES = {ct.NodeType.GeoNodesStructure: (ct.NodeCategory.MAXWELLSIM_STRUCTURES)}

View File

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

View File

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

View File

@ -1,123 +1,101 @@
import tidy3d as td
import typing as typ
import sympy as sp
import sympy.physics.units as spu
import tidy3d as td
import bpy
from ......utils import analyze_geonodes
from ......assets.import_geonodes import GeoNodes, import_geonodes
from .... import contracts as ct
from .... import sockets
from .... import managed_objs
from ... import base
GEONODES_STRUCTURE_BOX = 'structure_box'
from .... import managed_objs, sockets
from ... import base, events
class BoxStructureNode(base.MaxwellSimNode):
node_type = ct.NodeType.BoxStructure
bl_label = 'Box Structure'
use_sim_node_name = True
####################
# - Sockets
####################
input_sockets = {
input_sockets: typ.ClassVar = {
'Medium': sockets.MaxwellMediumSocketDef(),
'Center': sockets.PhysicalPoint3DSocketDef(),
'Size': sockets.PhysicalSize3DSocketDef(
default_value=sp.Matrix([500, 500, 500]) * spu.nm
),
}
output_sockets = {
output_sockets: typ.ClassVar = {
'Structure': sockets.MaxwellStructureSocketDef(),
}
managed_obj_defs = {
'structure_box': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLObject(name),
name_prefix='',
)
managed_obj_defs: typ.ClassVar = {
'mesh': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLMesh(name),
),
'modifier': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLModifier(name),
),
}
####################
# - Output Socket Computation
# - Event Methods
####################
@base.computes_output_socket(
@events.computes_output_socket(
'Structure',
input_sockets={'Medium', 'Center', 'Size'},
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
scale_input_sockets={
'Center': 'Tidy3DUnits',
'Size': 'Tidy3DUnits',
},
)
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)
def compute_output(self, input_sockets: dict, unit_systems: dict) -> td.Box:
return td.Structure(
geometry=td.Box(
center=center,
size=size,
center=input_sockets['Center'],
size=input_sockets['Size'],
),
medium=medium,
medium=input_sockets['Medium'],
)
####################
# - Preview - Changes to Input Sockets
####################
@base.on_value_changed(
@events.on_value_changed(
socket_name={'Center', 'Size'},
prop_name='preview_active',
props={'preview_active'},
input_sockets={'Center', 'Size'},
managed_objs={'structure_box'},
managed_objs={'mesh', 'modifier'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={
'Center': 'BlenderUnits',
},
)
def on_value_changed__center_size(
def on_inputs_changed(
self,
input_sockets: dict,
props: dict,
managed_objs: dict[str, ct.schemas.ManagedObj],
input_sockets: dict,
unit_systems: dict,
):
_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_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 Input Values to GeoNodes Modifier
managed_objs['modifier'].bl_modifier(
managed_objs['mesh'].bl_object(location=input_sockets['Center']),
'NODES',
{
'node_group': import_geonodes(GeoNodes.PrimitiveBox, 'link'),
'unit_system': unit_systems['BlenderUnits'],
'inputs': {
'Size': input_sockets['Size'],
},
},
)
# Push Preview State
if props['preview_active']:
managed_objs['mesh'].show_preview()
# Sync Object Position
managed_objs['structure_box'].bl_object('MESH').location = center
####################
# - 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()
@events.on_init()
def on_init(self):
self.on_inputs_changed()
####################
@ -127,7 +105,5 @@ BL_REGISTER = [
BoxStructureNode,
]
BL_NODES = {
ct.NodeType.BoxStructure: (
ct.NodeCategory.MAXWELLSIM_STRUCTURES_PRIMITIVES
)
ct.NodeType.BoxStructure: (ct.NodeCategory.MAXWELLSIM_STRUCTURES_PRIMITIVES)
}

View File

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

View File

@ -1,121 +1,104 @@
import tidy3d as td
import sympy as sp
import typing as typ
import sympy.physics.units as spu
import tidy3d as td
import bpy
from ......utils import analyze_geonodes
from ......assets.import_geonodes import GeoNodes, import_geonodes
from .... import contracts as ct
from .... import sockets
from .... import managed_objs
from ... import base
GEONODES_STRUCTURE_SPHERE = 'structure_sphere'
from .... import managed_objs, sockets
from ... import base, events
class SphereStructureNode(base.MaxwellSimNode):
node_type = ct.NodeType.SphereStructure
bl_label = 'Sphere Structure'
use_sim_node_name = True
####################
# - Sockets
####################
input_sockets = {
input_sockets: typ.ClassVar = {
'Center': sockets.PhysicalPoint3DSocketDef(),
'Radius': sockets.PhysicalLengthSocketDef(
default_value=150 * spu.nm,
),
'Medium': sockets.MaxwellMediumSocketDef(),
}
output_sockets = {
output_sockets: typ.ClassVar = {
'Structure': sockets.MaxwellStructureSocketDef(),
}
managed_obj_defs = {
'structure_sphere': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLObject(name),
name_prefix='',
)
managed_obj_defs: typ.ClassVar = {
'mesh': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLMesh(name),
),
'modifier': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLModifier(name),
),
}
####################
# - Output Socket Computation
####################
@base.computes_output_socket(
@events.computes_output_socket(
'Structure',
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:
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(
geometry=td.Sphere(
radius=radius,
center=center,
radius=input_sockets['Radius'],
center=input_sockets['Center'],
),
medium=medium,
medium=input_sockets['Medium'],
)
####################
# - Preview - Changes to Input Sockets
####################
@base.on_value_changed(
@events.on_value_changed(
socket_name={'Center', 'Radius'},
prop_name='preview_active',
props={'preview_active'},
input_sockets={'Center', 'Radius'},
managed_objs={'structure_sphere'},
managed_objs={'mesh', 'modifier'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={
'Center': 'Tidy3DUnits',
'Radius': 'Tidy3DUnits',
},
)
def on_value_changed__center_radius(
def on_inputs_changed(
self,
input_sockets: dict,
props: dict,
managed_objs: dict[str, ct.schemas.ManagedObj],
input_sockets: dict,
unit_systems: dict,
):
_center = input_sockets['Center']
center = tuple(
[float(el) for el in spu.convert_to(_center, spu.um) / spu.um]
)
_radius = input_sockets['Radius']
radius = float(spu.convert_to(_radius, 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_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 Input Values to GeoNodes Modifier
managed_objs['modifier'].bl_modifier(
managed_objs['mesh'].bl_object(location=input_sockets['Center']),
'NODES',
{
'node_group': import_geonodes(GeoNodes.PrimitiveSphere, 'link'),
'unit_system': unit_systems['BlenderUnits'],
'inputs': {
'Radius': input_sockets['Radius'],
},
},
)
# Push Preview State
if props['preview_active']:
managed_objs['mesh'].show_preview()
# Sync Object Position
managed_objs['structure_sphere'].bl_object('MESH').location = center
####################
# - 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()
@events.on_init()
def on_init(self):
self.on_inputs_changed()
####################
@ -125,7 +108,5 @@ BL_REGISTER = [
SphereStructureNode,
]
BL_NODES = {
ct.NodeType.SphereStructure: (
ct.NodeCategory.MAXWELLSIM_STRUCTURES_PRIMITIVES
)
ct.NodeType.SphereStructure: (ct.NodeCategory.MAXWELLSIM_STRUCTURES_PRIMITIVES)
}

View File

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

View File

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

View File

@ -1,11 +1,9 @@
import tidy3d as td
import scipy as sc
import sympy as sp
import sympy.physics.units as spu
import scipy as sc
from .... import contracts
from .... import sockets
from ... import base
from .... import contracts, sockets
from ... import base, events
class WaveConverterNode(base.MaxwellSimTreeNode):
@ -46,11 +44,9 @@ class WaveConverterNode(base.MaxwellSimTreeNode):
####################
# - Output Socket Computation
####################
@base.computes_output_socket('freq')
@events.computes_output_socket('freq')
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')
@ -59,11 +55,9 @@ class WaveConverterNode(base.MaxwellSimTreeNode):
spu.hertz,
)
@base.computes_output_socket('vacwl')
@events.computes_output_socket('vacwl')
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')

View File

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

View File

@ -1,72 +1,35 @@
from . import base
from ....utils import logger
from .. import contracts as ct
from . import basic, blender, maxwell, number, physical, tidy3d, vector
from .scan_socket_defs import scan_for_socket_defs
from . import basic
log = logger.get(__name__)
sockets_modules = [basic, number, vector, physical, blender, maxwell, tidy3d]
AnySocketDef = basic.AnySocketDef
BoolSocketDef = basic.BoolSocketDef
StringSocketDef = basic.StringSocketDef
FilePathSocketDef = basic.FilePathSocketDef
####################
# - Scan for SocketDefs
####################
SOCKET_DEFS = {}
for sockets_module in sockets_modules:
SOCKET_DEFS |= scan_for_socket_defs(sockets_module)
from . import number
# Set Global Names from SOCKET_DEFS
## 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
IntegerNumberSocketDef = number.IntegerNumberSocketDef
RationalNumberSocketDef = number.RationalNumberSocketDef
RealNumberSocketDef = number.RealNumberSocketDef
ComplexNumberSocketDef = number.ComplexNumberSocketDef
from . import vector
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
# Validate SocketType -> SocketDef
## All SocketTypes should have a SocketDef
for socket_type in ct.SocketType:
if (
globals().get(socket_type.value.removesuffix('SocketType') + 'SocketDef')
is None
):
log.warning('Missing SocketDef for %s', socket_type.value)
####################
# - Exports
####################
BL_REGISTER = [
*basic.BL_REGISTER,
*number.BL_REGISTER,
@ -76,3 +39,13 @@ BL_REGISTER = [
*maxwell.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,12 +1,11 @@
import typing as typ
import typing_extensions as typx
import functools
import typing as typ
import bpy
import pydantic as pyd
import sympy as sp
import sympy.physics.units as spu
import typing_extensions as typx
from .. import contracts as ct
@ -43,7 +42,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
# - Initialization
####################
def __init_subclass__(cls, **kwargs: typ.Any):
super().__init_subclass__(**kwargs) ## Yucky superclass setup.
super().__init_subclass__(**kwargs)
# Setup Blender ID for Node
if not hasattr(cls, 'socket_type'):
@ -181,7 +180,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
if self.locked:
return False
if self.is_output:
msg = f"Tried to sync 'link add' on output socket"
msg = "Tried to sync 'link add' on output socket"
raise RuntimeError(msg)
self.trigger_action('value_changed')
@ -196,7 +195,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
if self.locked:
return False
if self.is_output:
msg = f"Tried to sync 'link add' on output socket"
msg = "Tried to sync 'link add' on output socket"
raise RuntimeError(msg)
self.trigger_action('value_changed')
@ -267,15 +266,12 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
if kind == ct.DataFlowKind.Value:
if self.is_list:
return self.value_list
else:
return self.value
elif kind == ct.DataFlowKind.LazyValue:
return self.value
if kind == ct.DataFlowKind.LazyValue:
if self.is_list:
return self.lazy_value_list
else:
return self.lazy_value
return self.lazy_value
elif kind == ct.DataFlowKind.Capabilities:
if kind == ct.DataFlowKind.Capabilities:
return self.capabilities
return None
@ -303,9 +299,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
return self._compute_data(kind)
## 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
if len(linked_values) == 1:
@ -362,7 +356,6 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
Can be overridden if more specific logic is required.
"""
prev_value = self.value / self.unit * self.prev_unit
## After changing units, self.value is expressed in the wrong unit.
## - Therefore, we removing the new unit, and re-add the prev unit.
@ -401,7 +394,6 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
text: str,
) -> None:
"""Called by Blender to draw the socket UI."""
if self.is_output:
self.draw_output(context, layout, node, text)
else:
@ -458,9 +450,8 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
if self.locked:
row = col.row(align=False)
row.enabled = False
else:
if self.locked:
row.enabled = False
elif self.locked:
row.enabled = False
# Value Column(s)
col = row.column(align=True)
@ -498,11 +489,9 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
Can be overridden.
"""
pass
def draw_value_list(self, col: bpy.types.UILayout) -> None:
"""Called to draw the value list column in unlinked input sockets.
Can be overridden.
"""
pass

View File

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

View File

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

View File

@ -1,11 +1,8 @@
import typing as typ
import bpy
import sympy as sp
import pydantic as pyd
from .. import base
from ... import contracts as ct
from .. import base
####################
@ -28,9 +25,7 @@ class BoolBLSocket(base.MaxwellSimSocket):
####################
# - 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.prop(self, 'raw_value', text='')

View File

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

View File

@ -1,11 +1,8 @@
import typing as typ
import bpy
import sympy as sp
import pydantic as pyd
from .. import base
from ... import contracts as ct
from .. import base
####################
@ -28,9 +25,7 @@ class StringBLSocket(base.MaxwellSimSocket):
####################
# - 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)
####################

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,55 @@
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,10 +1,8 @@
import typing as typ
import bpy
import pydantic as pyd
from .. import base
from ... import contracts as ct
from .. import base
####################

View File

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

View File

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

View File

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

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