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.
main
Sofus Albert Høgsbro Rose 2024-02-06 21:44:43 +01:00
commit 63270857ae
28 changed files with 938 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
from . import definitions, categories
BL_REGISTER = [
*definitions.BL_REGISTER,
]
BL_NODE_CATEGORIES = [
*categories.BL_NODE_CATEGORIES,
]

View File

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

View File

@ -0,0 +1 @@
from .. import constants as tree_constants

View File

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

View File

@ -0,0 +1 @@
BL_REGISTER = []

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,12 @@
import bpy
####################
# - Blender Types
####################
MaxwellSimTreeType = 'MaxwellSimTreeType'
MaxwellSourceSocketType = 'MaxwellSourceSocketType'
MaxwellMediumSocketType = 'MaxwellMediumSocketType'
MaxwellStructureSocketType = 'MaxwellStructureSocketType'
MaxwellBoundSocketType = 'MaxwellBoundSocketType'
MaxwellFDTDSimSocketType = 'MaxwellFDTDSimSocketType'

View File

@ -0,0 +1,7 @@
from . import install_deps
from . import uninstall_deps
BL_REGISTER = [
*install_deps.BL_REGISTER,
*uninstall_deps.BL_REGISTER,
]

View File

@ -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 <https://developer.blender.org/docs/release_notes/2.91/python_api/>
# 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,
]

View File

@ -0,0 +1,7 @@
import bpy
####################
# - Blender Types
####################
BlenderMaxwellInstallDependencies = "blender_maxwell.install_dependencies"
BlenderMaxwellUninstallDependencies = "blender_maxwell.uninstall_dependencies"

View File

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

View File

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

View File

@ -0,0 +1,2 @@
tidy3d==2.5.2
pydantic==2.6.0

View File

108
run.py 100644
View File

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

6
run.sh 100644
View File

@ -0,0 +1,6 @@
#!/bin/bash
blender --python run.py
if [ $? -eq 42 ]; then
blender --python run.py
fi