refactor: Far more well-functioning baseline.

blender-plugin-mvp
Sofus Albert Høgsbro Rose 2024-02-14 12:33:40 +01:00
parent b592ea4b10
commit b78dd8dd56
Signed by: so-rose
GPG Key ID: AD901CB0F3701434
61 changed files with 2404 additions and 355 deletions

View File

@ -1,11 +1,11 @@
from . import sockets
from . import node_tree
from . import nodes
from . import categories
from . import socket_types
from . import tree
BL_REGISTER = [
*tree.BL_REGISTER,
*socket_types.BL_REGISTER,
*sockets.BL_REGISTER,
*node_tree.BL_REGISTER,
*nodes.BL_REGISTER,
*categories.BL_REGISTER,
]

View File

@ -1,18 +1,10 @@
## TODO: Refactor this whole horrible module.
import bpy
import nodeitems_utils
from . import types
from . import contracts
from .nodes import BL_NODES
####################
# - Assembly of Node Categories
####################
class MaxwellSimNodeCategory(nodeitems_utils.NodeCategory):
@classmethod
def poll(cls, context):
"""Constrain node category availability to within a MaxwellSimTree."""
return context.space_data.tree_type == types.TreeType.MaxwellSim.value
DYNAMIC_SUBMENU_REGISTRATIONS = []
def mk_node_categories(
tree,
@ -23,7 +15,7 @@ def mk_node_categories(
items = []
# Add Node Items
base_category = types.NodeCategory["_".join(syllable_prefix)]
base_category = contracts.NodeCategory["_".join(syllable_prefix)]
for node_type, node_category in BL_NODES.items():
if node_category == base_category:
items.append(nodeitems_utils.NodeItem(node_type.value))
@ -31,7 +23,7 @@ def mk_node_categories(
# Add Node Sub-Menus
for syllable, sub_tree in tree.items():
current_syllable_path = syllable_prefix + [syllable]
current_category = types.NodeCategory[
current_category = contracts.NodeCategory[
"_".join(current_syllable_path)
]
@ -51,10 +43,12 @@ def mk_node_categories(
nodeitems_utils.NodeItem,
):
nodeitem = nodeitem_or_submenu
self.layout.operator(
op_add_node_cfg = self.layout.operator(
"node.add_node",
text=nodeitem.label,
).type = nodeitem.nodetype
)
op_add_node_cfg.type = nodeitem.nodetype
op_add_node_cfg.use_transform = True
elif isinstance(nodeitem_or_submenu, str):
submenu_id = nodeitem_or_submenu
self.layout.menu(submenu_id)
@ -62,7 +56,7 @@ def mk_node_categories(
menu_class = type(current_category.value, (bpy.types.Menu,), {
'bl_idname': current_category.value,
'bl_label': types.NodeCategory_to_category_label[current_category],
'bl_label': contracts.NodeCategory_to_category_label[current_category],
'draw': draw_factory(tuple(subitems)),
})
@ -78,7 +72,7 @@ def mk_node_categories(
# - Blender Registration
####################
BL_NODE_CATEGORIES = mk_node_categories(
types.NodeCategory.get_tree()["MAXWELL"]["SIM"],
contracts.NodeCategory.get_tree()["MAXWELL"]["SIM"],
syllable_prefix = ["MAXWELL", "SIM"],
)
## TODO: refractor, this has a big code smell
@ -88,7 +82,7 @@ BL_REGISTER = [
## TEST - TODO this is a big code smell
def menu_draw(self, context):
if context.space_data.tree_type == types.TreeType.MaxwellSim.value:
if context.space_data.tree_type == contracts.TreeType.MaxwellSim.value:
for nodeitem_or_submenu in BL_NODE_CATEGORIES:
if isinstance(nodeitem_or_submenu, str):
submenu_id = nodeitem_or_submenu

View File

@ -1,20 +0,0 @@
####################
# - Colors
####################
COLOR_SOCKET_SOURCE = (0.4, 0.4, 0.9, 1.0)
COLOR_SOCKET_MEDIUM = (1.0, 0.4, 0.2, 1.0)
COLOR_SOCKET_STRUCTURE = (0.2, 0.4, 0.8, 1.0)
COLOR_SOCKET_BOUND = (0.7, 0.8, 0.7, 1.0)
COLOR_SOCKET_FDTDSIM = (0.9, 0.9, 0.9, 1.0)
####################
# - Icons
####################
ICON_SIM = 'MOD_SIMPLEDEFORM'
ICON_SIM_SOURCE = 'FORCE_CHARGE'
ICON_SIM_MEDIUM = 'MATSHADERBALL'
ICON_SIM_STRUCTURE = 'OUTLINER_DATA_MESH'
ICON_SIM_BOUND = 'MOD_MESHDEFORM'
ICON_SIM_SIMULATION = ICON_SIM

View File

@ -1,10 +1,72 @@
import bpy
import typing as typ
import typing_extensions as pytypes_ext
import enum
import sympy as sp
import sympy.physics.units as spu
import pydantic as pyd
import bpy
from ...utils.blender_type_enum import (
BlenderTypeEnum, append_cls_name_to_values
)
####################
# - String Types
####################
BlenderColorRGB = tuple[float, float, float, float]
BlenderID = pytypes_ext.Annotated[str, pyd.StringConstraints(
pattern=r'^[A-Z_]+$',
)]
# Socket ID
SocketName = pytypes_ext.Annotated[str, pyd.StringConstraints(
pattern=r'^[a-zA-Z0-9_]+$',
)]
BLSocketName = pytypes_ext.Annotated[str, pyd.StringConstraints(
pattern=r'^[a-zA-Z0-9_]+$',
)]
# Socket ID
PresetID = pytypes_ext.Annotated[str, pyd.StringConstraints(
pattern=r'^[A-Z_]+$',
)]
####################
# - Generic Types
####################
SocketReturnType = typ.TypeVar('SocketReturnType', covariant=True)
## - Covariance: If B subtypes A, then Container[B] subtypes Container[A].
## - This is absolutely what we want here.
####################
# - Sympy Expression Typing
####################
ALL_UNIT_SYMBOLS = {
unit
for unit in spu.__dict__.values()
if isinstance(unit, spu.Quantity)
}
def has_units(expr: sp.Expr):
return any(
symbol in ALL_UNIT_SYMBOLS
for symbol in expr.atoms(sp.Symbol)
)
def is_exactly_expressed_as_unit(expr: sp.Expr, unit) -> bool:
#try:
converted_expr = expr / unit
return (
converted_expr.is_number
and not converted_expr.has(spu.Quantity)
)
####################
# - Icon Types
####################
class Icon(BlenderTypeEnum):
MaxwellSimTree = "MOD_SIMPLEDEFORM"
####################
# - Tree Types
####################
@ -17,6 +79,17 @@ class TreeType(BlenderTypeEnum):
####################
@append_cls_name_to_values
class SocketType(BlenderTypeEnum):
Any = enum.auto()
Text = enum.auto()
FilePath = enum.auto()
RationalNumber = enum.auto()
RealNumber = enum.auto()
ComplexNumber = enum.auto()
PhysicalLength = enum.auto()
PhysicalArea = enum.auto()
MaxwellSource = enum.auto()
MaxwellMedium = enum.auto()
MaxwellStructure = enum.auto()
@ -132,7 +205,7 @@ class NodeType(BlenderTypeEnum):
KSpaceNearFieldProjectionMonitor = enum.auto()
# Simulations
FDTDSimulation = enum.auto()
FDTDSim = enum.auto()
SimulationGridDiscretization = enum.auto()
@ -155,8 +228,6 @@ class NodeType(BlenderTypeEnum):
####################
# - Node Category Types
####################
#@append_cls_name_to_values
#@append_cls_name_to_values
class NodeCategory(BlenderTypeEnum):
MAXWELL_SIM = enum.auto()
@ -219,6 +290,7 @@ class NodeCategory(BlenderTypeEnum):
@classmethod
def get_tree(cls):
## TODO: Refactor
syllable_categories = [
node_category.value.split("_")
for node_category in cls
@ -296,3 +368,104 @@ NodeCategory_to_category_label = {
NodeCategory.MAXWELL_SIM_UTILITIES_MATH: "Math",
NodeCategory.MAXWELL_SIM_UTILITIES_FIELDMATH: "Field Math",
}
####################
# - Protocols
####################
class SocketDefProtocol(typ.Protocol):
socket_type: SocketType
label: str
def init(self, bl_socket: bpy.types.NodeSocket) -> None:
...
class PresetDef(pyd.BaseModel):
label: str
description: str
values: dict[SocketName, typ.Any]
@typ.runtime_checkable
#class BLSocketProtocol(typ.Protocol):
# socket_type: SocketType
# socket_color: BlenderColorRGB
#
# bl_label: str
#
# compatible_types: dict[typ.Type, set[typ.Callable[[typ.Any], bool]]]
#
# def draw(
# self,
# context: bpy.types.Context,
# layout: bpy.types.UILayout,
# node: bpy.types.Node,
# text: str,
# ) -> None:
# ...
#
# @property
# def default_value(self) -> typ.Any:
# ...
# @default_value.setter
# def default_value(self, value: typ.Any) -> typ.Any:
# ...
#
@typ.runtime_checkable
class NodeTypeProtocol(typ.Protocol):
node_type: NodeType
bl_label: str
input_sockets: dict[SocketName, SocketDefProtocol]
output_sockets: dict[SocketName, SocketDefProtocol]
presets: dict[PresetID, PresetDef] | None
# Built-In Blender Methods
def init(self, context: bpy.types.Context) -> None:
...
def draw_buttons(
self,
context: bpy.types.Context,
layout: bpy.types.UILayout,
) -> None:
...
@classmethod
def poll(cls, ntree: bpy.types.NodeTree) -> None:
...
# Socket Getters
def g_input_bl_socket(
self,
input_socket_name: SocketName,
) -> bpy.types.NodeSocket:
...
def g_output_bl_socket(
self,
output_socket_name: SocketName,
) -> bpy.types.NodeSocket:
...
# Socket Methods
def s_input_value(
self,
input_socket_name: SocketName,
value: typ.Any
) -> typ.Any:
...
# Data-Flow Methods
def compute_input(
self,
input_socket_name: SocketName,
) -> typ.Any:
...
def compute_output(
self,
output_socket_name: SocketName,
) -> typ.Any:
...

View File

@ -0,0 +1,57 @@
import bpy
from . import contracts
ICON_SIM_TREE = 'MOD_SIMPLEDEFORM'
####################
# - Node Tree Definition
####################
class MaxwellSimTree(bpy.types.NodeTree):
bl_idname = contracts.TreeType.MaxwellSim
bl_label = "Maxwell Sim Editor"
bl_icon = contracts.Icon.MaxwellSimTree
####################
# - Blender Registration
####################
BL_REGISTER = [
MaxwellSimTree,
]
####################
# - Red Edges on Error
####################
## TODO: Refactor
#def link_callback_new(context):
# print("A THING HAPPENED")
# node_tree_type = contracts.TreeType.MaxwellSim.value
# link = context.link
#
# if not (
# link.from_node.node_tree.bl_idname == node_tree_type
# and link.to_node.node_tree.bl_idname == node_tree_type
# ):
# return
#
# source_node = link.from_node
#
# source_socket_name = source_node.g_output_socket_name(
# link.from_socket.name
# )
# link_data = source_node.compute_output(source_socket_name)
#
# destination_socket = link.to_socket
# link.is_valid = destination_socket.is_compatible(link_data)
#
# print(source_node, destination_socket, link.is_valid)
#
#bpy.msgbus.subscribe_rna(
# key=("active", "node_tree"),
# owner=MaxwellSimTree,
# args=(bpy.context,),
# notify=link_callback_new,
# options={'PERSISTENT'}
#)

View File

@ -1,8 +1,23 @@
from . import inputs
from . import outputs
from . import sources
from . import mediums
from . import simulations
from . import structures
BL_REGISTER = [
*inputs.BL_REGISTER,
*outputs.BL_REGISTER,
*mediums.BL_REGISTER,
*sources.BL_REGISTER,
*simulations.BL_REGISTER,
*structures.BL_REGISTER,
]
BL_NODES = {
**inputs.BL_NODES,
**outputs.BL_NODES,
**sources.BL_NODES,
**mediums.BL_NODES,
**simulations.BL_NODES,
**structures.BL_NODES,
}

View File

@ -0,0 +1,412 @@
import typing as typ
import typing_extensions as pytypes_ext
import bpy
import pydantic as pyd
from .. import contracts
from .. import sockets
####################
# - Decorator: Output Socket Computation
####################
@typ.runtime_checkable
class ComputeOutputSocketFunc(typ.Protocol[contracts.SocketReturnType]):
"""Protocol describing a function that computes the value of an
output socket.
"""
def __call__(
_self,
self: contracts.NodeTypeProtocol,
) -> contracts.SocketReturnType:
"""Describes the function signature of all functions that compute
the value of an output socket.
Args:
node: A node in the tree, passed via the 'self' attribute of the
node.
Returns:
The value of the output socket, as the relevant type.
"""
...
class PydanticProtocolMeta(type(pyd.BaseModel), type(typ.Protocol)): pass
class FuncOutputSocket(
pyd.BaseModel,
typ.Generic[contracts.SocketReturnType],
ComputeOutputSocketFunc[contracts.SocketReturnType],
metaclass=PydanticProtocolMeta,
):
"""Defines a function (-like object) that defines an attachment from
an output socket name, to the original method that computes the value of
an output socket.
Conforms to the protocol `ComputeOutputSocketFunc`.
Validation is provided by subtyping `pydantic.BaseModel`.
Attributes:
output_socket_func: The original method computing the value of an
output socket.
output_socket_name: The SocketName of the output socket for which
this function should be called to compute.
"""
output_socket_func: typ.Callable[
[contracts.NodeTypeProtocol],
contracts.SocketReturnType,
]
output_socket_name: contracts.SocketName
def __call__(
self,
node: contracts.NodeTypeProtocol
) -> contracts.SocketReturnType:
"""Computes the value of an output socket.
Args:
node: A node in the tree, passed via the 'self' attribute of the
node.
Returns:
The value of the output socket, as the relevant type.
"""
return self.output_socket_func(node)
# Define Factory Function & Decorator
def computes_output_socket(
output_socket_name: contracts.SocketName,
return_type: typ.Generic[contracts.SocketReturnType],
) -> typ.Callable[
[ComputeOutputSocketFunc[contracts.SocketReturnType]],
FuncOutputSocket[contracts.SocketReturnType],
]:
"""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.
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`.
"""
def decorator(
output_socket_func: ComputeOutputSocketFunc[contracts.SocketReturnType]
) -> FuncOutputSocket[contracts.SocketReturnType]:
return FuncOutputSocket(
output_socket_func=output_socket_func,
output_socket_name=output_socket_name,
)
return decorator
####################
# - Node Callbacks
####################
def sync_selected_preset(node: contracts.NodeTypeProtocol) -> None:
"""Whenever a preset is set in a NodeTypeProtocol, this function
should be called to overwrite the `default_value`s of the input sockets
with the actual preset values.
Args:
node: The node for which input socket `default_value`s should be
set to the values defined within the currently selected preset.
"""
if node.preset is None:
msg = f"Node {node} has no preset EnumProperty"
raise ValueError(msg)
if node.presets is None:
msg = f"Node {node} has preset EnumProperty, but no defined presets."
raise ValueError(msg)
# Set Input Sockets to Preset Values
preset_def = node.presets[node.preset]
for input_socket_name, value in preset_def.values.items():
node.s_input_value(input_socket_name, value)
####################
# - Node Superclass Definition
####################
class MaxwellSimTreeNode(bpy.types.Node):
"""A base type for nodes that greatly simplifies the implementation of
reliable, powerful nodes.
Should be used together with `contracts.NodeTypeProtocol`.
"""
def __init_subclass__(cls, **kwargs: typ.Any):
super().__init_subclass__(**kwargs) ## Yucky superclass setup.
# Set bl_idname
cls.bl_idname = cls.node_type.value
# Declare Node Property: 'preset' EnumProperty
if hasattr(cls, "presets"):
first_preset = list(cls.presets.keys())[0]
cls.__annotations__["preset"] = bpy.props.EnumProperty(
name="Presets",
description="Select a preset",
items=[
(
preset_name,
preset_def.label,
preset_def.description,
)
for preset_name, preset_def in cls.presets.items()
],
default=first_preset, ## 1st is Default
update=(lambda self, context: sync_selected_preset(self)),
)
else:
cls.preset = None
cls.presets = None
####################
# - Blender Init / Constraints
####################
def init(self, context: bpy.types.Context):
"""Declares input and output sockets as described by the
`NodeTypeProtocol` specification, and initializes each as described
by user-provided `SocketDefProtocol`s.
"""
# Initialize Input Sockets
for socket_name, socket_def in self.input_sockets.items():
self.inputs.new(
socket_def.socket_type.value, ## strenum.value => a real str
socket_def.label,
)
# Retrieve the Blender Socket (bpy.types.NodeSocket)
## We could use self.g_input_bl_socket()...
## ...but that would rely on implicit semi-initialized state.
bl_socket = self.inputs[
self.input_sockets[socket_name].label
]
# Initialize the Socket from the Socket Definition
## `bl_socket` knows whether it's an input or output socket...
## ...via its `.is_output` attribute.
socket_def.init(bl_socket)
# Initialize Output Sockets
for socket_name, socket_def in self.output_sockets.items():
self.outputs.new(
socket_def.socket_type.value,
socket_def.label,
)
bl_socket = self.outputs[
self.output_sockets[socket_name].label
]
socket_def.init(bl_socket)
# Sync Default Preset to Input Socket Values
if self.preset is not None:
sync_selected_preset(self)
@classmethod
def poll(cls, ntree: bpy.types.NodeTree) -> bool:
"""This class method controls whether a node can be instantiated
in a given node tree.
In our case, we restrict node instantiation to within a
MaxwellSimTree.
Args:
ntree: The node tree within which the user is currently working.
Returns:
Whether or not the user should be able to instantiate the node.
"""
return ntree.bl_idname == contracts.TreeType.MaxwellSim.value
####################
# - UI Methods
####################
def draw_buttons(
self,
context: bpy.types.Context,
layout: bpy.types.UILayout,
) -> None:
"""This method draws the UI of the node itself.
Specifically, it is used to expose the Presets dropdown.
"""
if self.preset is not None:
layout.prop(self, "preset", text="")
if hasattr(self, "draw_operators"):
self.draw_operators(context, layout)
####################
# - Socket Getters
####################
def g_input_bl_socket(
self,
input_socket_name: contracts.SocketName,
) -> bpy.types.NodeSocket:
"""Returns the `bpy.types.NodeSocket` of an input socket by name.
Args:
input_socket_name: The name of the input socket, as defined in
`self.input_sockets`.
Returns:
Blender's own node socket object.
"""
# (Guard) Socket Exists
if input_socket_name not in self.input_sockets:
msg = f"Input socket with name {input_socket_name} does not exist"
raise ValueError(msg)
return self.inputs[self.input_sockets[input_socket_name].label]
def g_output_bl_socket(
self,
output_socket_name: contracts.SocketName,
) -> bpy.types.NodeSocket:
"""Returns the `bpy.types.NodeSocket` of an output socket by name.
Args:
output_socket_name: The name of the output socket, as defined in
`self.output_sockets`.
Returns:
Blender's own node socket object.
"""
# (Guard) Socket Exists
if output_socket_name not in self.output_sockets:
msg = f"Input socket with name {output_socket_name} does not exist"
raise ValueError(msg)
return self.outputs[self.output_sockets[output_socket_name].label]
def g_output_socket_name(
self,
output_bl_socket_name: contracts.BLSocketName,
) -> contracts.SocketName:
return next(
output_socket_name
for output_socket_name in self.output_sockets.keys()
if self.output_sockets[
output_socket_name
].label == output_bl_socket_name
)
####################
# - Socket Setters
####################
def s_input_value(
self,
input_socket_name: contracts.SocketName,
value: typ.Any,
) -> None:
"""Sets the value of an input socket, if the value is compatible with
the socket.
Args:
input_socket_name: The name of the input socket.
value: The value to set, which must be compatible with the
socket.
Raises:
ValueError: If the value is incompatible with the socket, for
example due to incompatible types, then a ValueError will be
raised.
"""
bl_socket = self.g_input_bl_socket(input_socket_name)
# Set the Value
bl_socket.default_value = value
####################
# - Socket Computation
####################
def compute_input(
self,
input_socket_name: contracts.SocketName,
) -> typ.Any:
"""Computes the value of an input socket, by its name. Will
automatically compute the output socket value of any linked
nodes.
Args:
input_socket_name: The name of the input socket, as defined in
`self.input_sockets`.
"""
bl_socket = self.g_input_bl_socket(input_socket_name)
# Linked: Compute Output of Linked Socket
if bl_socket.is_linked:
linked_node = bl_socket.links[0].from_node
# Compute the Linked Socket Name
linked_bl_socket_name: contracts.BLSocketName = bl_socket.links[0].from_socket.name
linked_socket_name = linked_node.g_output_socket_name(
linked_bl_socket_name
)
# Compute the Linked Socket Value
linked_socket_value = linked_node.compute_output(
linked_socket_name
)
# (Guard) Check the Compatibility of the Linked Socket Value
if not bl_socket.is_compatible(linked_socket_value):
msg = f"Tried setting socket ({input_socket_name}) to incompatible value ({linked_socket_value}) of type {type(linked_socket_value)}"
raise ValueError(msg)
return linked_socket_value
# Unlinked: Simply Retrieve Socket Value
return bl_socket.default_value
def compute_output(
self,
output_socket_name: contracts.SocketName,
) -> typ.Any:
"""Computes the value of an output socket name, from its socket name.
Searches for methods decorated with `@computes_output_socket("name")`,
which describe the computation that occurs to actually compute the
value of an output socket from ex. input sockets and node properties.
Args:
output_socket_name: The name declaring the output socket,
for which this method computes the output.
Returns:
The value of the output socket, as computed by the dedicated method
registered using the `@computes_output_socket` decorator.
"""
# Lookup the Function that Computes the Output Socket
## The decorator ALWAYS produces a FuncOutputSocket.
## Thus, we merely need to find a FuncOutputSocket
output_socket_func = next(
method.output_socket_func
for attr_name in dir(self) ## Lookup self.*
if isinstance(
method := getattr(self, attr_name),
FuncOutputSocket,
)
if method.output_socket_name == output_socket_name
)
return output_socket_func(self)

View File

@ -0,0 +1,8 @@
from . import constants
BL_REGISTER = [
*constants.BL_REGISTER,
]
BL_NODES = {
**constants.BL_NODES,
}

View File

@ -0,0 +1,8 @@
from . import complex_constant
BL_REGISTER = [
*complex_constant.BL_REGISTER,
]
BL_NODES = {
**complex_constant.BL_NODES,
}

View File

@ -0,0 +1,44 @@
import bpy
import sympy as sp
from .... import contracts
from .... import sockets
from ... import base
class ComplexConstantNode(base.MaxwellSimTreeNode):
node_type = contracts.NodeType.ComplexConstant
bl_label = "Complex Constant"
#bl_icon = constants.ICON_SIM_INPUT
input_sockets = {
"value": sockets.ComplexNumberSocketDef(
label="Complex",
),
}
output_sockets = {
"value": sockets.ComplexNumberSocketDef(
label="Complex",
),
}
####################
# - Callbacks
####################
@base.computes_output_socket("value", sp.Expr)
def compute_value(self: contracts.NodeTypeProtocol) -> sp.Expr:
return self.compute_input("value")
####################
# - Blender Registration
####################
BL_REGISTER = [
ComplexConstantNode,
]
BL_NODES = {
contracts.NodeType.ComplexConstant: (
contracts.NodeCategory.MAXWELL_SIM_INPUTS_CONSTANTS
)
}

View File

@ -0,0 +1,84 @@
import bpy
import gpu
import gpu_extras
import sympy.physics.units as spu
from .... import types, constants
from ... import node_base
class TripleSellmeierMediumNode(node_base.MaxwellSimTreeNode):
bl_idname = types.NodeType.TripleSellmeierMedium.value
bl_label = "Triple Sellmeier Medium"
bl_icon = constants.ICON_SIM_MEDIUM
input_sockets = {
"B1": (types.SocketType.RealNumber, "B1"),
"B2": (types.SocketType.RealNumber, "B2"),
"B3": (types.SocketType.RealNumber, "B3"),
"C1": (types.SocketType.DimenArea, "C1"),
"C2": (types.SocketType.DimenArea, "C2"),
"C3": (types.SocketType.DimenArea, "C3"),
}
output_sockets = {
"medium": (types.SocketType.MaxwellMedium, "Medium")
}
input_unit_defaults = {
"B1": None,
"B2": None,
"B3": None,
"C1": spu.um**2,
"C2": spu.um**2,
"C3": spu.um**2,
}
socket_presets = {
"_description": [
('BK7', "BK7 Glass", "Borosilicate crown glass (known as BK7)"),
('FUSED_SILICA', "Fused Silica", "Fused silica aka. SiO2"),
],
"_default": "BK7",
"_values": {
"BK7": {
"B1": 1.03961212,
"B2": 0.231792344,
"B3": 1.01046945,
"C1": 6.00069867e-3 * spu.um**2,
"C2": 2.00179144e-2 * spu.um**2,
"C3": 103.560653 * spu.um**2,
},
"FUSED_SILICA": {
"B1": 0.696166300,
"B2": 0.407942600,
"B3": 0.897479400,
"C1": 4.67914826e-3 * spu.um**2,
"C2": 1.35120631e-2 * spu.um**2,
"C3": 97.9340025 * spu.um**2,
},
}
}
####################
# - Properties
####################
def draw_buttons(self, context, layout):
layout.prop(self, 'preset', text="")
####################
# - Callbacks
####################
@node_base.output_socket_cb("medium")
def compute_medium(self):
pass
####################
# - Blender Registration
####################
BL_REGISTER = [
TripleSellmeierMediumNode,
]
BL_NODES = {
types.NodeType.TripleSellmeierMedium: (
types.NodeCategory.MAXWELL_SIM_MEDIUMS_LINEARMEDIUMS
)
}

View File

@ -1,54 +1,90 @@
import bpy
from .... import types, constants
from ... import node_base
import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
class TripleSellmeierMediumNode(node_base.MaxwellSimTreeNode):
bl_idname = types.NodeType.TripleSellmeierMedium.value
bl_label = "Triple Sellmeier Medium"
bl_icon = constants.ICON_SIM_MEDIUM
from .... import contracts
from .... import sockets
from ... import base
class TripleSellmeierMediumNode(base.MaxwellSimTreeNode):
node_type = contracts.NodeType.TripleSellmeierMedium
bl_label = "Three-Parameter Sellmeier Medium"
#bl_icon = ...
####################
# - Sockets
####################
input_sockets = {
"B1": ("NodeSocketFloat", "B1"),
"B2": ("NodeSocketFloat", "B2"),
"B3": ("NodeSocketFloat", "B3"),
"C1": ("NodeSocketFloat", "C1 (um^2)"),
"C2": ("NodeSocketFloat", "C2 (um^2)"),
"C3": ("NodeSocketFloat", "C3 (um^2)"),
f"B{i}": sockets.RealNumberSocketDef(
label=f"B{i}",
)
for i in [1, 2, 3]
} | {
f"C{i}": sockets.PhysicalAreaSocketDef(
label=f"C{i}",
default_unit="UM_SQ"
)
for i in [1, 2, 3]
}
output_sockets = {
"medium": (types.SocketType.MaxwellMedium, "Medium")
"medium": sockets.MaxwellMediumSocketDef(
label="Medium"
),
}
socket_presets = {
"_description": [
('BK7', "BK7 Glass", "Borosilicate crown glass (known as BK7)"),
('FUSED_SILICA', "Fused Silica", "Fused silica aka. SiO2"),
],
"_default": "BK7",
"_values": {
"BK7": {
####################
# - Presets
####################
presets = {
"BK7": contracts.PresetDef(
label="BK7 Glass",
description="Borosilicate crown glass (known as BK7)",
values={
"B1": 1.03961212,
"B2": 0.231792344,
"B3": 1.01046945,
"C1": 6.00069867e-3,
"C2": 2.00179144e-2,
"C3": 103.560653,
},
"FUSED_SILICA": {
"C1": 6.00069867e-3 * spu.um**2,
"C2": 2.00179144e-2 * spu.um**2,
"C3": 103.560653 * spu.um**2,
}
),
"FUSED_SILICA": contracts.PresetDef(
label="Fused Silica",
description="Fused silica aka. SiO2",
values={
"B1": 0.696166300,
"B2": 0.407942600,
"B3": 0.897479400,
"C1": 4.67914826e-3,
"C2": 1.35120631e-2,
"C3": 97.9340025,
},
"C1": 4.67914826e-3 * spu.um**2,
"C2": 1.35120631e-2 * spu.um**2,
"C3": 97.9340025 * spu.um**2,
}
),
}
####################
# - Properties
# - Output Socket Computation
####################
def draw_buttons(self, context, layout):
layout.prop(self, 'preset', text="")
@base.computes_output_socket("medium", td.Sellmeier)
def compute_medium(self: contracts.NodeTypeProtocol) -> td.Sellmeier:
## Retrieval
#B1 = self.compute_input("B1")
#C1_with_units = self.compute_input("C1")
#
## Processing
#C1 = spu.convert_to(C1_with_units, spu.um**2) / spu.um**2
return td.Sellmeier(coeffs = [
(
self.compute_input(f"B{i}"),
spu.convert_to(
self.compute_input(f"C{i}"),
spu.um**2,
) / spu.um**2
)
for i in [1, 2, 3]
])
@ -59,7 +95,7 @@ BL_REGISTER = [
TripleSellmeierMediumNode,
]
BL_NODES = {
types.NodeType.TripleSellmeierMedium: (
types.NodeCategory.MAXWELL_SIM_MEDIUMS_LINEARMEDIUMS
contracts.NodeType.TripleSellmeierMedium: (
contracts.NodeCategory.MAXWELL_SIM_MEDIUMS_LINEARMEDIUMS
)
}

View File

@ -1,140 +0,0 @@
import numpy as np
import bpy
import nodeitems_utils
from .. import types
####################
# - Decorator: Output Socket
####################
def output_socket_cb(name):
def decorator(func):
func._output_socket_name = name # Set a marker attribute
return func
return decorator
####################
# - Socket Type Casters
####################
SOCKET_CAST_MAP = {
"NodeSocketBool": bool,
"NodeSocketFloat": float,
"NodeSocketFloatAngle": float,
"NodeSocketFloatDistance": float,
"NodeSocketFloatFactor": float,
"NodeSocketFloatPercentage": float,
"NodeSocketFloatTime": float,
"NodeSocketFloatTimeAbsolute": float,
"NodeSocketFloatUnsigned": float,
"NodeSocketFloatInt": int,
"NodeSocketFloatIntFactor": int,
"NodeSocketFloatIntPercentage": int,
"NodeSocketFloatIntUnsigned": int,
"NodeSocketString": str,
"NodeSocketVector": np.array,
"NodeSocketVectorAcceleration": np.array,
"NodeSocketVectorDirection": np.array,
"NodeSocketVectorTranslation": np.array,
"NodeSocketVectorVelocity": np.array,
"NodeSocketVectorXYZ": np.array,
}
####################
# - Node Superclass
####################
def set_preset(self, context):
for preset_name, preset_dict in self.socket_presets["_values"].items():
if self.preset == preset_name:
for input_socket_name, value in preset_dict.items():
self.inputs[
self.input_sockets[input_socket_name][1]
].default_value = value
break
class MaxwellSimTreeNode(bpy.types.Node):
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
required_attrs = [
'bl_idname',
'bl_label',
'bl_icon',
'input_sockets',
'output_sockets',
]
for attr in required_attrs:
if getattr(cls, attr, None) is None:
raise TypeError(
f"class {cls.__name__} is missing required '{attr}' attribute"
)
if hasattr(cls, 'socket_presets'):
cls.__annotations__["preset"] = bpy.props.EnumProperty(
name="Presets",
description="Select a preset",
items=cls.socket_presets["_description"],
default=cls.socket_presets["_default"],
update=set_preset,
)
####################
# - Node Initialization
####################
def init(self, context):
for input_socket_name in self.input_sockets:
self.inputs.new(*self.input_sockets[input_socket_name][:2])
for output_socket_name in self.output_sockets:
self.outputs.new(*self.output_sockets[output_socket_name][:2])
set_preset(self, context)
####################
# - Node Computation
####################
def compute_input(self, input_socket_name: str):
"""Computes the value of an input socket name.
"""
bl_socket_type = self.input_sockets[input_socket_name][0]
bl_socket = self.inputs[self.input_sockets[input_socket_name][1]]
if bl_socket.is_linked:
linked_node = bl_socket.links[0].from_node
linked_bl_socket_name = bl_socket.links[0].from_socket.name
result = linked_node.compute_output(linked_bl_socket_name)
else:
result = bl_socket.default_value
if bl_socket_type in SOCKET_CAST_MAP:
return SOCKET_CAST_MAP[bl_socket_type](result)
return result
def compute_output(self, output_bl_socket_name: str):
"""Computes the value of an output socket name, from its Blender display name.
"""
output_socket_name = next(
output_socket_name
for output_socket_name in self.output_sockets.keys()
if self.output_sockets[output_socket_name][1] == output_bl_socket_name
)
output_socket_name_to_cb = {
getattr(attr, '_output_socket_name'): attr
for attr_name in dir(self)
if (
callable(attr := getattr(self, attr_name))
and hasattr(attr, '_output_socket_name')
)
}
return output_socket_name_to_cb[output_socket_name]()
####################
# - Blender Configuration
####################
@classmethod
def poll(cls, ntree):
"""Constrain node instantiation to within a MaxwellSimTree."""
return ntree.bl_idname == types.TreeType.MaxwellSim

View File

@ -0,0 +1,8 @@
from . import exporters
BL_REGISTER = [
*exporters.BL_REGISTER,
]
BL_NODES = {
**exporters.BL_NODES,
}

View File

@ -0,0 +1,8 @@
from . import json_file_exporter
BL_REGISTER = [
*json_file_exporter.BL_REGISTER,
]
BL_NODES = {
**json_file_exporter.BL_NODES,
}

View File

@ -0,0 +1,111 @@
import typing as typ
import json
from pathlib import Path
import bpy
import sympy as sp
import pydantic as pyd
from .... import contracts
from .... import sockets
from ... import base
####################
# - Operators
####################
class JSONFileExporterPrintJSON(bpy.types.Operator):
bl_idname = "blender_maxwell.json_file_exporter_print_json"
bl_label = "Print the JSON of what's linked into a JSONFileExporterNode."
@classmethod
def poll(cls, context):
return True
def execute(self, context):
node = context.node
print(node.linked_data_as_json())
return {'FINISHED'}
class JSONFileExporterSaveJSON(bpy.types.Operator):
bl_idname = "blender_maxwell.json_file_exporter_save_json"
bl_label = "Save the JSON of what's linked into a JSONFileExporterNode."
@classmethod
def poll(cls, context):
return True
def execute(self, context):
node = context.node
node.export_data_as_json()
return {'FINISHED'}
####################
# - Node
####################
class JSONFileExporterNode(base.MaxwellSimTreeNode):
node_type = contracts.NodeType.JSONFileExporter
bl_label = "JSON File Exporter"
#bl_icon = constants.ICON_SIM_INPUT
input_sockets = {
"json_path": sockets.FilePathSocketDef(
label="JSON Path",
default_path="simulation.json"
),
"data": sockets.AnySocketDef(
label="Data",
),
}
output_sockets = {}
####################
# - UI Layout
####################
def draw_operators(
self,
context: bpy.types.Context,
layout: bpy.types.UILayout,
) -> None:
layout.operator(JSONFileExporterPrintJSON.bl_idname, text="Print")
layout.operator(JSONFileExporterSaveJSON.bl_idname, text="Save")
####################
# - Methods
####################
def linked_data_as_json(self) -> str | None:
if self.g_input_bl_socket("data").is_linked:
data: typ.Any = self.compute_input("data")
# Tidy3D Objects: Call .json()
if hasattr(data, "json"):
return data.json()
# Pydantic Models: Call .model_dump_json()
elif isinstance(data, pyd.BaseModel):
return data.model_dump_json()
# Finally: Try json.dumps (might fail)
else:
json.dumps(data)
def export_data_as_json(self) -> None:
if (data := self.linked_data_as_json()):
data_dict = json.loads(data)
with self.compute_input("json_path").open("w") as f:
json.dump(data_dict, f, ensure_ascii=False, indent=4)
####################
# - Blender Registration
####################
BL_REGISTER = [
JSONFileExporterPrintJSON,
JSONFileExporterNode,
JSONFileExporterSaveJSON,
]
BL_NODES = {
contracts.NodeType.JSONFileExporter: (
contracts.NodeCategory.MAXWELL_SIM_OUTPUTS_EXPORTERS
)
}

View File

@ -0,0 +1,8 @@
from . import fdtd_simulation
BL_REGISTER = [
*fdtd_simulation.BL_REGISTER,
]
BL_NODES = {
**fdtd_simulation.BL_NODES,
}

View File

@ -0,0 +1,92 @@
import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
from ... import contracts
from ... import sockets
from .. import base
class FDTDSimNode(base.MaxwellSimTreeNode):
node_type = contracts.NodeType.FDTDSim
bl_label = "FDTD Simulation"
#bl_icon = ...
####################
# - Sockets
####################
input_sockets = {
"run_time": sockets.RealNumberSocketDef(
label="Run Time",
default_value=1.0,
),
"size_x": sockets.RealNumberSocketDef(
label="Size X",
default_value=1.0,
),
"size_y": sockets.RealNumberSocketDef(
label="Size Y",
default_value=1.0,
),
"size_z": sockets.RealNumberSocketDef(
label="Size Z",
default_value=1.0,
),
"ambient_medium": sockets.MaxwellMediumSocketDef(
label="Ambient Medium",
),
"source": sockets.MaxwellSourceSocketDef(
label="Source",
),
"structure": sockets.MaxwellStructureSocketDef(
label="Structure",
),
"bound": sockets.MaxwellBoundSocketDef(
label="Bound",
),
}
output_sockets = {
"fdtd_sim": sockets.MaxwellFDTDSimSocketDef(
label="Medium",
),
}
####################
# - Output Socket Computation
####################
@base.computes_output_socket("fdtd_sim", td.Sellmeier)
def compute_simulation(self: contracts.NodeTypeProtocol) -> td.Simulation:
run_time = self.compute_input("run_time")
ambient_medium = self.compute_input("ambient_medium")
structures = [self.compute_input("structure")]
sources = [self.compute_input("source")]
bound = self.compute_input("bound")
size = (
self.compute_input("size_x"),
self.compute_input("size_y"),
self.compute_input("size_z"),
)
return td.Simulation(
size=size,
medium=ambient_medium,
structures=structures,
sources=sources,
boundary_spec=bound,
run_time=run_time,
)
####################
# - Blender Registration
####################
BL_REGISTER = [
FDTDSimNode,
]
BL_NODES = {
contracts.NodeType.FDTDSim: (
contracts.NodeCategory.MAXWELL_SIM_SIMULATIONS
)
}

View File

@ -0,0 +1,8 @@
from . import modelled
BL_REGISTER = [
*modelled.BL_REGISTER,
]
BL_NODES = {
**modelled.BL_NODES,
}

View File

@ -0,0 +1,8 @@
from . import point_dipole_source
BL_REGISTER = [
*point_dipole_source.BL_REGISTER,
]
BL_NODES = {
**point_dipole_source.BL_NODES,
}

View File

@ -0,0 +1,70 @@
import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
from .... import contracts
from .... import sockets
from ... import base
class PointDipoleSourceNode(base.MaxwellSimTreeNode):
node_type = contracts.NodeType.PointDipoleSource
bl_label = "Point Dipole Source"
#bl_icon = ...
####################
# - Sockets
####################
input_sockets = {
"center_x": sockets.RealNumberSocketDef(
label="Center X",
default_value=0.0,
),
"center_y": sockets.RealNumberSocketDef(
label="Center Y",
default_value=0.0,
),
"center_z": sockets.RealNumberSocketDef(
label="Center Z",
default_value=0.0,
),
}
output_sockets = {
"source": sockets.MaxwellSourceSocketDef(
label="Source",
),
}
####################
# - Output Socket Computation
####################
@base.computes_output_socket("source", td.PointDipole)
def compute_source(self: contracts.NodeTypeProtocol) -> td.PointDipole:
center = (
self.compute_input("center_x"),
self.compute_input("center_y"),
self.compute_input("center_z"),
)
cheating_pulse = td.GaussianPulse(freq0=200e12, fwidth=20e12)
return td.PointDipole(
center=center,
source_time=cheating_pulse,
interpolate=True,
polarization="Ex",
)
####################
# - Blender Registration
####################
BL_REGISTER = [
PointDipoleSourceNode,
]
BL_NODES = {
contracts.NodeType.PointDipoleSource: (
contracts.NodeCategory.MAXWELL_SIM_SOURCES_MODELLED
)
}

View File

@ -0,0 +1,8 @@
from . import primitives
BL_REGISTER = [
*primitives.BL_REGISTER,
]
BL_NODES = {
**primitives.BL_NODES,
}

View File

@ -0,0 +1,8 @@
from . import box_structure
BL_REGISTER = [
*box_structure.BL_REGISTER,
]
BL_NODES = {
**box_structure.BL_NODES,
}

View File

@ -0,0 +1,90 @@
import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
from .... import contracts
from .... import sockets
from ... import base
class BoxStructureNode(base.MaxwellSimTreeNode):
node_type = contracts.NodeType.BoxStructure
bl_label = "Box Structure"
#bl_icon = ...
####################
# - Sockets
####################
input_sockets = {
"medium": sockets.MaxwellMediumSocketDef(
label="Medium",
),
"center_x": sockets.RealNumberSocketDef(
label="Center X",
default_value=0.0,
),
"center_y": sockets.RealNumberSocketDef(
label="Center Y",
default_value=0.0,
),
"center_z": sockets.RealNumberSocketDef(
label="Center Z",
default_value=0.0,
),
"size_x": sockets.RealNumberSocketDef(
label="Size X",
default_value=1.0,
),
"size_y": sockets.RealNumberSocketDef(
label="Size Y",
default_value=1.0,
),
"size_z": sockets.RealNumberSocketDef(
label="Size Z",
default_value=1.0,
),
}
output_sockets = {
"structure": sockets.MaxwellStructureSocketDef(
label="Structure",
),
}
####################
# - Output Socket Computation
####################
@base.computes_output_socket("structure", td.Box)
def compute_simulation(self: contracts.NodeTypeProtocol) -> td.Box:
medium = self.compute_input("medium")
center = (
self.compute_input("center_x"),
self.compute_input("center_y"),
self.compute_input("center_z"),
)
size = (
self.compute_input("size_x"),
self.compute_input("size_y"),
self.compute_input("size_z"),
)
return td.Structure(
geometry=td.Box(
center=center,
size=size,
),
medium=medium,
)
####################
# - Blender Registration
####################
BL_REGISTER = [
BoxStructureNode,
]
BL_NODES = {
contracts.NodeType.BoxStructure: (
contracts.NodeCategory.MAXWELL_SIM_STRUCTURES_PRIMITIES
)
}

View File

@ -1,72 +0,0 @@
import bpy
from . import constants, types
class MaxwellSourceSocket(bpy.types.NodeSocket):
bl_idname = types.SocketType.MaxwellSource
bl_label = "Maxwell Source"
def draw(self, context, layout, node, text):
layout.label(text=text)
@classmethod
def draw_color_simple(cls):
return constants.COLOR_SOCKET_SOURCE
class MaxwellMediumSocket(bpy.types.NodeSocket):
bl_idname = types.SocketType.MaxwellMedium
bl_label = "Maxwell Medium"
def draw(self, context, layout, node, text):
layout.label(text=text)
@classmethod
def draw_color_simple(cls):
return constants.COLOR_SOCKET_MEDIUM
class MaxwellStructureSocket(bpy.types.NodeSocket):
bl_idname = types.SocketType.MaxwellStructure
bl_label = "Maxwell Structure"
def draw(self, context, layout, node, text):
layout.label(text=text)
@classmethod
def draw_color_simple(cls):
return constants.COLOR_SOCKET_STRUCTURE
class MaxwellBoundSocket(bpy.types.NodeSocket):
bl_idname = types.SocketType.MaxwellBound
bl_label = "Maxwell Bound"
def draw(self, context, layout, node, text):
layout.label(text=text)
@classmethod
def draw_color_simple(cls):
return constants.COLOR_SOCKET_BOUND
class MaxwellFDTDSimSocket(bpy.types.NodeSocket):
bl_idname = types.SocketType.MaxwellFDTDSim
bl_label = "Maxwell FDTD Simulation"
def draw(self, context, layout, node, text):
layout.label(text=text)
@classmethod
def draw_color_simple(cls):
return constants.COLOR_SOCKET_FDTDSIM
####################
# - Blender Registration
####################
BL_REGISTER = [
MaxwellSourceSocket,
MaxwellMediumSocket,
MaxwellStructureSocket,
MaxwellBoundSocket,
MaxwellFDTDSimSocket,
]

View File

@ -0,0 +1,25 @@
from . import basic
AnySocketDef = basic.AnySocketDef
TextSocketDef = basic.TextSocketDef
FilePathSocketDef = basic.FilePathSocketDef
from . import number
RealNumberSocketDef = number.RealNumberSocketDef
ComplexNumberSocketDef = number.ComplexNumberSocketDef
from . import physical
PhysicalAreaSocketDef = physical.PhysicalAreaSocketDef
from . import maxwell
MaxwellBoundSocketDef = maxwell.MaxwellBoundSocketDef
MaxwellFDTDSimSocketDef = maxwell.MaxwellFDTDSimSocketDef
MaxwellMediumSocketDef = maxwell.MaxwellMediumSocketDef
MaxwellSourceSocketDef = maxwell.MaxwellSourceSocketDef
MaxwellStructureSocketDef = maxwell.MaxwellStructureSocketDef
BL_REGISTER = [
*basic.BL_REGISTER,
*number.BL_REGISTER,
*physical.BL_REGISTER,
*maxwell.BL_REGISTER,
]

View File

@ -0,0 +1,105 @@
import typing as typ
import bpy
from .. import contracts
class BLSocket(bpy.types.NodeSocket):
"""A base type for nodes that greatly simplifies the implementation of
reliable, powerful nodes.
Should be used together with `contracts.BLSocketProtocol`.
"""
def __init_subclass__(cls, **kwargs: typ.Any):
super().__init_subclass__(**kwargs) ## Yucky superclass setup.
# Set bl_idname
cls.bl_idname = cls.socket_type.value
# Declare Node Property: 'preset' EnumProperty
if hasattr(cls, "draw_preview"):
cls.__annotations__["preview_active"] = bpy.props.BoolProperty(
name="Preview",
description="Preview the socket value",
default=False,
)
####################
# - Methods
####################
def is_compatible(self, value: typ.Any) -> bool:
for compatible_type, checks in self.compatible_types.items():
if (
compatible_type is typ.Any or
isinstance(value, compatible_type)
):
return all(check(value) for check in checks)
return False
####################
# - UI
####################
@classmethod
def draw_color_simple(cls) -> contracts.BlenderColorRGB:
return cls.socket_color
def draw(
self,
context: bpy.types.Context,
layout: bpy.types.UILayout,
node: bpy.types.Node,
text: str,
) -> None:
if self.is_output:
self.draw_output(context, layout, node, text)
else:
self.draw_input(context, layout, node, text)
def draw_input(
self,
context: bpy.types.Context,
layout: bpy.types.UILayout,
node: bpy.types.Node,
text: str,
) -> None:
if self.is_linked:
layout.label(text=text)
return
# Column
col = layout.column(align=True)
# Row: Label & Preview Toggle
label_col_row = col.row(align=True)
if hasattr(self, "draw_label_row"):
self.draw_label_row(label_col_row, text)
else:
label_col_row.label(text=text)
if hasattr(self, "draw_preview"):
label_col_row.prop(
self,
"preview_active",
toggle=True,
text="",
icon="SEQ_PREVIEW",
)
# Row: Preview (in Box)
if hasattr(self, "draw_preview"):
if self.preview_active:
col_box = col.box()
self.draw_preview(col_box)
# Row(s): Value
if hasattr(self, "draw_value"):
self.draw_value(col)
def draw_output(
self,
context: bpy.types.Context,
layout: bpy.types.UILayout,
node: bpy.types.Node,
text: str,
) -> None:
layout.label(text=text)

View File

@ -0,0 +1,15 @@
from . import any_socket
AnySocketDef = any_socket.AnySocketDef
from . import text_socket
TextSocketDef = text_socket.TextSocketDef
from . import file_path_socket
FilePathSocketDef = file_path_socket.FilePathSocketDef
BL_REGISTER = [
*any_socket.BL_REGISTER,
*text_socket.BL_REGISTER,
*file_path_socket.BL_REGISTER,
]

View File

@ -0,0 +1,57 @@
import typing as typ
import bpy
import sympy as sp
import pydantic as pyd
from .. import base
from ... import contracts
####################
# - Blender Socket
####################
class AnyBLSocket(base.BLSocket):
socket_type = contracts.SocketType.Any
socket_color = (0.0, 0.0, 0.0, 1.0)
bl_label = "Any"
compatible_types = {
typ.Any: {},
}
####################
# - Socket UI
####################
def draw_label_row(self, label_col_row: bpy.types.UILayout, text: str) -> None:
"""Draw the value of the real number.
"""
label_col_row.label(text=text)
####################
# - Computation of Default Value
####################
@property
def default_value(self) -> None:
return None
@default_value.setter
def default_value(self, value: typ.Any) -> None:
pass
####################
# - Socket Configuration
####################
class AnySocketDef(pyd.BaseModel):
socket_type: contracts.SocketType = contracts.SocketType.Any
label: str
def init(self, bl_socket: AnyBLSocket) -> None:
pass
####################
# - Blender Registration
####################
BL_REGISTER = [
AnyBLSocket,
]

View File

@ -0,0 +1,84 @@
import typing as typ
from pathlib import Path
import bpy
import sympy as sp
import pydantic as pyd
from .. import base
from ... import contracts
####################
# - Blender Socket
####################
class FilePathBLSocket(base.BLSocket):
socket_type = contracts.SocketType.FilePath
socket_color = (0.2, 0.2, 0.2, 1.0)
bl_label = "File Path"
compatible_types = {
Path: {},
}
####################
# - Properties
####################
raw_value: bpy.props.StringProperty(
name="File Path",
description="Represents the path to a file",
#default="",
subtype="FILE_PATH",
)
####################
# - Socket UI
####################
def draw_value(self, col: bpy.types.UILayout) -> None:
col_row = col.row(align=True)
col_row.prop(self, "raw_value", text="")
####################
# - Computation of Default Value
####################
@property
def default_value(self) -> Path:
"""Return the text.
Returns:
The text as a string.
"""
return Path(str(self.raw_value))
@default_value.setter
def default_value(self, value: typ.Any) -> None:
"""Set the real number from some compatible type, namely
real sympy expressions with no symbols, or floats.
"""
# (Guard) Value Compatibility
if not self.is_compatible(value):
msg = f"Tried setting socket ({self}) to incompatible value ({value}) of type {type(value)}"
raise ValueError(msg)
self.raw_value = str(Path(value))
####################
# - Socket Configuration
####################
class FilePathSocketDef(pyd.BaseModel):
socket_type: contracts.SocketType = contracts.SocketType.FilePath
label: str
default_path: Path
def init(self, bl_socket: FilePathBLSocket) -> None:
bl_socket.default_value = self.default_path
####################
# - Blender Registration
####################
BL_REGISTER = [
FilePathBLSocket,
]

View File

@ -0,0 +1,81 @@
import typing as typ
import bpy
import sympy as sp
import pydantic as pyd
from .. import base
from ... import contracts
####################
# - Blender Socket
####################
class TextBLSocket(base.BLSocket):
socket_type = contracts.SocketType.Text
socket_color = (0.2, 0.2, 0.2, 1.0)
bl_label = "Text"
compatible_types = {
str: {},
}
####################
# - Properties
####################
raw_value: bpy.props.StringProperty(
name="Text",
description="Represents some text",
default="",
)
####################
# - Socket UI
####################
def draw_label_row(self, label_col_row: bpy.types.UILayout, text: str) -> None:
"""Draw the value of the real number.
"""
label_col_row.prop(self, "raw_value", text=text)
####################
# - Computation of Default Value
####################
@property
def default_value(self) -> str:
"""Return the text.
Returns:
The text as a string.
"""
return self.raw_value
@default_value.setter
def default_value(self, value: typ.Any) -> None:
"""Set the real number from some compatible type, namely
real sympy expressions with no symbols, or floats.
"""
# (Guard) Value Compatibility
if not self.is_compatible(value):
msg = f"Tried setting socket ({self}) to incompatible value ({value}) of type {type(value)}"
raise ValueError(msg)
self.raw_value = str(value)
####################
# - Socket Configuration
####################
class TextSocketDef(pyd.BaseModel):
socket_type: contracts.SocketType = contracts.SocketType.Text
label: str
def init(self, bl_socket: TextBLSocket) -> None:
pass
####################
# - Blender Registration
####################
BL_REGISTER = [
TextBLSocket,
]

View File

@ -0,0 +1,23 @@
from . import maxwell_bound_socket
MaxwellBoundSocketDef = maxwell_bound_socket.MaxwellBoundSocketDef
from . import maxwell_fdtd_sim_socket
MaxwellFDTDSimSocketDef = maxwell_fdtd_sim_socket.MaxwellFDTDSimSocketDef
from . import maxwell_medium_socket
MaxwellMediumSocketDef = maxwell_medium_socket.MaxwellMediumSocketDef
from . import maxwell_source_socket
MaxwellSourceSocketDef = maxwell_source_socket.MaxwellSourceSocketDef
from . import maxwell_structure_socket
MaxwellStructureSocketDef = maxwell_structure_socket.MaxwellStructureSocketDef
BL_REGISTER = [
*maxwell_bound_socket.BL_REGISTER,
*maxwell_fdtd_sim_socket.BL_REGISTER,
*maxwell_medium_socket.BL_REGISTER,
*maxwell_source_socket.BL_REGISTER,
*maxwell_structure_socket.BL_REGISTER,
]

View File

@ -0,0 +1,46 @@
import typing as typ
import bpy
import pydantic as pyd
import tidy3d as td
from .. import base
from ... import contracts
class MaxwellBoundBLSocket(base.BLSocket):
socket_type = contracts.SocketType.MaxwellBound
socket_color = (0.8, 0.8, 0.4, 1.0)
bl_label = "Maxwell Bound"
compatible_types = {
td.BoundarySpec: {}
}
####################
# - Computation of Default Value
####################
@property
def default_value(self) -> td.BoundarySpec:
return td.BoundarySpec()
@default_value.setter
def default_value(self, value: typ.Any) -> None:
return None
####################
# - Socket Configuration
####################
class MaxwellBoundSocketDef(pyd.BaseModel):
socket_type: contracts.SocketType = contracts.SocketType.MaxwellBound
label: str
def init(self, bl_socket: MaxwellBoundBLSocket) -> None:
pass
####################
# - Blender Registration
####################
BL_REGISTER = [
MaxwellBoundBLSocket,
]

View File

@ -0,0 +1,46 @@
import typing as typ
import bpy
import pydantic as pyd
import tidy3d as td
from .. import base
from ... import contracts
class MaxwellFDTDSimBLSocket(base.BLSocket):
socket_type = contracts.SocketType.MaxwellFDTDSim
socket_color = (0.8, 0.8, 0.4, 1.0)
bl_label = "Maxwell Source"
compatible_types = {
td.Simulation: {},
}
####################
# - Computation of Default Value
####################
@property
def default_value(self) -> None:
return None
@default_value.setter
def default_value(self, value: typ.Any) -> None:
pass
####################
# - Socket Configuration
####################
class MaxwellFDTDSimSocketDef(pyd.BaseModel):
socket_type: contracts.SocketType = contracts.SocketType.MaxwellFDTDSim
label: str
def init(self, bl_socket: MaxwellFDTDSimBLSocket) -> None:
pass
####################
# - Blender Registration
####################
BL_REGISTER = [
MaxwellFDTDSimBLSocket,
]

View File

@ -0,0 +1,89 @@
import typing as typ
import bpy
import pydantic as pyd
import tidy3d as td
from .. import base
from ... import contracts
class MaxwellMediumBLSocket(base.BLSocket):
socket_type = contracts.SocketType.MaxwellMedium
socket_color = (0.8, 0.8, 0.4, 1.0)
bl_label = "Maxwell Medium"
compatible_types = {
td.components.medium.AbstractMedium: {}
}
####################
# - Properties
####################
rel_permittivity: bpy.props.FloatProperty(
name="Permittivity",
description="Represents a simple, real permittivity.",
default=0.0,
precision=6,
)
####################
# - Socket UI
####################
def draw_value(self, col: bpy.types.UILayout) -> None:
"""Draw the value of the area, including a toggle for
specifying the active unit.
"""
col_row = col.row(align=True)
col_row.prop(self, "rel_permittivity", text="Re(eps_r)")
####################
# - Computation of Default Value
####################
@property
def default_value(self) -> td.Medium:
"""Return the built-in medium representation as a `tidy3d` object,
ready to use in the simulation.
Returns:
A completely normal medium with permittivity set.
"""
return td.Medium(
permittivity=self.rel_permittivity,
)
@default_value.setter
def default_value(self, value: typ.Any) -> None:
"""Set the built-in medium representation by adjusting the
permittivity, ONLY.
Args:
value: Must be a tidy3d.Medium, or similar subclass.
"""
# ONLY Allow td.Medium
if isinstance(value, td.Medium):
self.rel_permittivity = value.permittivity
msg = f"Tried setting MaxwellMedium socket ({self}) to something that isn't a simple `tidy3d.Medium`"
raise ValueError(msg)
####################
# - Socket Configuration
####################
class MaxwellMediumSocketDef(pyd.BaseModel):
socket_type: contracts.SocketType = contracts.SocketType.MaxwellMedium
label: str
rel_permittivity: float = 1.0
def init(self, bl_socket: MaxwellMediumBLSocket) -> None:
bl_socket.rel_permittivity = self.rel_permittivity
####################
# - Blender Registration
####################
BL_REGISTER = [
MaxwellMediumBLSocket,
]

View File

@ -0,0 +1,46 @@
import typing as typ
import bpy
import pydantic as pyd
import tidy3d as td
from .. import base
from ... import contracts
class MaxwellSourceBLSocket(base.BLSocket):
socket_type = contracts.SocketType.MaxwellSource
socket_color = (0.8, 0.8, 0.4, 1.0)
bl_label = "Maxwell Source"
compatible_types = {
td.components.base_sim.source.AbstractSource: {}
}
####################
# - Computation of Default Value
####################
@property
def default_value(self) -> td.Medium:
return None
@default_value.setter
def default_value(self, value: typ.Any) -> None:
pass
####################
# - Socket Configuration
####################
class MaxwellSourceSocketDef(pyd.BaseModel):
socket_type: contracts.SocketType = contracts.SocketType.MaxwellSource
label: str
def init(self, bl_socket: MaxwellSourceBLSocket) -> None:
pass
####################
# - Blender Registration
####################
BL_REGISTER = [
MaxwellSourceBLSocket,
]

View File

@ -0,0 +1,46 @@
import typing as typ
import bpy
import pydantic as pyd
import tidy3d as td
from .. import base
from ... import contracts
class MaxwellStructureBLSocket(base.BLSocket):
socket_type = contracts.SocketType.MaxwellStructure
socket_color = (0.8, 0.8, 0.4, 1.0)
bl_label = "Maxwell Structure"
compatible_types = {
td.components.structure.AbstractStructure: {}
}
####################
# - Computation of Default Value
####################
@property
def default_value(self) -> None:
return None
@default_value.setter
def default_value(self, value: typ.Any) -> None:
pass
####################
# - Socket Configuration
####################
class MaxwellStructureSocketDef(pyd.BaseModel):
socket_type: contracts.SocketType = contracts.SocketType.MaxwellStructure
label: str
def init(self, bl_socket: MaxwellStructureBLSocket) -> None:
pass
####################
# - Blender Registration
####################
BL_REGISTER = [
MaxwellStructureBLSocket,
]

View File

@ -0,0 +1,11 @@
from . import real_number_socket
RealNumberSocketDef = real_number_socket.RealNumberSocketDef
from . import complex_number_socket
ComplexNumberSocketDef = complex_number_socket.ComplexNumberSocketDef
BL_REGISTER = [
*real_number_socket.BL_REGISTER,
*complex_number_socket.BL_REGISTER,
]

View File

@ -0,0 +1,160 @@
import typing as typ
import bpy
import sympy as sp
import pydantic as pyd
from .. import base
from ... import contracts
####################
# - Blender Socket
####################
class ComplexNumberBLSocket(base.BLSocket):
socket_type = contracts.SocketType.ComplexNumber
socket_color = (0.6, 0.6, 0.6, 1.0)
bl_label = "Complex Number"
compatible_types = {
complex: {},
sp.Expr: {
lambda v: v.is_complex,
lambda v: len(v.free_symbols) == 0,
},
}
####################
# - Properties
####################
raw_value: bpy.props.FloatVectorProperty(
name="Complex Number",
description="Represents a complex number (real, imaginary)",
size=2,
default=(0.0, 0.0),
subtype='NONE'
)
coord_sys: bpy.props.EnumProperty(
name="Coordinate System",
description="Choose between cartesian and polar form",
items=[
("CARTESIAN", "Cartesian", "Use Cartesian Coordinates", "EMPTY_AXIS", 0),
("POLAR", "Polar", "Use Polar Coordinates", "DRIVER_ROTATIONAL_DIFFERENCE", 1),
],
default="CARTESIAN",
update=lambda self, context: self._update_coord_sys(),
)
####################
# - Socket UI
####################
def draw_value(self, col: bpy.types.UILayout) -> None:
"""Draw the value of the complex number, including a toggle for
specifying the active coordinate system.
"""
col_row = col.row()
col_row.prop(self, "raw_value", text="")
col.prop(self, "coord_sys", text="")
def draw_preview(self, col_box: bpy.types.UILayout) -> None:
"""Draw a live-preview value for the complex number, into the
given preview box.
- Cartesian: a,b -> a + ib
- Polar: r,t -> re^(it)
Returns:
The sympy expression representing the complex number.
"""
if self.coord_sys == "CARTESIAN":
text = f"= {self.default_value.n(2)}"
elif self.coord_sys == "POLAR":
r = sp.Abs(self.default_value).n(2)
theta_rad = sp.arg(self.default_value).n(2)
text = f"= {r*sp.exp(sp.I*theta_rad)}"
else:
raise RuntimeError("Invalid coordinate system for complex number")
col_box.label(text=text)
####################
# - Computation of Default Value
####################
@property
def default_value(self) -> sp.Expr:
"""Return the complex number as a sympy expression, of a form
determined by the coordinate system.
- Cartesian: a,b -> a + ib
- Polar: r,t -> re^(it)
Returns:
The sympy expression representing the complex number.
"""
v1, v2 = self.raw_value
return {
"CARTESIAN": v1 + sp.I*v2,
"POLAR": v1 * sp.exp(sp.I*v2),
}[self.coord_sys]
@default_value.setter
def default_value(self, value: typ.Any) -> None:
"""Set the complex number from a sympy expression, using an internal
representation determined by the coordinate system.
- Cartesian: a,b -> a + ib
- Polar: r,t -> re^(it)
"""
# (Guard) Value Compatibility
if not self.is_compatible(value):
msg = f"Tried setting socket ({self}) to incompatible value ({value}) of type {type(value)}"
raise ValueError(msg)
self.raw_value = {
"CARTESIAN": (sp.re(value), sp.im(value)),
"POLAR": (sp.Abs(value), sp.arg(value)),
}[self.coord_sys]
####################
# - Internal Update Methods
####################
def _update_coord_sys(self):
if self.coord_sys == "CARTESIAN":
r, theta_rad = self.raw_value
self.raw_value = (
r * sp.cos(theta_rad),
r * sp.sin(theta_rad),
)
elif self.coord_sys == "POLAR":
x, y = self.raw_value
cart_value = x + sp.I*y
self.raw_value = (
sp.Abs(cart_value),
sp.arg(cart_value) if y != 0 else 0,
)
####################
# - Socket Configuration
####################
class ComplexNumberSocketDef(pyd.BaseModel):
socket_type: contracts.SocketType = contracts.SocketType.ComplexNumber
label: str
preview: bool = False
coord_sys: typ.Literal["CARTESIAN", "POLAR"] = "CARTESIAN"
def init(self, bl_socket: ComplexNumberBLSocket) -> None:
bl_socket.preview_active = self.preview
bl_socket.coord_sys = self.coord_sys
####################
# - Blender Registration
####################
BL_REGISTER = [
ComplexNumberBLSocket,
]

View File

@ -0,0 +1,88 @@
import typing as typ
import bpy
import sympy as sp
import pydantic as pyd
from .. import base
from ... import contracts
####################
# - Blender Socket
####################
class RealNumberBLSocket(base.BLSocket):
socket_type = contracts.SocketType.RealNumber
socket_color = (0.6, 0.6, 0.6, 1.0)
bl_label = "Real Number"
compatible_types = {
float: {},
sp.Expr: {
lambda v: v.is_real,
lambda v: len(v.free_symbols) == 0,
},
}
####################
# - Properties
####################
raw_value: bpy.props.FloatProperty(
name="Real Number",
description="Represents a real number",
default=0.0,
precision=6,
)
####################
# - Socket UI
####################
def draw_label_row(self, label_col_row: bpy.types.UILayout, text: str) -> None:
"""Draw the value of the real number.
"""
label_col_row.prop(self, "raw_value", text=text)
####################
# - Computation of Default Value
####################
@property
def default_value(self) -> float:
"""Return the real number.
Returns:
The real number as a float.
"""
return self.raw_value
@default_value.setter
def default_value(self, value: typ.Any) -> None:
"""Set the real number from some compatible type, namely
real sympy expressions with no symbols, or floats.
"""
# (Guard) Value Compatibility
if not self.is_compatible(value):
msg = f"Tried setting socket ({self}) to incompatible value ({value}) of type {type(value)}"
raise ValueError(msg)
self.raw_value = float(value)
####################
# - Socket Configuration
####################
class RealNumberSocketDef(pyd.BaseModel):
socket_type: contracts.SocketType = contracts.SocketType.RealNumber
label: str
default_value: float = 0.0
def init(self, bl_socket: RealNumberBLSocket) -> None:
bl_socket.default_value = self.default_value
####################
# - Blender Registration
####################
BL_REGISTER = [
RealNumberBLSocket,
]

View File

@ -0,0 +1,7 @@
from . import physical_area_socket
PhysicalAreaSocketDef = physical_area_socket.PhysicalAreaSocketDef
BL_REGISTER = [
*physical_area_socket.BL_REGISTER,
]

View File

@ -0,0 +1,147 @@
import typing as typ
import bpy
import sympy as sp
import sympy.physics.units as spu
import pydantic as pyd
sp.printing.str.StrPrinter._default_settings['abbrev'] = True
## When we str() a unit expression, use abbrevied units.
from .. import base
from ... import contracts
UNITS = {
"PM_SQ": spu.pm**2,
"A_SQ": spu.angstrom**2,
"NM_SQ": spu.nm**2,
"UM_SQ": spu.um**2,
"MM_SQ": spu.mm**2,
"CM_SQ": spu.cm**2,
"M_SQ": spu.m**2,
}
DEFAULT_UNIT = "UM_SQ"
class PhysicalAreaBLSocket(base.BLSocket):
socket_type = contracts.SocketType.PhysicalArea
socket_color = (0.8, 0.5, 0.5, 1.0)
bl_label = "Physical Area"
compatible_types = {
sp.Expr: {
lambda v: v.is_real,
lambda v: len(v.free_symbols) == 0,
lambda v: any(
contracts.is_exactly_expressed_as_unit(v, unit)
for unit in UNITS.values()
)
},
}
####################
# - Properties
####################
raw_value: bpy.props.FloatProperty(
name="Unitless Area",
description="Represents the unitless part of the area",
default=0.0,
precision=6,
)
unit: bpy.props.EnumProperty(
name="Unit",
description="Choose between area units",
items=[
(unit_name, str(unit_value), str(unit_value))
for unit_name, unit_value in UNITS.items()
],
default=DEFAULT_UNIT,
update=lambda self, context: self._update_unit(),
)
unit_previous: bpy.props.StringProperty(default=DEFAULT_UNIT)
####################
# - Socket UI
####################
def draw_label_row(self, label_col_row: bpy.types.UILayout, text: str) -> None:
"""Draw the value of the area, including a toggle for
specifying the active unit.
"""
label_col_row.label(text=text)
#label_col_row.prop(self, "raw_value", text="")
label_col_row.prop(self, "unit", text="")
def draw_value(self, col: bpy.types.UILayout) -> None:
col_row = col.row(align=True)
col_row.prop(self, "raw_value", text="")
#col_row.prop(self, "unit", text="")
####################
# - Computation of Default Value
####################
@property
def default_value(self) -> sp.Expr:
"""Return the area as a sympy expression, which is a pure real
number perfectly expressed as the active unit.
Returns:
The area as a sympy expression (with units).
"""
return self.raw_value * UNITS[self.unit]
@default_value.setter
def default_value(self, value: typ.Any) -> None:
"""Set the area from a sympy expression, including any required
unit conversions to normalize the input value to the selected
units.
"""
# (Guard) Value Compatibility
if not self.is_compatible(value):
msg = f"Tried setting socket ({self}) to incompatible value ({value}) of type {type(value)}"
raise ValueError(msg)
self.raw_value = spu.convert_to(
value, UNITS[self.unit]
) / UNITS[self.unit]
####################
# - Internal Update Methods
####################
def _update_unit(self):
old_unit = UNITS[self.unit_previous]
new_unit = UNITS[self.unit]
self.raw_value = spu.convert_to(
self.raw_value * old_unit,
new_unit,
) / new_unit
self.unit_previous = self.unit
####################
# - Socket Configuration
####################
class PhysicalAreaSocketDef(pyd.BaseModel):
socket_type: contracts.SocketType = contracts.SocketType.PhysicalArea
label: str
default_unit: typ.Literal[
"PM_SQ",
"A_SQ",
"NM_SQ",
"UM_SQ",
"MM_SQ",
"CM_SQ",
"M_SQ",
]
def init(self, bl_socket: PhysicalAreaBLSocket) -> None:
bl_socket.unit = self.default_unit
####################
# - Blender Registration
####################
BL_REGISTER = [
PhysicalAreaBLSocket,
]

View File

@ -1,17 +0,0 @@
import bpy
from . import types, constants
class MaxwellSimTree(bpy.types.NodeTree):
bl_idname = types.TreeType.MaxwellSim
bl_label = "Maxwell Sim Editor"
bl_icon = constants.ICON_SIM ## Icon ID
####################
# - Blender Registration
####################
BL_REGISTER = [
MaxwellSimTree,
]

View File

@ -1,2 +1,3 @@
tidy3d==2.5.2
pydantic==2.6.0
sympy==1.12

BIN
code/demo.blend (Stored with Git LFS)

Binary file not shown.

View File

@ -2,5 +2,10 @@
blender --python run.py
if [ $? -eq 42 ]; then
echo
echo
echo
echo
echo
blender --python run.py
fi

View File

@ -1,34 +0,0 @@
import enum
class BlenderTypeEnum(str, enum.Enum):
def _generate_next_value_(name, start, count, last_values):
return name
def blender_type_enum(cls):
# Construct Set w/Modified Member Names
new_members = {name: f"{name}{cls.__name__}" for name, member in cls.__members__.items()}
# Dynamically Declare New Enum Class w/Modified Members
new_cls = enum.Enum(cls.__name__, new_members, type=BlenderTypeEnum)
new_cls.__module__ = cls.__module__
# Return New (Replacing) Enum Class
return new_cls
@blender_type_enum
class TreeType(enum.Enum):
MaxwellSim = enum.auto()
@blender_type_enum
class SocketType(enum.Enum):
MaxwellSource = enum.auto()
MaxwellMedium = enum.auto()
MaxwellStructure = enum.auto()
MaxwellBound = enum.auto()
MaxwellFDTDSim = enum.auto()
# Demonstration
print(TreeType.MaxwellSim.value) # Should print "MaxwellSimTreeType"
print(SocketType.MaxwellSource.value) # Should print "MaxwellSourceSocketType"