commit 3d9398cffd2c035485fbe27834a4e6535f744981 Author: Sofus Albert Høgsbro Rose Date: Tue Feb 6 21:44:43 2024 +0100 feat: Somewhat working addon. Solved a lot of problems related to bundled Python environment flushing for reloading. However, we have a really solid framework for computing node trees, and we can now both construct Tidy3D objects and noodle them into the "Debug Printer". Next step is rote implementation of relevant nodes, then live-visualization of the simulation setup. See #2 for progress tracking. diff --git a/blender_maxwell/__init__.py b/blender_maxwell/__init__.py new file mode 100644 index 0000000..e75a821 --- /dev/null +++ b/blender_maxwell/__init__.py @@ -0,0 +1,72 @@ +bl_info = { + "name": "Maxwell Simulation and Visualization", + "blender": (4, 0, 2), + "category": "Node", + "description": "Custom node trees for defining and visualizing Maxwell simulation.", + "author": "Sofus Albert Høgsbro Rose", + "version": (0, 1), + "wiki_url": "https://git.sofus.io/dtu-courses/bsc_thesis", + "tracker_url": "https://git.sofus.io/dtu-courses/bsc_thesis/issues", +} + +#################### +# - sys.path Library Inclusion +#################### +import sys +sys.path.insert(0, "/home/sofus/src/college/bsc_ge/thesis/code/.cached-dependencies") +## ^^ Placeholder + +#################### +# - Module Import +#################### +if "bpy" not in locals(): + import bpy + import nodeitems_utils + try: + from . import node_trees + from . import operators + from . import preferences + except ImportError: + import sys + sys.path.insert(0, "/home/sofus/src/college/bsc_ge/thesis/code/blender-maxwell") + import node_trees + import operators + import preferences +else: + import importlib + + importlib.reload(node_trees) + + +#################### +# - Registration +#################### +BL_REGISTER = [ + *node_trees.BL_REGISTER, + *operators.BL_REGISTER, + *preferences.BL_REGISTER, +] +BL_NODE_CATEGORIES = [ + *node_trees.BL_NODE_CATEGORIES, +] + +def register(): + for cls in BL_REGISTER: + bpy.utils.register_class(cls) + + for bl_node_category in BL_NODE_CATEGORIES: + nodeitems_utils.register_node_categories(*bl_node_category) + +def unregister(): + for bl_node_category in reversed(BL_NODE_CATEGORIES): + try: + nodeitems_utils.unregister_node_categories(bl_node_category[0]) + except: pass + + for cls in reversed(BL_REGISTER): + try: + bpy.utils.unregister_class(cls) + except: pass + +if __name__ == "__main__": + register() diff --git a/blender_maxwell/node_trees/__init__.py b/blender_maxwell/node_trees/__init__.py new file mode 100644 index 0000000..80f25bd --- /dev/null +++ b/blender_maxwell/node_trees/__init__.py @@ -0,0 +1,9 @@ +from . import maxwell_sim_nodes + +BL_REGISTER = [ + *maxwell_sim_nodes.BL_REGISTER, +] + +BL_NODE_CATEGORIES = [ + *maxwell_sim_nodes.BL_NODE_CATEGORIES, +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/__init__.py b/blender_maxwell/node_trees/maxwell_sim_nodes/__init__.py new file mode 100644 index 0000000..5c906d5 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/__init__.py @@ -0,0 +1,11 @@ +from . import tree, socket_types, nodes + +BL_REGISTER = [ + *tree.BL_REGISTER, + *socket_types.BL_REGISTER, + *nodes.BL_REGISTER, +] + +BL_NODE_CATEGORIES = [ + *nodes.BL_NODE_CATEGORIES, +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/constants.py b/blender_maxwell/node_trees/maxwell_sim_nodes/constants.py new file mode 100644 index 0000000..3355e8d --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/constants.py @@ -0,0 +1,20 @@ +#################### +# - 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/nodes/__init__.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/__init__.py new file mode 100644 index 0000000..93d120e --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/__init__.py @@ -0,0 +1,9 @@ +from . import definitions, categories + +BL_REGISTER = [ + *definitions.BL_REGISTER, +] + +BL_NODE_CATEGORIES = [ + *categories.BL_NODE_CATEGORIES, +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/categories.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/categories.py new file mode 100644 index 0000000..2012d78 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/categories.py @@ -0,0 +1,58 @@ +import nodeitems_utils +from . import types + +#################### +# - Node Category Types +#################### +CATEGORIES_MAXWELL_SIM = 'MAXWELL_SIM' + +CATEGORY_MAXWELL_SIM_DEBUG = 'MAXWELL_SIM_DEBUG' +CATEGORY_MAXWELL_SIM_SOURCES = 'MAXWELL_SIM_SOURCES' +CATEGORY_MAXWELL_SIM_MEDIUMS = 'MAXWELL_SIM_MEDIUMS' +CATEGORY_MAXWELL_SIM_STRUCTURES = 'MAXWELL_SIM_STRUCTURES' +CATEGORY_MAXWELL_SIM_BOUNDS = 'MAXWELL_SIM_BOUNDS' +CATEGORY_MAXWELL_SIM_SIMULATIONS = 'MAXWELL_SIM_SIMULATIONS' + +#################### +# - Node Category Class +#################### +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.tree_types.MaxwellSimTreeType + + +#################### +# - Node Category Definition +#################### +CATEGORIES_MaxwellSimTree = [ + MaxwellSimNodeCategory(CATEGORY_MAXWELL_SIM_DEBUG, "Debug", items=[ + nodeitems_utils.NodeItem(types.DebugPrinterNodeType), + ]), + MaxwellSimNodeCategory(CATEGORY_MAXWELL_SIM_SOURCES, "Sources", items=[ + nodeitems_utils.NodeItem(types.PointDipoleMaxwellSourceNodeType), + ]), + MaxwellSimNodeCategory(CATEGORY_MAXWELL_SIM_MEDIUMS, "Mediums", items=[ + nodeitems_utils.NodeItem(types.SellmeierMaxwellMediumNodeType), + ]), + MaxwellSimNodeCategory(CATEGORY_MAXWELL_SIM_STRUCTURES, "Structures", items=[ + nodeitems_utils.NodeItem(types.TriMeshMaxwellStructureNodeType), + ]), + MaxwellSimNodeCategory(CATEGORY_MAXWELL_SIM_BOUNDS, "Bounds", items=[ + nodeitems_utils.NodeItem(types.PMLMaxwellBoundNodeType), + ]), + MaxwellSimNodeCategory(CATEGORY_MAXWELL_SIM_SIMULATIONS, "Simulation", items=[ + nodeitems_utils.NodeItem(types.FDTDMaxwellSimulationNodeType), + ]), +] + + + +#################### +# - Blender Registration +#################### +BL_NODE_CATEGORIES = [ + (CATEGORIES_MAXWELL_SIM, CATEGORIES_MaxwellSimTree) +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/constants.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/constants.py new file mode 100644 index 0000000..c766047 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/constants.py @@ -0,0 +1 @@ +from .. import constants as tree_constants diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/definitions/__init__.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/definitions/__init__.py new file mode 100644 index 0000000..c79f642 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/definitions/__init__.py @@ -0,0 +1,16 @@ +#from . import bounds, mediums, simulations, sources, structures +from . import debug +from . import sources +#from . import mediums +#from . import structures +#from . import bounds +from . import simulations + +BL_REGISTER = [ + *debug.BL_REGISTER, + #*bounds.BL_REGISTER, + #*mediums.BL_REGISTER, + *sources.BL_REGISTER, + #*structures.BL_REGISTER, + *simulations.BL_REGISTER, +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/definitions/bounds.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/definitions/bounds.py new file mode 100644 index 0000000..7a80f20 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/definitions/bounds.py @@ -0,0 +1 @@ +BL_REGISTER = [] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/definitions/debug.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/definitions/debug.py new file mode 100644 index 0000000..417e20f --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/definitions/debug.py @@ -0,0 +1,53 @@ +import bpy +from .. import types, constants + +class DebugPrinterNodeOperator(bpy.types.Operator): + """Print, to the console, the object retrieved by the calling + DebugPrinterNode. + """ + + bl_idname = "blender_maxwell.debug_printer_node_operator" + bl_label = "Print the object linked into a DebugPrinterNode." + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + node = context.node + node.print_linked_data() + return {'FINISHED'} + +class DebugPrinterNode(types.MaxwellSimTreeNode): + bl_idname = types.DebugPrinterNodeType + bl_label = "Debug Printer" + bl_icon = constants.tree_constants.ICON_SIM + + input_sockets = { + "data": ("NodeSocketVirtual", "Data", lambda v: v), + } + output_sockets = {} + + #################### + # - Setup and Computation + #################### + def print_linked_data(self): + if self.inputs[self.input_sockets["data"][1]].is_linked: + print(self.compute_input("data")) + + + #################### + # - Node UI and Layout + #################### + def draw_buttons(self, context, layout): + layout.operator(DebugPrinterNodeOperator.bl_idname, text="Print Debug") + + + +#################### +# - Blender Registration +#################### +BL_REGISTER = [ + DebugPrinterNodeOperator, + DebugPrinterNode, +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/definitions/mediums.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/definitions/mediums.py new file mode 100644 index 0000000..b87bcb2 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/definitions/mediums.py @@ -0,0 +1,88 @@ +from dataclasses import dataclass +import bpy +from .. import types, constants + +#################### +# - Utilities (to move out) +#################### +def mk_set_preset(preset_dataclass): + def set_preset(self, context): + if self.preset in preset_dataclass.values: + for parameter, value in preset_dataclass.values.items(): + self.inputs[parameter].default_value = value + + return set_preset + + +#################### +# - Medium: Sellmeier +#################### +@dataclass +class _Presets_SellmeierMaxwellMediumNode: + descriptions = [ + ('BK7', "BK7 Glass", "Borosilicate crown glass (known as BK7)"), + ('FUSED_SILICA', "Fused Silica", "Fused silica aka. SiO2"), + ] + values = { + "BK7": { + "B1": 1.03961212, + "B2": 0.231792344, + "B3": 1.01046945, + "C1 (um^2)": 6.00069867e-3, + "C2 (um^2)": 2.00179144e-2, + "C3 (um^2)": 103.560653, + }, + "FUSED_SILICA": { + "B1": 0.696166300, + "B2": 0.407942600, + "B3": 0.897479400, + "C1 (um^2)": 4.67914826e-3, + "C2 (um^2)": 1.35120631e-2, + "C3 (um^2)": 97.9340025, + }, + } + + def mk_set_preset(self): + return mk_set_preset(self) + +Presets_SellmeierMaxwellMediumNode = _Presets_SellmeierMaxwellMediumNode() + +class SellmeierMaxwellMediumNode(types.MaxwellSimTreeNode, bpy.types.Node): + bl_idname = types.SellmeierMaxwellMediumNodeType + bl_label = "Sellmeier" + bl_icon = constants.tree_constants.ICON_SIM_MEDIUM + + preset: bpy.props.EnumProperty( + name="Presets", + description="Select a preset", + items=Presets_SellmeierMaxwellMediumNode.descriptions, + default='BK7', + update=Presets_SellmeierMaxwellMediumNode.mk_set_preset(), + ) + + def init(self, context): + # Declare Node Inputs + self.inputs.new('NodeSocketFloat', "B1") + self.inputs.new('NodeSocketFloat', "B2") + self.inputs.new('NodeSocketFloat', "B3") + self.inputs.new('NodeSocketFloat', "C1 (um^2)") + self.inputs.new('NodeSocketFloat', "C2 (um^2)") + self.inputs.new('NodeSocketFloat', "C3 (um^2)") + + # Declare Node Outputs + self.outputs.new(types.tree_types.MaxwellMediumSocketType, "Medium") + + # Set Preset Values + Presets_SellmeierMaxwellMediumNode.mk_set_preset()(self, context) + + def draw_buttons(self, context, layout): + layout.prop(self, 'sellmeier_presets', text="") + + + +#################### +# - Blender Registration +#################### +BL_REGISTER = [ + SellmeierMaxwellMediumNode, +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/definitions/simulations.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/definitions/simulations.py new file mode 100644 index 0000000..076e620 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/definitions/simulations.py @@ -0,0 +1,49 @@ +import bpy +import tidy3d as td + +from .. import types, constants + +class FDTDMaxwellSimulationNode(types.MaxwellSimTreeNode, bpy.types.Node): + bl_idname = types.FDTDMaxwellSimulationNodeType + bl_label = "FDTD" + bl_icon = constants.tree_constants.ICON_SIM_SIMULATION + + input_sockets = { + "run_time": ("NodeSocketFloatTime", "Run Time"), + "ambient_medium": (types.tree_types.MaxwellMediumSocketType, "Ambient Medium"), + "source": (types.tree_types.MaxwellSourceSocketType, "Source"), + "structure": (types.tree_types.MaxwellStructureSocketType, "Structure"), + "bound": (types.tree_types.MaxwellBoundSocketType, "Bound"), + } + output_sockets = { + "fdtd_sim": (types.tree_types.MaxwellFDTDSimSocketType, "FDTD Sim") + } + + #################### + # - Socket Properties + #################### + @types.output_socket_cb("fdtd_sim") + def output_source(self): + return td.Simulation( + #center= ## Default: 0,0,0 + size=(1, 1, 1), ## PLACEHOLDER + run_time=self.compute_input("run_time"), + #structures=[], + #symmetry=, + sources=[self.compute_input("source")], + #boundary_spec=, ## Default: PML + monitors=[], + #grid_spec=, ## Default: Autogrid + #shutoff=, ## Default: 1e-05 + #subpixel=, ## Default: True + #normalize_index=, ## Default: 0 + #courant=, ## Default: 0.99 + ) + + +#################### +# - Blender Registration +#################### +BL_REGISTER = [ + FDTDMaxwellSimulationNode, +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/definitions/sources.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/definitions/sources.py new file mode 100644 index 0000000..4bd94a6 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/definitions/sources.py @@ -0,0 +1,63 @@ +import bpy +from .. import types, constants + +import tidy3d as td + +class PointDipoleMaxwellSourceNode(types.MaxwellSimTreeNode, bpy.types.Node): + bl_idname = types.PointDipoleMaxwellSourceNodeType + bl_label = "Point Dipole" + bl_icon = constants.tree_constants.ICON_SIM_SOURCE + + input_sockets = { + "center": ("NodeSocketVector", "Center"), + "interpolate": ("NodeSocketBool", "Interpolate"), + } + output_sockets = { + "source": (types.tree_types.MaxwellSourceSocketType, "Source") + } + + #################### + # - Properties + #################### + polarization: bpy.props.EnumProperty( + name="Polarization", + description="Polarization of the generated point dipole field", + items=[ + ("Ex", "Ex", "x-component of E-field"), + ("Ey", "Ey", "y-component of E-field"), + ("Ez", "Ez", "z-component of E-field"), + ("Hx", "Hx", "x-component of H-field"), + ("Hy", "Hy", "y-component of H-field"), + ("Hz", "Hz", "z-component of H-field"), + ], + default="Ex", + ) + + #################### + # - Node UI and Layout + #################### + def draw_buttons(self, context, layout): + layout.prop(self, 'polarization', text="") + + #################### + # - Socket Properties + #################### + @types.output_socket_cb("source") + def output_source(self): + return td.PointDipole( + center=tuple(self.compute_input("center")), + size=(0, 0, 0), + source_time=td.GaussianPulse(freq0=200e12, fwidth=200e12), + ## ^ Placeholder + interpolate=self.compute_input("interpolate"), + polarization=str(self.polarization), + ) + + + +#################### +# - Blender Registration +#################### +BL_REGISTER = [ + PointDipoleMaxwellSourceNode, +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/definitions/structures.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/definitions/structures.py new file mode 100644 index 0000000..2dcae8e --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/definitions/structures.py @@ -0,0 +1,25 @@ +import bpy +from .. import types, constants + +class TriMeshMaxwellStructureNode(types.MaxwellSimTreeNode, bpy.types.Node): + bl_idname = types.TriMeshMaxwellStructureNodeType + bl_label = "TriMesh" + bl_icon = constants.tree_constants.ICON_SIM_STRUCTURE + + # Initialization - Called on Node Creation + def init(self, context): + # Declare Node Inputs + self.inputs.new('NodeSocketObject', "Object") + self.inputs.new(types.tree_types.MaxwellMediumSocketType, "Medium") + + # Declare Node Outputs + self.outputs.new(types.tree_types.MaxwellStructureSocketType, "Structure") + + + +#################### +# - Blender Registration +#################### +BL_REGISTER = [ + TriMeshMaxwellStructureNode, +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/types.py b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/types.py new file mode 100644 index 0000000..058010b --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/types.py @@ -0,0 +1,125 @@ +import bpy +import numpy as np +from .. import types as tree_types + +#################### +# - Blender Types +#################### +DebugPrinterNodeType = 'DebugPrinterNodeType' + +PointDipoleMaxwellSourceNodeType = 'PointDipoleMaxwellSourceNodeType' + +SellmeierMaxwellMediumNodeType = 'SellmeierMaxwellMediumNodeType' + +TriMeshMaxwellStructureNodeType = 'TriMeshMaxwellStructureNodeType' + +PMLMaxwellBoundNodeType = 'PMLMaxwellBoundNodeType' + +FDTDMaxwellSimulationNodeType = 'FDTDMaxwellSimulationNodeType' + +#################### +# - Node Superclass +#################### +def output_socket_cb(name): + def decorator(func): + func._output_socket_name = name # Set a marker attribute + return func + return decorator + +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, +} +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" + ) + + #################### + # - 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]) + + #################### + # - 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 == tree_types.MaxwellSimTreeType diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/socket_types.py b/blender_maxwell/node_trees/maxwell_sim_nodes/socket_types.py new file mode 100644 index 0000000..bb52809 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/socket_types.py @@ -0,0 +1,71 @@ +import bpy + +from . import constants, types + +class MaxwellSourceSocket(bpy.types.NodeSocket): + bl_idname = types.MaxwellSourceSocketType + 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.MaxwellMediumSocketType + 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.MaxwellStructureSocketType + 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.MaxwellBoundSocketType + 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.MaxwellFDTDSimSocketType + 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_FDTDSIM + + + +#################### +# - Blender Registration +#################### +BL_REGISTER = [ + MaxwellSourceSocket, + MaxwellMediumSocket, + MaxwellStructureSocket, + MaxwellBoundSocket, +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/tree.py b/blender_maxwell/node_trees/maxwell_sim_nodes/tree.py new file mode 100644 index 0000000..1f52441 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/tree.py @@ -0,0 +1,17 @@ +import bpy + +from . import types, constants + +class MaxwellSimTree(bpy.types.NodeTree): + bl_idname = types.MaxwellSimTreeType + bl_label = "Maxwell Sim Editor" + bl_icon = constants.ICON_SIM ## Icon ID + + + +#################### +# - Blender Registration +#################### +BL_REGISTER = [ + MaxwellSimTree, +] diff --git a/blender_maxwell/node_trees/maxwell_sim_nodes/types.py b/blender_maxwell/node_trees/maxwell_sim_nodes/types.py new file mode 100644 index 0000000..66113f6 --- /dev/null +++ b/blender_maxwell/node_trees/maxwell_sim_nodes/types.py @@ -0,0 +1,12 @@ +import bpy + +#################### +# - Blender Types +#################### +MaxwellSimTreeType = 'MaxwellSimTreeType' + +MaxwellSourceSocketType = 'MaxwellSourceSocketType' +MaxwellMediumSocketType = 'MaxwellMediumSocketType' +MaxwellStructureSocketType = 'MaxwellStructureSocketType' +MaxwellBoundSocketType = 'MaxwellBoundSocketType' +MaxwellFDTDSimSocketType = 'MaxwellFDTDSimSocketType' diff --git a/blender_maxwell/node_trees/maxwell_viz_nodes/__init__.py b/blender_maxwell/node_trees/maxwell_viz_nodes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blender_maxwell/operators/__init__.py b/blender_maxwell/operators/__init__.py new file mode 100644 index 0000000..76a0da7 --- /dev/null +++ b/blender_maxwell/operators/__init__.py @@ -0,0 +1,7 @@ +from . import install_deps +from . import uninstall_deps + +BL_REGISTER = [ + *install_deps.BL_REGISTER, + *uninstall_deps.BL_REGISTER, +] diff --git a/blender_maxwell/operators/install_deps.py b/blender_maxwell/operators/install_deps.py new file mode 100644 index 0000000..47c913b --- /dev/null +++ b/blender_maxwell/operators/install_deps.py @@ -0,0 +1,60 @@ +import sys +import subprocess +from pathlib import Path + +import bpy + +from . import types + +class BlenderMaxwellInstallDependenciesOperator(bpy.types.Operator): + bl_idname = types.BlenderMaxwellInstallDependencies + bl_label = "Install Dependencies for Blender Maxwell Addon" + + def execute(self, context): + addon_dir = Path(__file__).parent.parent + requirements_path = addon_dir / 'requirements.txt' + #addon_specific_folder = addon_dir / '.dependencies' + addon_specific_folder = Path("/home/sofus/src/college/bsc_ge/thesis/code/.cached-dependencies") + + # Create the Addon-Specific Folder + addon_specific_folder.mkdir(parents=True, exist_ok=True) + + # Determine Path to Blender's Bundled Python + python_exec = Path(sys.executable) + ## bpy.app.binary_path_python was deprecated in 2.91. + ## sys.executable points to the correct bundled Python. + ## See + + # Install Dependencies w/Bundled pip + try: + subprocess.check_call([ + str(python_exec), '-m', + 'pip', 'install', + '-r', str(requirements_path), + '--target', str(addon_specific_folder), + ]) + self.report( + {'INFO'}, + "Dependencies for 'blender_maxwell' installed successfully." + ) + except subprocess.CalledProcessError as e: + self.report( + {'ERROR'}, + f"Failed to install dependencies: {str(e)}" + ) + return {'CANCELLED'} + + # Install Dependencies w/Bundled pip + if str(addon_specific_folder) not in sys.path: + sys.path.insert(0, str(addon_specific_folder)) + + return {'FINISHED'} + + + +#################### +# - Blender Registration +#################### +BL_REGISTER = [ + BlenderMaxwellInstallDependenciesOperator, +] diff --git a/blender_maxwell/operators/types.py b/blender_maxwell/operators/types.py new file mode 100644 index 0000000..1ad0607 --- /dev/null +++ b/blender_maxwell/operators/types.py @@ -0,0 +1,7 @@ +import bpy + +#################### +# - Blender Types +#################### +BlenderMaxwellInstallDependencies = "blender_maxwell.install_dependencies" +BlenderMaxwellUninstallDependencies = "blender_maxwell.uninstall_dependencies" diff --git a/blender_maxwell/operators/uninstall_deps.py b/blender_maxwell/operators/uninstall_deps.py new file mode 100644 index 0000000..5f8043b --- /dev/null +++ b/blender_maxwell/operators/uninstall_deps.py @@ -0,0 +1,30 @@ +import sys +import shutil +import subprocess +from pathlib import Path + +import bpy + +from . import types + +class BlenderMaxwellUninstallDependenciesOperator(bpy.types.Operator): + bl_idname = types.BlenderMaxwellUninstallDependencies + bl_label = "Uninstall Dependencies for Blender Maxwell Addon" + + def execute(self, context): + addon_dir = Path(__file__).parent.parent + #addon_specific_folder = addon_dir / '.dependencies' + addon_specific_folder = Path("/home/sofus/src/college/bsc_ge/thesis/code/.cached-dependencies") + + shutil.rmtree(addon_specific_folder) + + return {'FINISHED'} + + + +#################### +# - Blender Registration +#################### +BL_REGISTER = [ + BlenderMaxwellUninstallDependenciesOperator, +] diff --git a/blender_maxwell/preferences.py b/blender_maxwell/preferences.py new file mode 100644 index 0000000..d5647bb --- /dev/null +++ b/blender_maxwell/preferences.py @@ -0,0 +1,18 @@ +import bpy + +from .operators import types as operators_types + +class BlenderMaxwellAddonPreferences(bpy.types.AddonPreferences): + bl_idname = "blender_maxwell" + + def draw(self, context): + layout = self.layout + layout.operator(operators_types.BlenderMaxwellInstallDependencies, text="Install Dependencies") + layout.operator(operators_types.BlenderMaxwellUninstallDependencies, text="Uninstall Dependencies") + +#################### +# - Blender Registration +#################### +BL_REGISTER = [ + BlenderMaxwellAddonPreferences, +] diff --git a/blender_maxwell/requirements.txt b/blender_maxwell/requirements.txt new file mode 100644 index 0000000..e06b29a --- /dev/null +++ b/blender_maxwell/requirements.txt @@ -0,0 +1,2 @@ +tidy3d==2.5.2 +pydantic==2.6.0 diff --git a/blender_maxwell/utils/__init__.py b/blender_maxwell/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/run.py b/run.py new file mode 100644 index 0000000..7f69c58 --- /dev/null +++ b/run.py @@ -0,0 +1,108 @@ +import zipfile +import contextlib +import shutil +import sys +from pathlib import Path + +import bpy +import addon_utils + +PATH_ROOT = Path(__file__).resolve().parent + +#################### +# - Defined Constants +#################### +ADDON_NAME = "blender_maxwell" +PATH_BLEND = PATH_ROOT / "demo.blend" +PATH_ADDON_DEPS = PATH_ROOT / ".cached-dependencies" + +#################### +# - Computed Constants +#################### +PATH_ADDON = PATH_ROOT / ADDON_NAME +PATH_ADDON_ZIP = PATH_ROOT / (ADDON_NAME + ".zip") + +#################### +# - Utilities +#################### +@contextlib.contextmanager +def zipped_directory(path_dir: Path, path_zip: Path): + """Context manager that exposes a zipped version of a directory, + then deletes the .zip file afterwards. + """ + # Delete Existing ZIP file (if exists) + if path_zip.is_file(): path_zip.unlink() + + # Create a (new) ZIP file of the addon directory + with zipfile.ZipFile(path_zip, 'w', zipfile.ZIP_DEFLATED) as f_zip: + for file_to_zip in path_dir.rglob('*'): + f_zip.write(file_to_zip, file_to_zip.relative_to(path_dir.parent)) + + # Delete the ZIP + try: + yield path_zip + finally: + path_zip.unlink() + +#################### +# - main() +#################### +if __name__ == "__main__": + # Check and uninstall the addon if it's enabled + is_loaded_by_default, is_loaded_now = addon_utils.check(ADDON_NAME) + if is_loaded_now: + # Disable the Addon + addon_utils.disable(ADDON_NAME, default_set=True, handle_error=None) + + # Completey Delete the Addon + for mod in addon_utils.modules(): + if mod.__name__ == ADDON_NAME: + # Delete Addon from Blender Python Tree + shutil.rmtree(Path(mod.__file__).parent) + + # Reset All Addons + addon_utils.reset_all() + + # Save User Preferences & Break + bpy.ops.wm.save_userpref() + break + + # Quit Blender (hard-flush Python environment) + ## - Python environments are not made to be partially flushed. + ## - This is the only truly reliable way to avoid all bugs. + ## - See https://github.com/JacquesLucke/blender_vscode + bpy.ops.wm.quit_blender() + try: + raise RuntimeError + except: + sys.exit(42) + + with zipped_directory(PATH_ADDON, PATH_ADDON_ZIP) as path_zipped: + # Install the ZIPped Addon + bpy.ops.preferences.addon_install(filepath=str(path_zipped)) + + # Enable the Addon + addon_utils.enable( + ADDON_NAME, + default_set=True, + persistent=True, + handle_error=None, + ) + + # Save User Preferences + bpy.ops.wm.save_userpref() + + # Load the .blend + bpy.ops.wm.open_mainfile(filepath=str(PATH_BLEND)) + + # Ensure Addon-Specific Dependency Cache is Importable + ## - In distribution, the addon keeps this folder in the Blender script tree. + ## - For testing, we need to hack sys.path here. + ## - This avoids having to install all deps with every reload. + if str(PATH_ADDON_DEPS) not in sys.path: + sys.path.insert(0, str(PATH_ADDON_DEPS)) + + # Modify any specific settings, if needed + # Example: bpy.context.preferences.addons[addon_name].preferences.your_setting = "your_value" + + diff --git a/run.sh b/run.sh new file mode 100644 index 0000000..2167806 --- /dev/null +++ b/run.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +blender --python run.py +if [ $? -eq 42 ]; then + blender --python run.py +fi diff --git a/template.blend b/template.blend new file mode 100644 index 0000000..6397a7c --- /dev/null +++ b/template.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6475230422328c74d45fc9f4db25ca3d1d0ec49f6cc3f539a4ced68fd3a533fd +size 741696