diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/__init__.py b/blender_maxwell/node_trees/maxwell_sim_nodes/__init__.py index 631e648..0c8d410 100644 --- a/blender_maxwell/node_trees/maxwell_sim_nodes/__init__.py +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/__init__.py @@ -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, ] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/categories.py b/blender_maxwell/node_trees/maxwell_sim_nodes/categories.py index fd785ca..5ad45ed 100644 --- a/blender_maxwell/node_trees/maxwell_sim_nodes/categories.py +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/categories.py @@ -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 diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/constants.py b/blender_maxwell/node_trees/maxwell_sim_nodes/constants.py deleted file mode 100644 index 3355e8d..0000000 --- a/blender_maxwell/node_trees/maxwell_sim_nodes/constants.py +++ /dev/null @@ -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 diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/types.py b/blender_maxwell/node_trees/maxwell_sim_nodes/contracts.py similarity index 70% rename from blender_maxwell/node_trees/maxwell_sim_nodes/types.py rename to blender_maxwell/node_trees/maxwell_sim_nodes/contracts.py index 5211c2e..2fffb00 100644 --- a/blender_maxwell/node_trees/maxwell_sim_nodes/types.py +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/contracts.py @@ -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: + ... diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/node_tree.py b/blender_maxwell/node_trees/maxwell_sim_nodes/node_tree.py new file mode 100644 index 0000000..3494b83 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/node_tree.py @@ -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'} +#) diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/__init__.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/__init__.py index 72785cc..4511cb8 100644 --- a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/__init__.py +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/__init__.py @@ -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, } diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py new file mode 100644 index 0000000..fd08654 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py @@ -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) diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/__init__.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/__init__.py new file mode 100644 index 0000000..18e54fb --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/__init__.py @@ -0,0 +1,8 @@ +from . import constants + +BL_REGISTER = [ + *constants.BL_REGISTER, +] +BL_NODES = { + **constants.BL_NODES, +} diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/__init__.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/__init__.py new file mode 100644 index 0000000..3293f53 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/__init__.py @@ -0,0 +1,8 @@ +from . import complex_constant + +BL_REGISTER = [ + *complex_constant.BL_REGISTER, +] +BL_NODES = { + **complex_constant.BL_NODES, +} diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constant/complex_constant.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/area_constant.py similarity index 100% rename from blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constant/complex_constant.py rename to blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/area_constant.py diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/complex_constant.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/complex_constant.py new file mode 100644 index 0000000..b5c9dc3 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/complex_constant.py @@ -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 + ) +} diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constant/float_constant.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/length_constant.py similarity index 100% rename from blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constant/float_constant.py rename to blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/length_constant.py diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constant/scientific_constant.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/real_constant.py similarity index 100% rename from blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constant/scientific_constant.py rename to blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/real_constant.py diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constant/vec3_constant.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/scientific_constant.py similarity index 100% rename from blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constant/vec3_constant.py rename to blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/scientific_constant.py diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/parameter/complex_parameter.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/vec3_constant.py similarity index 100% rename from blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/parameter/complex_parameter.py rename to blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/vec3_constant.py diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/parameter/float_parameter.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/parameters/complex_parameter.py similarity index 100% rename from blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/parameter/float_parameter.py rename to blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/parameters/complex_parameter.py diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/parameter/vec3_parameter.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/parameters/float_parameter.py similarity index 100% rename from blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/parameter/vec3_parameter.py rename to blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/parameters/float_parameter.py diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporter/json_file_export.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/parameters/vec3_parameter.py similarity index 100% rename from blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporter/json_file_export.py rename to blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/parameters/vec3_parameter.py diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/mediums/linear_mediums/alternate_idea.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/mediums/linear_mediums/alternate_idea.py new file mode 100644 index 0000000..f11b58a --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/mediums/linear_mediums/alternate_idea.py @@ -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 + ) +} diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/mediums/linear_mediums/triple_sellmeier_medium.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/mediums/linear_mediums/triple_sellmeier_medium.py index de705a8..abc4aeb 100644 --- a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/mediums/linear_mediums/triple_sellmeier_medium.py +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/mediums/linear_mediums/triple_sellmeier_medium.py @@ -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") - } - 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, - "C2": 2.00179144e-2, - "C3": 103.560653, - }, - "FUSED_SILICA": { - "B1": 0.696166300, - "B2": 0.407942600, - "B3": 0.897479400, - "C1": 4.67914826e-3, - "C2": 1.35120631e-2, - "C3": 97.9340025, - }, - } + "medium": sockets.MaxwellMediumSocketDef( + label="Medium" + ), } #################### - # - Properties + # - Presets #################### - def draw_buttons(self, context, layout): - layout.prop(self, 'preset', text="") + 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 * 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 * spu.um**2, + "C2": 1.35120631e-2 * spu.um**2, + "C3": 97.9340025 * spu.um**2, + } + ), + } + + #################### + # - Output Socket Computation + #################### + @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 ) } diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/node_base.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/node_base.py deleted file mode 100644 index 8cb3b48..0000000 --- a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/node_base.py +++ /dev/null @@ -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 diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/__init__.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/__init__.py new file mode 100644 index 0000000..5d3ef72 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/__init__.py @@ -0,0 +1,8 @@ +from . import exporters + +BL_REGISTER = [ + *exporters.BL_REGISTER, +] +BL_NODES = { + **exporters.BL_NODES, +} diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporters/__init__.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporters/__init__.py new file mode 100644 index 0000000..7f2f7b3 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporters/__init__.py @@ -0,0 +1,8 @@ +from . import json_file_exporter + +BL_REGISTER = [ + *json_file_exporter.BL_REGISTER, +] +BL_NODES = { + **json_file_exporter.BL_NODES, +} diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporters/json_file_exporter.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporters/json_file_exporter.py new file mode 100644 index 0000000..c6bd439 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporters/json_file_exporter.py @@ -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 + ) +} diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/__init__.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/__init__.py new file mode 100644 index 0000000..6da029d --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/__init__.py @@ -0,0 +1,8 @@ +from . import fdtd_simulation + +BL_REGISTER = [ + *fdtd_simulation.BL_REGISTER, +] +BL_NODES = { + **fdtd_simulation.BL_NODES, +} diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/fdtd_simulation.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/fdtd_simulation.py index e69de29..56bd91b 100644 --- a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/fdtd_simulation.py +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/fdtd_simulation.py @@ -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 + ) +} diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/__init__.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/__init__.py new file mode 100644 index 0000000..b416f32 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/__init__.py @@ -0,0 +1,8 @@ +from . import modelled + +BL_REGISTER = [ + *modelled.BL_REGISTER, +] +BL_NODES = { + **modelled.BL_NODES, +} diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/modelled/__init__.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/modelled/__init__.py new file mode 100644 index 0000000..8cd9029 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/modelled/__init__.py @@ -0,0 +1,8 @@ +from . import point_dipole_source + +BL_REGISTER = [ + *point_dipole_source.BL_REGISTER, +] +BL_NODES = { + **point_dipole_source.BL_NODES, +} diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/modelled/point_dipole_source.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/modelled/point_dipole_source.py index e69de29..8c182c5 100644 --- a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/modelled/point_dipole_source.py +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/modelled/point_dipole_source.py @@ -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 + ) +} diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/__init__.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/__init__.py new file mode 100644 index 0000000..edc6d9c --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/__init__.py @@ -0,0 +1,8 @@ +from . import primitives + +BL_REGISTER = [ + *primitives.BL_REGISTER, +] +BL_NODES = { + **primitives.BL_NODES, +} diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/__init__.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/__init__.py new file mode 100644 index 0000000..8413691 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/__init__.py @@ -0,0 +1,8 @@ +from . import box_structure + +BL_REGISTER = [ + *box_structure.BL_REGISTER, +] +BL_NODES = { + **box_structure.BL_NODES, +} diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/box_structure.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/box_structure.py index e69de29..731a945 100644 --- a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/box_structure.py +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/box_structure.py @@ -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 + ) +} diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/socket_types.py b/blender_maxwell/node_trees/maxwell_sim_nodes/socket_types.py deleted file mode 100644 index 220aa25..0000000 --- a/blender_maxwell/node_trees/maxwell_sim_nodes/socket_types.py +++ /dev/null @@ -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, -] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/__init__.py b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/__init__.py new file mode 100644 index 0000000..e2cd105 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/__init__.py @@ -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, +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/base.py b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/base.py new file mode 100644 index 0000000..3888c5a --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/base.py @@ -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) diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/__init__.py b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/__init__.py new file mode 100644 index 0000000..0f5cc2a --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/__init__.py @@ -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, +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/any_socket.py b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/any_socket.py new file mode 100644 index 0000000..1da41b2 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/any_socket.py @@ -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, +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/file_path_socket.py b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/file_path_socket.py new file mode 100644 index 0000000..9516cfe --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/file_path_socket.py @@ -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, +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/text_socket.py b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/text_socket.py new file mode 100644 index 0000000..dfdbff7 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/text_socket.py @@ -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, +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/__init__.py b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/__init__.py new file mode 100644 index 0000000..3c36818 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/__init__.py @@ -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, +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/maxwell_bound_socket.py b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/maxwell_bound_socket.py new file mode 100644 index 0000000..8192a09 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/maxwell_bound_socket.py @@ -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, +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/maxwell_fdtd_sim_socket.py b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/maxwell_fdtd_sim_socket.py new file mode 100644 index 0000000..5aecc8e --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/maxwell_fdtd_sim_socket.py @@ -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, +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/maxwell_medium_socket.py b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/maxwell_medium_socket.py new file mode 100644 index 0000000..97f7f53 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/maxwell_medium_socket.py @@ -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, +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/maxwell_source_socket.py b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/maxwell_source_socket.py new file mode 100644 index 0000000..49ed8e8 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/maxwell_source_socket.py @@ -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, +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/maxwell_structure_socket.py b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/maxwell_structure_socket.py new file mode 100644 index 0000000..801f1f7 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/maxwell_structure_socket.py @@ -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, +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/__init__.py b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/__init__.py new file mode 100644 index 0000000..6a946e5 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/__init__.py @@ -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, +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/complex_number_socket.py b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/complex_number_socket.py new file mode 100644 index 0000000..8e4c7bc --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/complex_number_socket.py @@ -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, +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/rational_number_socket.py b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/rational_number_socket.py new file mode 100644 index 0000000..e69de29 diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/real_number_socket.py b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/real_number_socket.py new file mode 100644 index 0000000..09bafbf --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/real_number_socket.py @@ -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, +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/__init__.py b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/__init__.py new file mode 100644 index 0000000..ade66a6 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/__init__.py @@ -0,0 +1,7 @@ +from . import physical_area_socket +PhysicalAreaSocketDef = physical_area_socket.PhysicalAreaSocketDef + + +BL_REGISTER = [ + *physical_area_socket.BL_REGISTER, +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/base.py b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/base.py new file mode 100644 index 0000000..e69de29 diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/physical_area_socket.py b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/physical_area_socket.py new file mode 100644 index 0000000..cbac4a3 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/physical_area_socket.py @@ -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, +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/physical_length_socket.py b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/physical_length_socket.py new file mode 100644 index 0000000..e69de29 diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/physical_mass_socket.py b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/physical_mass_socket.py new file mode 100644 index 0000000..e69de29 diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/physical_speed_socket.py b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/physical_speed_socket.py new file mode 100644 index 0000000..e69de29 diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/physical_time_socket.py b/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/physical_time_socket.py new file mode 100644 index 0000000..e69de29 diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/tree.py b/blender_maxwell/node_trees/maxwell_sim_nodes/tree.py deleted file mode 100644 index 79f0ca6..0000000 --- a/blender_maxwell/node_trees/maxwell_sim_nodes/tree.py +++ /dev/null @@ -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, -] diff --git a/blender_maxwell/requirements.txt b/blender_maxwell/requirements.txt index e06b29a..2d1034c 100644 --- a/blender_maxwell/requirements.txt +++ b/blender_maxwell/requirements.txt @@ -1,2 +1,3 @@ tidy3d==2.5.2 pydantic==2.6.0 +sympy==1.12 diff --git a/run.sh b/run.sh index 2167806..7a061df 100644 --- a/run.sh +++ b/run.sh @@ -2,5 +2,10 @@ blender --python run.py if [ $? -eq 42 ]; then + echo + echo + echo + echo + echo blender --python run.py fi diff --git a/test.py b/test.py deleted file mode 100644 index a5f8502..0000000 --- a/test.py +++ /dev/null @@ -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" -