Compare commits
13 Commits
main
...
blender-pl
Author | SHA1 | Date |
---|---|---|
Sofus Albert Høgsbro Rose | 1e420376fc | |
Sofus Albert Høgsbro Rose | 134bf0c358 | |
Sofus Albert Høgsbro Rose | 1ebb57cff7 | |
Sofus Albert Høgsbro Rose | d95210dc34 | |
Sofus Albert Høgsbro Rose | 74d5a5daf8 | |
Sofus Albert Høgsbro Rose | 7344913c0e | |
Sofus Albert Høgsbro Rose | 586d6fa74b | |
Sofus Albert Høgsbro Rose | de8d64b5b3 | |
Sofus Albert Høgsbro Rose | 3793175011 | |
Sofus Albert Høgsbro Rose | 0bf6100e19 | |
Sofus Albert Høgsbro Rose | b78dd8dd56 | |
Sofus Albert Høgsbro Rose | b592ea4b10 | |
Sofus Albert Høgsbro Rose | a7fc66376b |
|
@ -2,6 +2,11 @@
|
|||
# - Standard Ignores
|
||||
####################
|
||||
dev
|
||||
.cached-dependencies
|
||||
__pycache__
|
||||
*.blend1
|
||||
*.blend2
|
||||
*.blend3
|
||||
|
||||
|
||||
####################
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
# Projects / Plugins
|
||||
## Larger Architectural Changes
|
||||
[ ] Dedicated way of generating properties for sockets and nodes, incl. helping make better (less boilerplatey) use of callbacks
|
||||
- Perhaps, we should also go 100% custom `PropertyGroup`, to the point that we have `nodes`, `sockets` and `props`.
|
||||
- This would allow far simplified sockets (the more complex kinds), and help standardize use of ex. units in node properties.
|
||||
- Having a dedicated base class for custom props would help avoid issues like forgetting to run `self.sync_prop` on every goddamn update method in every goddamn socket.
|
||||
[ ] Dedicated way of handling node-specific operators without all the boilerplate.
|
||||
|
||||
## Field Data
|
||||
[ ] Directly dealing with field data, instead of having field manipulations be baked into viz node(s).
|
||||
[ ] Yee Cell Data as Attributes on By-Cell Point Cloud w/GeoNodes Integrations
|
||||
- In effect, when we have xarray data defined based on Yee Cells ex. Poynting vector coordinates, let's import this to Blender as a simple point cloud centered at each cell and grant each an attribute corresponding to the data.
|
||||
- What we can then do is use vanilla GeoNodes to ex. read the vector attribute, and draw small arrow meshes (maybe resampled which auto-interpolates the field values) from each point, thus effectively visualizing . vector fields and many other fun things.
|
||||
- Of course, this is no good for volume cell data - but we can just overlay the raw volume cell data as we please. We can also, if we're sneaky, deal with our volume data as points as far as we can, and then finally do a "points to volume" type deal to make it sufficiently "fluffy/cloudy".
|
||||
- I wonder if we could use the Attribute node in the shader editor to project interpolated values from points, onto a ex. plane mesh, in a way that would also be visualizable in the viewport.
|
||||
|
||||
## Tidy3D Features
|
||||
[ ] Symmetry for Performance
|
||||
- [ ] Implement <https://docs.flexcompute.com/projects/tidy3d/en/latest/notebooks/Symmetry.html>
|
||||
[ ] Dispersive Model Fitting
|
||||
[ ] Scattering Matrix Calculator
|
||||
[ ] Resonance Finder
|
||||
[ ] Adjoint Optimization
|
||||
[ ] Design Space Exploration / Parameterization
|
||||
|
||||
## Preview Semantics
|
||||
[ ] Node tree header toggle that toggles a modal operator on and off, which constantly checks the context of the selected nodes, and tries to `bl_select` them (which in turn, should cause the node base class to `bl_select` shit inside).
|
||||
- Shouldn't survive a file save; always startup with this thing off.
|
||||
[ ] Custom gizmos attached to preview toggles!
|
||||
- There is a WIP for GN-driven gizmos: <https://projects.blender.org/blender/blender/pulls/112677>
|
||||
- Probably best to wait for that, then just add gizmos to existing driven GN trees, as opposed to unholy OGL spaghetti.
|
||||
[ ] Node-ManagedObj Selection binding
|
||||
- BL to Node:
|
||||
- Trigger: The post-depsgraph handler seems appropriate.
|
||||
- Input: Read the object location (origin), using a unit system.
|
||||
- Output: Write the input socket value.
|
||||
- Condition: Input socket is unlinked. (If it's linked, then lock the object's position. Use sync_link_added() for that)
|
||||
- Node to BL:
|
||||
- Trigger: "Report" action on an input socket that the managed object declares reliance on.
|
||||
- Input: The input socket value (linked or unlinked)
|
||||
- Output: The object location (origin), using a unit system.
|
||||
|
||||
## Parametric Geometry UX
|
||||
[ ] Consider allowing a mesh attribute (set in ex. geometry node) to specify the name of a medium.
|
||||
- This allows assembling complex multi-medium structures in one geonodes tree.
|
||||
- This should result in the spawning of several Medium input sockets in the GeoNodes structure node, named as the attributes are.
|
||||
- The GeoNodes structure node should then output as array-like TriMeshes, for which mediums are correctly defined.
|
||||
|
||||
## Alternative Engines
|
||||
[ ] Heat Solver
|
||||
[ ] MEEP integration (<https://meep.readthedocs.io/en/latest/>)
|
||||
- The main boost would be if we could setup a MEEP simulation entirely from a td.Simulation object.
|
|
@ -0,0 +1,399 @@
|
|||
# Nodes
|
||||
**LEGEND**:
|
||||
- [-] Exists but doesn't quite work good enough.
|
||||
- [x] Done to working degree (the standard is "good enough for the demo").
|
||||
- See check marks underneath
|
||||
- [?] Unsure whether we should do this.
|
||||
|
||||
## Inputs
|
||||
[x] Wave Constant
|
||||
- [ ] Implement export of frequency / wavelength array/range.
|
||||
[-] Unit System
|
||||
- [ ] Implement presets, including "Tidy3D" and "Blender", shown in the label row.
|
||||
|
||||
[ ] Constants / Scientific Constant
|
||||
[x] Constants / Number Constant
|
||||
[ ] Constants / Physical Constant
|
||||
- [ ] Pol: Elliptical plot viz
|
||||
- [ ] Pol: Poincare sphere viz
|
||||
[x] Constants / Blender Constant
|
||||
|
||||
[x] Web / Tidy3D Web Importer
|
||||
|
||||
[ ] File Import / JSON File Import
|
||||
- [ ] Dropdown to choose various supported JSON-sourced objects incl.
|
||||
[ ] File Import / Tidy3D File Import
|
||||
- [ ] Implement HDF-based import of Tidy3D-exported object (which includes ex. mesh data and such)
|
||||
[ ] File Import / Array File Import
|
||||
- [ ] Standardize 1D and 2D array loading/saving on numpy's savetxt with gzip enabled.
|
||||
- [ ] Implement datatype dropdown to guide format from disk, prefilled to detected.
|
||||
- [ ] Implement unit system input to guide conversion from numpy data type.
|
||||
- [ ] Implement a LazyValue to provide a data path that avoids having to load massive arrays every time always.
|
||||
|
||||
## Outputs
|
||||
[x] Viewer
|
||||
- [ ] **BIG ONE**: Remove image preview when disabling plots.
|
||||
- [ ] Either enforce singleton, or find a way to have several viewers at the same time.
|
||||
- [ ] A setting that live-previews just a value.
|
||||
- [ ] Pop-up multiline string print as alternative to console print.
|
||||
- [x] Toggleable auto-plot, auto-3D-preview, auto-value-view, (?)auto-text-view.
|
||||
|
||||
[x] Web Export / Tidy3D Web Exporter
|
||||
- [ ] We need better ways of doing checks before uploading, like for monitor data size. Maybe a SimInfo node?
|
||||
- [ ] We need to be able to "delete and re-upload" (or maybe just delete from the interface).
|
||||
|
||||
[x] File Export / JSON File Export
|
||||
[ ] File Import / Tidy3D File Export
|
||||
- [ ] Implement HDF-based export of Tidy3D-exported object (which includes ex. mesh data and such)
|
||||
[ ] File Export / Array File Export
|
||||
- [ ] Implement datatype dropdown to guide format on disk.
|
||||
- [ ] Implement unit system input to guide conversion to numpy data type.
|
||||
- [ ] Standardize 1D and 2D array loading/saving on numpy's savetxt with gzip enabled.
|
||||
|
||||
## Viz
|
||||
[ ] Sim Info
|
||||
- [ ] Implement estimation of monitor storage
|
||||
- [ ] Implement cost estimation
|
||||
[ ] Monitor Data Viz
|
||||
- [ ] Implement dropdown to choose which monitor in the SimulationData should be visualized (based on which are available in the SimulationData), and implement visualization based on every kind of monitor-adjascent output data type (<https://docs.flexcompute.com/projects/tidy3d/en/latest/api/output_data.html>)
|
||||
- [ ] Project field values onto a plane object (managed)
|
||||
|
||||
## Sources
|
||||
[x] Temporal Shapes / Gaussian Pulse Temporal Shape
|
||||
[x] Temporal Shapes / Continuous Wave Temporal Shape
|
||||
[ ] Temporal Shapes / Symbolic Temporal Shape
|
||||
- [ ] Specify a Sympy function to generate appropriate array based on
|
||||
[ ] Temporal Shapes / Array Temporal Shape
|
||||
|
||||
[x] Point Dipole Source
|
||||
- [ ] Consider a "real" mesh - the empty kind of gets stuck inside of the sim domain.
|
||||
[-] Plane Wave Source
|
||||
- [ ] **IMPORTANT**: Fix the math so that an actually valid construction emerges!!
|
||||
- [x] Implement an oriented vector input with 3D preview.
|
||||
[ ] Uniform Current Source
|
||||
[ ] TFSF Source
|
||||
|
||||
[ ] Gaussian Beam Source
|
||||
[ ] Astigmatic Gaussian Beam Source
|
||||
|
||||
[ ] Mode Source
|
||||
|
||||
[ ] Array Source / EH Array Source
|
||||
[ ] Array Source / EH Equivilance Array Source
|
||||
|
||||
## Mediums
|
||||
[x] Library Medium
|
||||
- [ ] Implement frequency range output
|
||||
[ ] PEC Medium
|
||||
[ ] Isotropic Medium
|
||||
[ ] Anisotropic Medium
|
||||
|
||||
[ ] Sellmeier Medium
|
||||
[ ] Drude Medium
|
||||
[ ] Drude-Lorentz Medium
|
||||
[ ] Debye Medium
|
||||
[ ] Pole-Residue Medium
|
||||
|
||||
[ ] Non-Linearity / `chi_3` Susceptibility Non-Linearity
|
||||
[ ] Non-Linearity / Two-Photon Absorption Non-Linearity
|
||||
[ ] Non-Linearity / Kerr Non-Linearity
|
||||
|
||||
[ ] Space/Time epsilon/mu Modulation
|
||||
|
||||
## Structures
|
||||
[ ] BLObject Structure
|
||||
[x] GeoNodes Structure
|
||||
- [x] Rewrite the `bl_socket_map.py`
|
||||
- [x] Use the modifier itself as memory, via the ManagedObj
|
||||
- [?] When GeoNodes themselves declare panels, implement a grid-like tab system to select which sockets should be exposed in the node at a given point in time.
|
||||
|
||||
[ ] Primitive Structures / Plane
|
||||
[ ] Primitive Structures / Box Structure
|
||||
[ ] Primitive Structures / Sphere
|
||||
[ ] Primitive Structures / Cylinder
|
||||
[ ] Primitive Structures / Ring
|
||||
[ ] Primitive Structures / Capsule
|
||||
[ ] Primitive Structures / Cone
|
||||
|
||||
## Monitors
|
||||
- **ALL**: "Steady-State" / "Time Domain" (only if relevant).
|
||||
|
||||
[ ] E/H Field Monitor
|
||||
- [ ] Monitor Domain as dropdown with Frequency or Time
|
||||
- [ ] Axis-aligned planar 2D (pixel) and coord-aligned box 3D (voxel).
|
||||
[ ] Field Power Flux Monitor
|
||||
- [ ] Monitor Domain as dropdown with Frequency or Time
|
||||
- [ ] Axis-aligned planar 2D (pixel) and coord-aligned box 3D (voxel).
|
||||
[ ] \epsilon Tensor Monitor
|
||||
- [ ] Axis-aligned planar 2D (pixel) and coord-aligned box 3D (voxel).
|
||||
[ ] Diffraction Monitor
|
||||
- [ ] Axis-aligned planar 2D (pixel)
|
||||
|
||||
[ ] Projected E/H Field Monitor / Cartesian Projected E/H Field Monitor
|
||||
- [ ] Use to implement the metalens: <https://docs.flexcompute.com/projects/tidy3d/en/latest/notebooks/Metalens.html>
|
||||
[ ] Projected E/H Field Monitor / Angle Projected E/H Field Monitor
|
||||
[ ] Projected E/H Field Monitor / K-Space Projected E/H Field Monitor
|
||||
|
||||
- **TODO**: "Modal" solver monitoring (seems to be some kind of spatial+frequency feature, which an EM field can be decomposed into using a specially configured solver, which can be used to look for very particular kinds of effects by constraining investigations of a solver result to filter out everything that isn't these particular modes aka. features. Kind of a fourier-based redimensionalization, almost).
|
||||
|
||||
## Simulations
|
||||
[-] FDTDSim
|
||||
|
||||
[x] Sim Domain
|
||||
- [ ] By-Medium batching of Structures when building the td.Simulation object, which can have significant performance implications.
|
||||
|
||||
[x] Boundary Conds
|
||||
- [x] Rename from Bounds / BoundBox
|
||||
[ ] Boundary Cond / PML Bound Face
|
||||
- [ ] Implement dropdown for "Normal" and "Stable"
|
||||
[ ] Boundary Cond / PEC Bound Face
|
||||
[ ] Boundary Cond / PMC Bound Face
|
||||
[ ] Boundary Cond / Bloch Bound Face
|
||||
[ ] Boundary Cond / Periodic Bound Face
|
||||
[ ] Boundary Cond / Absorbing Bound Face
|
||||
|
||||
[ ] Sim Grid
|
||||
[ ] Sim Grid Axes / Auto Sim Grid Axis
|
||||
[ ] Sim Grid Axes / Manual Sim Grid Axis
|
||||
[ ] Sim Grid Axes / Uniform Sim Grid Axis
|
||||
[ ] Sim Grid Axes / Array Sim Grid Axis
|
||||
|
||||
## Converters
|
||||
[ ] Math
|
||||
- [ ] Implement common operations w/secondary choice of socket type based on a custom internal data structure
|
||||
- [ ] Implement angfreq/frequency/vacwl conversion.
|
||||
[ ] Separate
|
||||
[ ] Combine
|
||||
- [ ] Implement concatenation of sim-critical socket types into their multi-type
|
||||
|
||||
|
||||
|
||||
# GeoNodes
|
||||
[ ] Tests / Monkey (suzanne deserves to be simulated, she may need manifolding up though :))
|
||||
[ ] Tests / Wood Pile
|
||||
|
||||
[ ] Primitives / Plane
|
||||
[ ] Primitives / Box
|
||||
[ ] Primitives / Sphere
|
||||
[ ] Primitives / Cylinder
|
||||
[ ] Primitives / Ring
|
||||
[ ] Primitives / Capsule
|
||||
[ ] Primitives / Cone
|
||||
|
||||
[ ] Array / Square Array **NOTE: Ring and cylinder**
|
||||
[ ] Array / Hex Array **NOTE: Ring and cylinder**
|
||||
[ ] Hole Array / Square Hole Array: Takes a primitive hole shape.
|
||||
[ ] Hole Array / Hex Hole Array: Takes a primitive hole shape.
|
||||
[ ] Cavity Array / Hex Array w/ L-Cavity
|
||||
[ ] Cavity Array / Hex Array w/ H-Cavity
|
||||
|
||||
[ ] Crystal Sphere Lattice / Sphere FCC Array
|
||||
[ ] Crystal Sphere Lattice / Sphere BCC Array
|
||||
|
||||
|
||||
|
||||
# Benchmark / Example Sims
|
||||
[ ] Research-Grade Experiment
|
||||
- Membrane 15nm thickness suspended in air
|
||||
- Square lattice of holes period 900nm (900nm between each hole, air inside holes)
|
||||
- Holes square radius 100nm
|
||||
- Square lattice
|
||||
- Analysis of transmission
|
||||
- Guided mode resonance
|
||||
[ ] Tunable Chiral Metasurface <https://docs.flexcompute.com/projects/tidy3d/en/latest/notebooks/TunableChiralMetasurface.html>
|
||||
|
||||
|
||||
|
||||
# Sockets
|
||||
## Basic
|
||||
[x] Any
|
||||
[x] Bool
|
||||
[x] String
|
||||
- [ ] Rename from "Text"
|
||||
[x] File Path
|
||||
[x] Color
|
||||
|
||||
## Number
|
||||
[x] Integer
|
||||
[x] Rational
|
||||
- [ ] Implement constrained SympyExpr check for Rational.
|
||||
[x] Real
|
||||
- [ ] Implement min/max for ex. 0..1 factor support.
|
||||
- [ ] Implement constrained SympyExpr check for Rational.
|
||||
[x] Complex
|
||||
|
||||
## Blender
|
||||
[x] Object
|
||||
- [ ] Implement default SocketDef object name
|
||||
[x] Collection
|
||||
- [ ] Implement default SocketDef collection name
|
||||
|
||||
[x] Image
|
||||
- [ ] Implement default SocketDef image name
|
||||
|
||||
[x] GeoNodes
|
||||
- [ ] Implement default SocketDef geonodes name
|
||||
[x] Text
|
||||
- [ ] Implement default SocketDef object name
|
||||
|
||||
## Maxwell
|
||||
[x] Bound Conds
|
||||
[ ] Bound Cond
|
||||
|
||||
[x] Medium
|
||||
[ ] Medium Non-Linearity
|
||||
|
||||
[x] Source
|
||||
[ ] Temporal Shape
|
||||
- [ ] Sane-default pulses for easy access.
|
||||
|
||||
[ ] Structure
|
||||
[ ] Monitor
|
||||
|
||||
[ ] FDTD Sim
|
||||
[ ] Sim Domain
|
||||
- [?] Toggleable option to sync the simulation time duration to the scene end time (how to handle FPS vs time-step? Should we adjust the FPS such that there is one time step per frame, while keeping the definition of "second" aligned to a unit system?)
|
||||
[ ] Sim Grid
|
||||
[ ] Sim Grid Axis
|
||||
|
||||
[ ] Simulation Data
|
||||
|
||||
## Tidy3D
|
||||
[x] Cloud Task
|
||||
- [ ] Implement switcher for API-key-having config filconfig file vs. direct entry of API key. It should be auto-filled with the config file when such a thing exists.
|
||||
|
||||
## Physical
|
||||
[x] Unit System
|
||||
- [ ] Implement more comprehensible UI; honestly, probably with the new panels (<https://developer.blender.org/docs/release_notes/4.1/python_api/>)
|
||||
|
||||
[x] Time
|
||||
|
||||
[x] Angle
|
||||
[ ] Solid Angle (steradian)
|
||||
|
||||
[x] Frequency (hertz)
|
||||
[ ] Angular Frequency (`rad*hertz`)
|
||||
### Cartesian
|
||||
[x] Length
|
||||
[x] Area
|
||||
[x] Volume
|
||||
|
||||
[ ] Point 1D
|
||||
[ ] Point 2D
|
||||
[x] Point 3D
|
||||
|
||||
[ ] Size 2D
|
||||
[x] Size 3D
|
||||
|
||||
[ ] Rotation 3D
|
||||
- [ ] Implement Euler methods
|
||||
- [ ] Implement Quaternion methods
|
||||
### Mechanical
|
||||
[ ] Mass
|
||||
|
||||
[x] Speed
|
||||
[ ] Velocity 3D
|
||||
[x] Acceleration Scalar
|
||||
[ ] Acceleration 3D
|
||||
[x] Force Scalar
|
||||
[ ] Force 3D
|
||||
[ ] Pressure
|
||||
### Energy
|
||||
[ ] Energy (joule)
|
||||
[ ] Power (watt)
|
||||
[ ] Temperature
|
||||
### Electrodynamical
|
||||
[ ] Current (ampere)
|
||||
[ ] Current Density 3D
|
||||
|
||||
[ ] Charge (coulomb)
|
||||
[ ] Voltage (volts)
|
||||
[ ] Capacitance (farad)
|
||||
[ ] Resistance (ohm)
|
||||
[ ] Electric Conductance (siemens)
|
||||
|
||||
[ ] Magnetic Flux (weber)
|
||||
[ ] Magnetic Flux Density (tesla)
|
||||
[ ] Inductance (henry)
|
||||
|
||||
[ ] Electric Field 3D (`volt*meter`)
|
||||
[ ] Magnetic Field 3D (tesla)
|
||||
### Luminal
|
||||
[ ] Luminous Intensity (candela)
|
||||
[ ] Luminous Flux (lumen)
|
||||
[ ] Illuminance (lux)
|
||||
### Optical
|
||||
[ ] Jones Polarization
|
||||
[ ] Polarization (Stokes)
|
||||
|
||||
|
||||
|
||||
# Style
|
||||
[ ] Rethink the meaning of color and shapes in node sockets, including whether dynamic functionality is needed when it comes to socket shape (ex. it might be nice to know whether a socket is array-like or uses units).
|
||||
[ ] Rethink the meaning of color and shapes in node sockets, including whether dynamic functionality is needed when it comes to socket shape.
|
||||
|
||||
|
||||
|
||||
# Architecture
|
||||
## Registration and Contracts
|
||||
[x] Finish the contract code converting from Blender sockets to our sockets based on dimensionality and the property description.
|
||||
[ ] Refactor the node category code; it's ugly.
|
||||
[?] Would be nice with some kind of indicator somewhere to help set good socket descriptions when using geonodes and wanting units.
|
||||
|
||||
## Managed Objects
|
||||
[x] Implement modifier support on the managed BL object, with special attention paid to the needs of the GeoNodes socket.
|
||||
- [x] Implement preview toggling too, ex. using the relevant node tree collections
|
||||
- Remember, the managed object is "dumb". It's the node's responsibility to react to any relevant `on_value_change`, and forward all state needed by the modifier to the managed obj. It's only the managed obj's responsibility to not update any modifier value that wouldn't change anything.
|
||||
[ ] Implement loading the xarray-defined voxels into OpenVDB, saving it, and loading it as a managed BL object with the volume setting.
|
||||
[ ] Implement basic jax-driven volume voxel processing, especially cube based slicing.
|
||||
[ ] Implement jax-driven linear interpolation of volume voxels to an image texture, whose pixels are sized according to the dimensions of another managed plane object (perhaps a uniquely described Managed BL object itself).
|
||||
|
||||
## Utils or Services
|
||||
[ ] Dedicated module for managing the interaction with the tidy3d cloud, to help nuke all the random caches out of existance.
|
||||
|
||||
## Node Base Class
|
||||
[ ] Dedicated `draw_preview`-type draw functions for plot customizations.
|
||||
- [ ] For now, previewing isn't something I think should be part of the node
|
||||
[ ] Custom `@cache`/`@lru_cache`/`@cached_property` which caches by instance ID (possibly based on `beartype` or `pydantic`).
|
||||
[ ] When presets are used, if a preset is selected and the user alters a preset setting, then dynamically switch the preset indicator back to "Custom" to indicate that there is no active preset
|
||||
[ ] It seems that `node.inputs` and `node.outputs` allows the use of a `move` method, which may allow reordering sockets dynamically, which we should expose to the user as user-configurable ordering rules (maybe resolved with a constraint solver).
|
||||
[?] Mechanism for dynamic names (ex. "Library Medium" becoming "Au Medium")
|
||||
[-] Mechanism for selecting a blender object managed by a particular node.
|
||||
[ ] Mechanism for ex. specially coloring a node that is currently participating in the preview.
|
||||
[ ] Custom callbacks when deleting a node (in `free()`), to ex. delete all previews with the viewer node.
|
||||
|
||||
## Socket Base Class
|
||||
[ ] A feature `use_array` which allows a socket to declare that it can be both a single value and array-like (possibly constrained to a given shape). This should also allow the SocketDef to request that the input socket be initialised as a multi-input socket, once Blender updates to support those.
|
||||
- [ ] Implement a shape-selector, with a dropdown for dimensionality and an appropriate `IntegerVectorProperty` for each kind of shape (supporting also straight-up inf), which is declared to the node that supports array-likeness so it can decide how exactly to expose properties in the array-like context of things.
|
||||
[ ] Make `to_socket`s no-consent to new links from `from_socket`s of differing type (we'll see if this controls the typing story enough for now, and how much we'll need capabilities in the long run)
|
||||
- [?] Alternatively, reject non matching link types, and red-mark non matching capabilities?
|
||||
|
||||
## Many Nodes
|
||||
[ ] Implement LazyValue stuff, including LazyParamValue on a new class of constant-like input nodes that really just emit ex. sympy variables.
|
||||
[?] Require a Unit System for nodes that construct Tidy3D objects
|
||||
[ ] Medium Features
|
||||
- [ ] Accept spatial field. Else, spatial uniformity.
|
||||
- [ ] Accept non-linearity. Else, linear.
|
||||
- [ ] Accept space-time modulation. Else, static.
|
||||
[ ] Modal Features
|
||||
- [ ] ModeSpec, for use by ModeSource, ModeMonitor, ModeSolverMonitor. Data includes ModeSolverData, ModeData, ScalarModeFieldDataArray, ModeAmpsDataArray, ModeIndexDataArray, ModeSolver.
|
||||
|
||||
## Development Tooling
|
||||
[ ] Implement `rye` support
|
||||
[ ] Setup neovim to be an ideal editor
|
||||
|
||||
## Version Churn
|
||||
[ ] Implement real StrEnum sockets, since they appear in py3.11
|
||||
[ ] Think about implementing new panels where appropriate (<https://developer.blender.org/docs/release_notes/4.1/python_api/>)
|
||||
[ ] Think about using the new bl4.1 file handler API to enable drag and drop creation of appropriate nodes (for importing files without hassle).
|
||||
[ ] Keep an eye on our manual `__annotations__` hacking; python 3.13 is apparently fucking with it.
|
||||
[ ] Plan for multi-input sockets <https://projects.blender.org/blender/blender/commit/14106150797a6ce35e006ffde18e78ea7ae67598> (for now, just use the "Combine" node and have seperate socket types for both).
|
||||
[ ] Keep an eye out for volume geonodes in 4.2 (July 16, 2024), which will better allow for more complicated volume processing (we might still want/need the jax based stuff after, but let's keep it minimal just in case)
|
||||
|
||||
## Packaging
|
||||
[ ] Allow specifying custom dir for keeping pip dependencies, so we can unify prod and dev (currently we hard-code a dev dependency path).
|
||||
[ ] Refactor top-level `__init__.py` to check dependencies first. If not everything is available, it should only register a minimal addon; specifically, a message telling the user that the addon requires additional dependencies (list which), and the button to install them. When the installation is done, re-check deps and register the rest of the addon.
|
||||
[ ] Use a Modal and multiline-text-like construction to print `pip install` as we install dependencies, so that the user has an idea that something is happening.
|
||||
[ ] Test on Windows
|
||||
|
||||
## Node Tree Cache Semantics
|
|
@ -0,0 +1,84 @@
|
|||
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_KMI_REGISTER = [
|
||||
*operators.BL_KMI_REGISTER,
|
||||
]
|
||||
BL_NODE_CATEGORIES = [
|
||||
*node_trees.BL_NODE_CATEGORIES,
|
||||
]
|
||||
|
||||
km = bpy.context.window_manager.keyconfigs.addon.keymaps.new(
|
||||
name='Node Editor',
|
||||
space_type="NODE_EDITOR",
|
||||
)
|
||||
REGISTERED_KEYMAPS = []
|
||||
def register():
|
||||
global REGISTERED_KEYMAPS
|
||||
|
||||
for cls in BL_REGISTER:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
for kmi_def in BL_KMI_REGISTER:
|
||||
kmi = km.keymap_items.new(
|
||||
*kmi_def["_"],
|
||||
ctrl=kmi_def["ctrl"],
|
||||
shift=kmi_def["shift"],
|
||||
alt=kmi_def["alt"],
|
||||
)
|
||||
REGISTERED_KEYMAPS.append(kmi)
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(BL_REGISTER):
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
for kmi in REGISTERED_KEYMAPS:
|
||||
km.keymap_items.remove(kmi)
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
|
@ -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,
|
||||
]
|
|
@ -0,0 +1,21 @@
|
|||
import sympy as sp
|
||||
sp.printing.str.StrPrinter._default_settings['abbrev'] = True
|
||||
## In this tree, all Sympy unit printing must be abbreviated.
|
||||
## By configuring this in __init__.py, we guarantee it for all subimports.
|
||||
## (Unless, elsewhere, this setting is changed. Be careful!)
|
||||
|
||||
from . import sockets
|
||||
from . import node_tree
|
||||
from . import nodes
|
||||
from . import categories
|
||||
|
||||
BL_REGISTER = [
|
||||
*sockets.BL_REGISTER,
|
||||
*node_tree.BL_REGISTER,
|
||||
*nodes.BL_REGISTER,
|
||||
*categories.BL_REGISTER,
|
||||
]
|
||||
|
||||
BL_NODE_CATEGORIES = [
|
||||
*categories.BL_NODE_CATEGORIES,
|
||||
]
|
|
@ -0,0 +1,251 @@
|
|||
import typing as typ
|
||||
import typing_extensions as typx
|
||||
|
||||
import pydantic as pyd
|
||||
import sympy as sp
|
||||
import sympy.physics.units as spu
|
||||
|
||||
import bpy
|
||||
|
||||
from ...utils import extra_sympy_units as spuex
|
||||
from . import contracts as ct
|
||||
from .contracts import SocketType as ST
|
||||
from . import sockets as sck
|
||||
|
||||
# TODO: Caching?
|
||||
# TODO: Move the manual labor stuff to contracts
|
||||
|
||||
BLSocketType = str ## A Blender-Defined Socket Type
|
||||
BLSocketSize = int
|
||||
DescType = str
|
||||
Unit = typ.Any ## Type of a valid unit
|
||||
|
||||
####################
|
||||
# - Socket to SocketDef
|
||||
####################
|
||||
SOCKET_DEFS = {
|
||||
socket_type: getattr(
|
||||
sck,
|
||||
socket_type.value.removesuffix("SocketType") + "SocketDef",
|
||||
)
|
||||
for socket_type in ST
|
||||
if hasattr(
|
||||
sck,
|
||||
socket_type.value.removesuffix("SocketType") + "SocketDef"
|
||||
)
|
||||
}
|
||||
## TODO: Bit of a hack. Is it robust enough?
|
||||
|
||||
for socket_type in ST:
|
||||
if not hasattr(
|
||||
sck,
|
||||
socket_type.value.removesuffix("SocketType") + "SocketDef",
|
||||
):
|
||||
print("Missing SocketDef for", socket_type.value)
|
||||
|
||||
|
||||
####################
|
||||
# - BL Socket Size Parser
|
||||
####################
|
||||
BL_SOCKET_3D_TYPE_PREFIXES = {
|
||||
"NodeSocketVector",
|
||||
"NodeSocketRotation",
|
||||
}
|
||||
BL_SOCKET_4D_TYPE_PREFIXES = {
|
||||
"NodeSocketColor",
|
||||
}
|
||||
def size_from_bl_interface_socket(
|
||||
bl_interface_socket: bpy.types.NodeTreeInterfaceSocket
|
||||
) -> typx.Literal[1, 2, 3, 4]:
|
||||
"""Parses the `size`, aka. number of elements, contained within the `default_value` of a Blender interface socket.
|
||||
|
||||
Since there are no 2D sockets in Blender, the user can specify "2D" in the Blender socket's description to "promise" that only the first two values will be used.
|
||||
When this is done, the third value is left entirely untouched by this entire system.
|
||||
|
||||
A hard-coded set of NodeSocket<Type> prefixes are used to determine which interface sockets are, in fact, 3D.
|
||||
- For 3D sockets, a hard-coded list of Blender node socket types is used.
|
||||
- Else, it is a 1D socket type.
|
||||
"""
|
||||
if bl_interface_socket.description.startswith("2D"): return 2
|
||||
if any(
|
||||
bl_interface_socket.socket_type.startswith(bl_socket_3d_type_prefix)
|
||||
for bl_socket_3d_type_prefix in BL_SOCKET_3D_TYPE_PREFIXES
|
||||
):
|
||||
return 3
|
||||
if any(
|
||||
bl_interface_socket.socket_type.startswith(bl_socket_4d_type_prefix)
|
||||
for bl_socket_4d_type_prefix in BL_SOCKET_4D_TYPE_PREFIXES
|
||||
):
|
||||
return 4
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
####################
|
||||
# - BL Socket Type / Unit Parser
|
||||
####################
|
||||
def parse_bl_interface_socket(
|
||||
bl_interface_socket: bpy.types.NodeTreeInterfaceSocket,
|
||||
) -> tuple[ST, sp.Expr | None]:
|
||||
"""Parse a Blender interface socket by parsing its description, falling back to any direct type links.
|
||||
|
||||
Arguments:
|
||||
bl_interface_socket: An interface socket associated with the global input to a node tree.
|
||||
|
||||
Returns:
|
||||
The type of a corresponding MaxwellSimSocket, as well as a unit (if a particular unit was requested by the Blender interface socket).
|
||||
"""
|
||||
size = size_from_bl_interface_socket(bl_interface_socket)
|
||||
|
||||
# Determine Direct Socket Type
|
||||
if (
|
||||
direct_socket_type := ct.BL_SOCKET_DIRECT_TYPE_MAP.get(
|
||||
(bl_interface_socket.socket_type, size)
|
||||
)
|
||||
) is None:
|
||||
msg = "Blender interface socket has no mapping among 'MaxwellSimSocket's."
|
||||
raise ValueError(msg)
|
||||
|
||||
# (Maybe) Return Direct Socket Type
|
||||
## When there's no description, that's it; return.
|
||||
if not ct.BL_SOCKET_DESCR_ANNOT_STRING in bl_interface_socket.description:
|
||||
return (direct_socket_type, None)
|
||||
|
||||
# Parse Description for Socket Type
|
||||
tokens = (
|
||||
_tokens
|
||||
if (_tokens := bl_interface_socket.description.split(" "))[0] != "2D"
|
||||
else _tokens[1:]
|
||||
) ## Don't include the "2D" token, if defined.
|
||||
if (
|
||||
socket_type := ct.BL_SOCKET_DESCR_TYPE_MAP.get(
|
||||
(tokens[0], bl_interface_socket.socket_type, size)
|
||||
)
|
||||
) is None:
|
||||
return (direct_socket_type, None) ## Description doesn't map to anything
|
||||
|
||||
# Determine Socket Unit (to use instead of "unit system")
|
||||
## This is entirely OPTIONAL
|
||||
socket_unit = None
|
||||
if socket_type in ct.SOCKET_UNITS:
|
||||
## Case: Unit is User-Defined
|
||||
if len(tokens) > 1 and "(" in tokens[1] and ")" in tokens[1]:
|
||||
# Compute (<unit_str>) as Unit Token
|
||||
unit_token = tokens[1].removeprefix("(").removesuffix(")")
|
||||
|
||||
# Compare Unit Token to Valid Sympy-Printed Units
|
||||
socket_unit = _socket_unit if (_socket_unit := [
|
||||
unit
|
||||
for unit in ct.SOCKET_UNITS[socket_type]["values"].values()
|
||||
if str(unit) == unit_token
|
||||
]) else ct.SOCKET_UNITS[socket_type]["values"][
|
||||
ct.SOCKET_UNITS[socket_type]["default"]
|
||||
]
|
||||
## TODO: Enforce abbreviated sympy printing here, not globally
|
||||
|
||||
return (socket_type, socket_unit)
|
||||
|
||||
|
||||
####################
|
||||
# - BL Socket Interface Definition
|
||||
####################
|
||||
def socket_def_from_bl_interface_socket(
|
||||
bl_interface_socket: bpy.types.NodeTreeInterfaceSocket,
|
||||
):
|
||||
"""Computes an appropriate (no-arg) SocketDef from the given `bl_interface_socket`, by parsing it.
|
||||
"""
|
||||
return SOCKET_DEFS[
|
||||
parse_bl_interface_socket(bl_interface_socket)[0]
|
||||
]
|
||||
|
||||
|
||||
####################
|
||||
# - Extract Default Interface Socket Value
|
||||
####################
|
||||
def value_from_bl(
|
||||
bl_interface_socket: bpy.types.NodeTreeInterfaceSocket,
|
||||
unit_system: dict | None = None,
|
||||
) -> typ.Any:
|
||||
"""Reads the value of any Blender socket, and writes its `default_value` to the `value` of any `MaxwellSimSocket`.
|
||||
- If the size of the Blender socket is >1, then `value` is written to as a `sympy.Matrix`.
|
||||
- If a unit system is given, then the Blender socket is matched to a `MaxwellSimSocket`, which is used to lookup an appropriate unit in the given `unit_system`.
|
||||
|
||||
"""
|
||||
## TODO: Consider sympy.S()'ing the default_value
|
||||
parsed_bl_socket_value = {
|
||||
1: lambda: bl_interface_socket.default_value,
|
||||
2: lambda: sp.Matrix(tuple(bl_interface_socket.default_value)[:2]),
|
||||
3: lambda: sp.Matrix(tuple(bl_interface_socket.default_value)),
|
||||
4: lambda: sp.Matrix(tuple(bl_interface_socket.default_value)),
|
||||
}[size_from_bl_interface_socket(bl_interface_socket)]()
|
||||
## The 'lambda' delays construction until size is determined
|
||||
|
||||
socket_type, unit = parse_bl_interface_socket(bl_interface_socket)
|
||||
|
||||
# Add Unit to Parsed (if relevant)
|
||||
if unit is not None:
|
||||
parsed_bl_socket_value *= unit
|
||||
elif unit_system is not None:
|
||||
parsed_bl_socket_value *= unit_system[socket_type]
|
||||
|
||||
return parsed_bl_socket_value
|
||||
|
||||
####################
|
||||
# - Convert to Blender-Compatible Value
|
||||
####################
|
||||
def make_scalar_bl_compat(scalar: typ.Any) -> typ.Any:
|
||||
"""Blender doesn't accept ex. Sympy numbers as values.
|
||||
Therefore, we need to do some conforming.
|
||||
|
||||
Currently hard-coded; this is probably best.
|
||||
"""
|
||||
if isinstance(scalar, sp.Integer):
|
||||
return int(scalar)
|
||||
elif isinstance(scalar, sp.Float):
|
||||
return float(scalar)
|
||||
elif isinstance(scalar, sp.Rational):
|
||||
return float(scalar)
|
||||
elif isinstance(scalar, sp.Expr):
|
||||
return float(scalar.n())
|
||||
## TODO: More?
|
||||
|
||||
return scalar
|
||||
|
||||
def value_to_bl(
|
||||
bl_interface_socket: bpy.types.NodeSocket,
|
||||
value: typ.Any,
|
||||
unit_system: dict | None = None,
|
||||
) -> typ.Any:
|
||||
socket_type, unit = parse_bl_interface_socket(bl_interface_socket)
|
||||
|
||||
# Set Socket
|
||||
if unit is not None:
|
||||
bl_socket_value = spu.convert_to(value, unit) / unit
|
||||
elif (
|
||||
unit_system is not None
|
||||
and socket_type in unit_system
|
||||
):
|
||||
bl_socket_value = spu.convert_to(
|
||||
value, unit_system[socket_type]
|
||||
) / unit_system[socket_type]
|
||||
else:
|
||||
bl_socket_value = value
|
||||
|
||||
return {
|
||||
1: lambda: make_scalar_bl_compat(bl_socket_value),
|
||||
2: lambda: tuple([
|
||||
make_scalar_bl_compat(bl_socket_value[0]),
|
||||
make_scalar_bl_compat(bl_socket_value[1]),
|
||||
bl_interface_socket.default_value[2]
|
||||
## Don't touch (unused) 3rd bl_socket coordinate
|
||||
]),
|
||||
3: lambda: tuple([
|
||||
make_scalar_bl_compat(el)
|
||||
for el in bl_socket_value
|
||||
]),
|
||||
4: lambda: tuple([
|
||||
make_scalar_bl_compat(el)
|
||||
for el in bl_socket_value
|
||||
]),
|
||||
}[size_from_bl_interface_socket(bl_interface_socket)]()
|
||||
## The 'lambda' delays construction until size is determined
|
|
@ -0,0 +1,91 @@
|
|||
## TODO: Refactor this whole horrible module.
|
||||
|
||||
import bpy
|
||||
import nodeitems_utils
|
||||
from . import contracts as ct
|
||||
from .nodes import BL_NODES
|
||||
|
||||
DYNAMIC_SUBMENU_REGISTRATIONS = []
|
||||
def mk_node_categories(
|
||||
tree,
|
||||
syllable_prefix = [],
|
||||
#root = True,
|
||||
):
|
||||
global DYNAMIC_SUBMENU_REGISTRATIONS
|
||||
items = []
|
||||
|
||||
# Add Node Items
|
||||
base_category = ct.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))
|
||||
|
||||
# Add Node Sub-Menus
|
||||
for syllable, sub_tree in tree.items():
|
||||
current_syllable_path = syllable_prefix + [syllable]
|
||||
current_category = ct.NodeCategory[
|
||||
"_".join(current_syllable_path)
|
||||
]
|
||||
|
||||
# Build Items for Sub-Categories
|
||||
subitems = mk_node_categories(
|
||||
sub_tree,
|
||||
current_syllable_path,
|
||||
)
|
||||
if len(subitems) == 0: continue
|
||||
|
||||
# Define Dynamic Node Submenu
|
||||
def draw_factory(items):
|
||||
def draw(self, context):
|
||||
for nodeitem_or_submenu in items:
|
||||
if isinstance(
|
||||
nodeitem_or_submenu,
|
||||
nodeitems_utils.NodeItem,
|
||||
):
|
||||
nodeitem = nodeitem_or_submenu
|
||||
op_add_node_cfg = self.layout.operator(
|
||||
"node.add_node",
|
||||
text=nodeitem.label,
|
||||
)
|
||||
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)
|
||||
return draw
|
||||
|
||||
menu_class = type(str(current_category.value), (bpy.types.Menu,), {
|
||||
'bl_idname': current_category.value,
|
||||
'bl_label': ct.NODE_CAT_LABELS[current_category],
|
||||
'draw': draw_factory(tuple(subitems)),
|
||||
})
|
||||
|
||||
# Report to Items and Registration List
|
||||
items.append(current_category.value)
|
||||
DYNAMIC_SUBMENU_REGISTRATIONS.append(menu_class)
|
||||
|
||||
return items
|
||||
|
||||
|
||||
|
||||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_NODE_CATEGORIES = mk_node_categories(
|
||||
ct.NodeCategory.get_tree()["MAXWELLSIM"],
|
||||
syllable_prefix = ["MAXWELLSIM"],
|
||||
)
|
||||
## TODO: refactor, this has a big code smell
|
||||
BL_REGISTER = [
|
||||
*DYNAMIC_SUBMENU_REGISTRATIONS
|
||||
] ## Must be run after, right now.
|
||||
|
||||
## TEST - TODO this is a big code smell
|
||||
def menu_draw(self, context):
|
||||
if context.space_data.tree_type == ct.TreeType.MaxwellSim.value:
|
||||
for nodeitem_or_submenu in BL_NODE_CATEGORIES:
|
||||
if isinstance(nodeitem_or_submenu, str):
|
||||
submenu_id = nodeitem_or_submenu
|
||||
self.layout.menu(submenu_id)
|
||||
|
||||
bpy.types.NODE_MT_add.append(menu_draw)
|
|
@ -0,0 +1,56 @@
|
|||
####################
|
||||
# - String Types
|
||||
####################
|
||||
from .bl import SocketName
|
||||
from .bl import PresetName
|
||||
from .bl import ManagedObjName
|
||||
|
||||
from .bl import BLEnumID
|
||||
from .bl import BLColorRGBA
|
||||
|
||||
####################
|
||||
# - Icon Types
|
||||
####################
|
||||
from .icons import Icon
|
||||
|
||||
####################
|
||||
# - Tree Types
|
||||
####################
|
||||
from .trees import TreeType
|
||||
|
||||
####################
|
||||
# - Socket Types
|
||||
####################
|
||||
from .socket_types import SocketType
|
||||
|
||||
from .socket_units import SOCKET_UNITS
|
||||
from .socket_colors import SOCKET_COLORS
|
||||
from .socket_shapes import SOCKET_SHAPES
|
||||
|
||||
from .socket_from_bl_desc import BL_SOCKET_DESCR_TYPE_MAP
|
||||
from .socket_from_bl_direct import BL_SOCKET_DIRECT_TYPE_MAP
|
||||
|
||||
from .socket_from_bl_desc import BL_SOCKET_DESCR_ANNOT_STRING
|
||||
|
||||
####################
|
||||
# - Node Types
|
||||
####################
|
||||
from .node_types import NodeType
|
||||
|
||||
from .node_cats import NodeCategory
|
||||
from .node_cat_labels import NODE_CAT_LABELS
|
||||
|
||||
####################
|
||||
# - Managed Obj Type
|
||||
####################
|
||||
from .managed_obj_type import ManagedObjType
|
||||
|
||||
####################
|
||||
# - Data Flows
|
||||
####################
|
||||
from .data_flows import DataFlowKind
|
||||
|
||||
####################
|
||||
# - Schemas
|
||||
####################
|
||||
from . import schemas
|
|
@ -0,0 +1,26 @@
|
|||
import typing as typ
|
||||
import pydantic as pyd
|
||||
import typing_extensions as pytypes_ext
|
||||
|
||||
import bpy
|
||||
|
||||
####################
|
||||
# - Pure BL Types
|
||||
####################
|
||||
BLEnumID = pytypes_ext.Annotated[str, pyd.StringConstraints(
|
||||
pattern=r'^[A-Z_]+$',
|
||||
)]
|
||||
SocketName = pytypes_ext.Annotated[str, pyd.StringConstraints(
|
||||
pattern=r'^[a-zA-Z0-9_]+$',
|
||||
)]
|
||||
PresetName = pytypes_ext.Annotated[str, pyd.StringConstraints(
|
||||
pattern=r'^[a-zA-Z0-9_]+$',
|
||||
)]
|
||||
BLColorRGBA = tuple[float, float, float, float]
|
||||
|
||||
####################
|
||||
# - Shared-With-BL Types
|
||||
####################
|
||||
ManagedObjName = pytypes_ext.Annotated[str, pyd.StringConstraints(
|
||||
pattern=r'^[a-z_]+$',
|
||||
)]
|
|
@ -0,0 +1,56 @@
|
|||
import enum
|
||||
|
||||
from ....utils.blender_type_enum import BlenderTypeEnum
|
||||
|
||||
class DataFlowKind(BlenderTypeEnum):
|
||||
"""Defines a shape/kind of data that may flow through a node tree.
|
||||
|
||||
Since a node socket may define one of each, we can support several related kinds of data flow through the same node-graph infrastructure.
|
||||
|
||||
Attributes:
|
||||
Value: A value usable without new data.
|
||||
- Basic types aka. float, int, list, string, etc. .
|
||||
- Exotic (immutable-ish) types aka. numpy array, KDTree, etc. .
|
||||
- A usable constructed object, ex. a `tidy3d.Box`.
|
||||
- Expressions (`sp.Expr`) that don't have unknown variables.
|
||||
- Lazy sequences aka. generators, with all data bound.
|
||||
|
||||
LazyValue: An object which, when given new data, can make many values.
|
||||
- An `sp.Expr`, which might need `simplify`ing, `jax` JIT'ing, unit cancellations, variable substitutions, etc. before use.
|
||||
- Lazy objects, for which all parameters aren't yet known.
|
||||
- A computational graph aka. `aesara`, which may even need to be handled before
|
||||
|
||||
Capabilities: A `ValueCapability` object providing compatibility.
|
||||
|
||||
# Value Data Flow
|
||||
Simply passing values is the simplest and easiest use case.
|
||||
|
||||
This doesn't mean it's "dumb" - ex. a `sp.Expr` might, before use, have `simplify`, rewriting, unit cancellation, etc. run.
|
||||
All of this is okay, as long as there is no *introduction of new data* ex. variable substitutions.
|
||||
|
||||
|
||||
# Lazy Value Data Flow
|
||||
By passing (essentially) functions, one supports:
|
||||
- **Lightness**: While lazy values can be made expensive to construct, they will generally not be nearly as heavy to handle when trying to work with ex. operations on voxel arrays.
|
||||
- **Performance**: Parameterizing ex. `sp.Expr` with variables allows one to build very optimized functions, which can make ex. node graph updates very fast if the only operation run is the `jax` JIT'ed function (aka. GPU accelerated) generated from the final full expression.
|
||||
- **Numerical Stability**: Libraries like `aesara` build a computational graph, which can be automatically rewritten to avoid many obvious conditioning / cancellation errors.
|
||||
- **Lazy Output**: The goal of a node-graph may not be the definition of a single value, but rather, a parameterized expression for generating *many values* with known properties. This is especially interesting for use cases where one wishes to build an optimization step using nodes.
|
||||
|
||||
|
||||
# Capability Passing
|
||||
By being able to pass "capabilities" next to other kinds of values, nodes can quickly determine whether a given link is valid without having to actually compute it.
|
||||
|
||||
|
||||
# Lazy Parameter Value
|
||||
When using parameterized LazyValues, one may wish to independently pass parameter values through the graph, so they can be inserted into the final (cached) high-performance expression without.
|
||||
|
||||
The advantage of using a different data flow would be changing this kind of value would ONLY invalidate lazy parameter value caches, which would allow an incredibly fast path of getting the value into the lazy expression for high-performance computation.
|
||||
|
||||
Implementation TBD - though, ostensibly, one would have a "parameter" node which both would only provide a LazyValue (aka. a symbolic variable), but would also be able to provide a LazyParamValue, which would be a particular value of some kind (probably via the `value` of some other node socket).
|
||||
"""
|
||||
|
||||
Value = enum.auto()
|
||||
LazyValue = enum.auto()
|
||||
Capabilities = enum.auto()
|
||||
|
||||
LazyParamValue = enum.auto()
|
|
@ -0,0 +1,4 @@
|
|||
from ....utils.blender_type_enum import BlenderTypeEnum
|
||||
|
||||
class Icon(BlenderTypeEnum):
|
||||
SimNodeEditor = "MOD_SIMPLEDEFORM"
|
|
@ -0,0 +1,9 @@
|
|||
import enum
|
||||
|
||||
from ....utils.blender_type_enum import (
|
||||
BlenderTypeEnum
|
||||
)
|
||||
|
||||
class ManagedObjType(BlenderTypeEnum):
|
||||
ManagedBLObject = enum.auto()
|
||||
ManagedBLImage = enum.auto()
|
|
@ -0,0 +1,46 @@
|
|||
from .node_cats import NodeCategory as NC
|
||||
|
||||
NODE_CAT_LABELS = {
|
||||
# Inputs/
|
||||
NC.MAXWELLSIM_INPUTS: "Inputs",
|
||||
NC.MAXWELLSIM_INPUTS_IMPORTERS: "Importers",
|
||||
NC.MAXWELLSIM_INPUTS_SCENE: "Scene",
|
||||
NC.MAXWELLSIM_INPUTS_PARAMETERS: "Parameters",
|
||||
NC.MAXWELLSIM_INPUTS_CONSTANTS: "Constants",
|
||||
NC.MAXWELLSIM_INPUTS_LISTS: "Lists",
|
||||
|
||||
# Outputs/
|
||||
NC.MAXWELLSIM_OUTPUTS: "Outputs",
|
||||
NC.MAXWELLSIM_OUTPUTS_VIEWERS: "Viewers",
|
||||
NC.MAXWELLSIM_OUTPUTS_EXPORTERS: "Exporters",
|
||||
NC.MAXWELLSIM_OUTPUTS_PLOTTERS: "Plotters",
|
||||
|
||||
# Sources/
|
||||
NC.MAXWELLSIM_SOURCES: "Sources",
|
||||
NC.MAXWELLSIM_SOURCES_TEMPORALSHAPES: "Temporal Shapes",
|
||||
|
||||
# Mediums/
|
||||
NC.MAXWELLSIM_MEDIUMS: "Mediums",
|
||||
NC.MAXWELLSIM_MEDIUMS_NONLINEARITIES: "Non-Linearities",
|
||||
|
||||
# Structures/
|
||||
NC.MAXWELLSIM_STRUCTURES: "Structures",
|
||||
NC.MAXWELLSIM_STRUCTURES_PRIMITIVES: "Primitives",
|
||||
|
||||
# Bounds/
|
||||
NC.MAXWELLSIM_BOUNDS: "Bounds",
|
||||
NC.MAXWELLSIM_BOUNDS_BOUNDCONDS: "Bound Conds",
|
||||
|
||||
# Monitors/
|
||||
NC.MAXWELLSIM_MONITORS: "Monitors",
|
||||
NC.MAXWELLSIM_MONITORS_NEARFIELDPROJECTIONS: "Near-Field Projections",
|
||||
|
||||
# Simulations/
|
||||
NC.MAXWELLSIM_SIMS: "Simulations",
|
||||
NC.MAXWELLSIM_SIMGRIDAXES: "Sim Grid Axes",
|
||||
|
||||
# Utilities/
|
||||
NC.MAXWELLSIM_UTILITIES: "Utilities",
|
||||
NC.MAXWELLSIM_UTILITIES_CONVERTERS: "Converters",
|
||||
NC.MAXWELLSIM_UTILITIES_OPERATIONS: "Operations",
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
import enum
|
||||
|
||||
from ....utils.blender_type_enum import (
|
||||
BlenderTypeEnum, wrap_values_in_MT
|
||||
)
|
||||
|
||||
@wrap_values_in_MT
|
||||
class NodeCategory(BlenderTypeEnum):
|
||||
MAXWELLSIM = enum.auto()
|
||||
|
||||
# Inputs/
|
||||
MAXWELLSIM_INPUTS = enum.auto()
|
||||
MAXWELLSIM_INPUTS_IMPORTERS = enum.auto()
|
||||
MAXWELLSIM_INPUTS_SCENE = enum.auto()
|
||||
MAXWELLSIM_INPUTS_PARAMETERS = enum.auto()
|
||||
MAXWELLSIM_INPUTS_CONSTANTS = enum.auto()
|
||||
MAXWELLSIM_INPUTS_LISTS = enum.auto()
|
||||
|
||||
# Outputs/
|
||||
MAXWELLSIM_OUTPUTS = enum.auto()
|
||||
MAXWELLSIM_OUTPUTS_VIEWERS = enum.auto()
|
||||
MAXWELLSIM_OUTPUTS_EXPORTERS = enum.auto()
|
||||
MAXWELLSIM_OUTPUTS_PLOTTERS = enum.auto()
|
||||
|
||||
# Sources/
|
||||
MAXWELLSIM_SOURCES = enum.auto()
|
||||
MAXWELLSIM_SOURCES_TEMPORALSHAPES = enum.auto()
|
||||
|
||||
# Mediums/
|
||||
MAXWELLSIM_MEDIUMS = enum.auto()
|
||||
MAXWELLSIM_MEDIUMS_NONLINEARITIES = enum.auto()
|
||||
|
||||
# Structures/
|
||||
MAXWELLSIM_STRUCTURES = enum.auto()
|
||||
MAXWELLSIM_STRUCTURES_PRIMITIVES = enum.auto()
|
||||
|
||||
# Bounds/
|
||||
MAXWELLSIM_BOUNDS = enum.auto()
|
||||
MAXWELLSIM_BOUNDS_BOUNDCONDS = enum.auto()
|
||||
|
||||
# Monitors/
|
||||
MAXWELLSIM_MONITORS = enum.auto()
|
||||
MAXWELLSIM_MONITORS_NEARFIELDPROJECTIONS = enum.auto()
|
||||
|
||||
# Simulations/
|
||||
MAXWELLSIM_SIMS = enum.auto()
|
||||
MAXWELLSIM_SIMGRIDAXES = enum.auto()
|
||||
|
||||
# Utilities/
|
||||
MAXWELLSIM_UTILITIES = enum.auto()
|
||||
MAXWELLSIM_UTILITIES_CONVERTERS = enum.auto()
|
||||
MAXWELLSIM_UTILITIES_OPERATIONS = enum.auto()
|
||||
|
||||
@classmethod
|
||||
def get_tree(cls):
|
||||
## TODO: Refactor
|
||||
syllable_categories = [
|
||||
str(node_category.value).split("_")
|
||||
for node_category in cls
|
||||
if node_category.value != "MAXWELLSIM"
|
||||
]
|
||||
|
||||
category_tree = {}
|
||||
for syllable_category in syllable_categories:
|
||||
# Set Current Subtree to Root
|
||||
current_category_subtree = category_tree
|
||||
|
||||
for i, syllable in enumerate(syllable_category):
|
||||
# Create New Category Subtree and/or Step to Subtree
|
||||
if syllable not in current_category_subtree:
|
||||
current_category_subtree[syllable] = {}
|
||||
current_category_subtree = current_category_subtree[syllable]
|
||||
|
||||
return category_tree
|
|
@ -0,0 +1,147 @@
|
|||
import enum
|
||||
|
||||
from ....utils.blender_type_enum import (
|
||||
BlenderTypeEnum, append_cls_name_to_values
|
||||
)
|
||||
|
||||
@append_cls_name_to_values
|
||||
class NodeType(BlenderTypeEnum):
|
||||
KitchenSink = enum.auto()
|
||||
|
||||
# Inputs
|
||||
UnitSystem = enum.auto()
|
||||
|
||||
## Inputs / Scene
|
||||
Time = enum.auto()
|
||||
|
||||
## Inputs / Importers
|
||||
Tidy3DWebImporter = enum.auto()
|
||||
|
||||
## Inputs / Parameters
|
||||
NumberParameter = enum.auto()
|
||||
PhysicalParameter = enum.auto()
|
||||
|
||||
## Inputs / Constants
|
||||
WaveConstant = enum.auto()
|
||||
ScientificConstant = enum.auto()
|
||||
NumberConstant = enum.auto()
|
||||
PhysicalConstant = enum.auto()
|
||||
BlenderConstant = enum.auto()
|
||||
|
||||
## Inputs / Lists
|
||||
RealList = enum.auto()
|
||||
ComplexList = enum.auto()
|
||||
|
||||
## Inputs /
|
||||
InputFile = enum.auto()
|
||||
|
||||
|
||||
# Outputs
|
||||
## Outputs / Viewers
|
||||
Viewer = enum.auto()
|
||||
ValueViewer = enum.auto()
|
||||
ConsoleViewer = enum.auto()
|
||||
|
||||
## Outputs / Exporters
|
||||
JSONFileExporter = enum.auto()
|
||||
Tidy3DWebExporter = enum.auto()
|
||||
|
||||
|
||||
# Sources
|
||||
## Sources / Temporal Shapes
|
||||
GaussianPulseTemporalShape = enum.auto()
|
||||
ContinuousWaveTemporalShape = enum.auto()
|
||||
ListTemporalShape = enum.auto()
|
||||
|
||||
## Sources /
|
||||
PointDipoleSource = enum.auto()
|
||||
UniformCurrentSource = enum.auto()
|
||||
PlaneWaveSource = enum.auto()
|
||||
ModeSource = enum.auto()
|
||||
GaussianBeamSource = enum.auto()
|
||||
AstigmaticGaussianBeamSource = enum.auto()
|
||||
TFSFSource = enum.auto()
|
||||
|
||||
EHEquivalenceSource = enum.auto()
|
||||
EHSource = enum.auto()
|
||||
|
||||
# Mediums
|
||||
LibraryMedium = enum.auto()
|
||||
|
||||
PECMedium = enum.auto()
|
||||
IsotropicMedium = enum.auto()
|
||||
AnisotropicMedium = enum.auto()
|
||||
|
||||
TripleSellmeierMedium = enum.auto()
|
||||
SellmeierMedium = enum.auto()
|
||||
PoleResidueMedium = enum.auto()
|
||||
DrudeMedium = enum.auto()
|
||||
DrudeLorentzMedium = enum.auto()
|
||||
DebyeMedium = enum.auto()
|
||||
|
||||
## Mediums / Non-Linearities
|
||||
AddNonLinearity = enum.auto()
|
||||
ChiThreeSusceptibilityNonLinearity = enum.auto()
|
||||
TwoPhotonAbsorptionNonLinearity = enum.auto()
|
||||
KerrNonLinearity = enum.auto()
|
||||
|
||||
# Structures
|
||||
ObjectStructure = enum.auto()
|
||||
GeoNodesStructure = enum.auto()
|
||||
ScriptedStructure = enum.auto()
|
||||
|
||||
## Structures / Primitives
|
||||
BoxStructure = enum.auto()
|
||||
SphereStructure = enum.auto()
|
||||
CylinderStructure = enum.auto()
|
||||
|
||||
|
||||
# Bounds
|
||||
BoundConds = enum.auto()
|
||||
|
||||
## Bounds / Bound Faces
|
||||
PMLBoundCond = enum.auto()
|
||||
PECBoundCond = enum.auto()
|
||||
PMCBoundCond = enum.auto()
|
||||
|
||||
BlochBoundCond = enum.auto()
|
||||
PeriodicBoundCond = enum.auto()
|
||||
AbsorbingBoundCond = enum.auto()
|
||||
|
||||
|
||||
# Monitors
|
||||
EHFieldMonitor = enum.auto()
|
||||
FieldPowerFluxMonitor = enum.auto()
|
||||
EpsilonTensorMonitor = enum.auto()
|
||||
DiffractionMonitor = enum.auto()
|
||||
|
||||
## Monitors / Near-Field Projections
|
||||
CartesianNearFieldProjectionMonitor = enum.auto()
|
||||
ObservationAngleNearFieldProjectionMonitor = enum.auto()
|
||||
KSpaceNearFieldProjectionMonitor = enum.auto()
|
||||
|
||||
|
||||
# Sims
|
||||
SimDomain = enum.auto()
|
||||
SimGrid = enum.auto()
|
||||
|
||||
## Sims / Sim Grid Axis
|
||||
AutomaticSimGridAxis = enum.auto()
|
||||
ManualSimGridAxis = enum.auto()
|
||||
UniformSimGridAxis = enum.auto()
|
||||
ArraySimGridAxis = enum.auto()
|
||||
|
||||
## Sim /
|
||||
FDTDSim = enum.auto()
|
||||
|
||||
|
||||
# Utilities
|
||||
Combine = enum.auto()
|
||||
Separate = enum.auto()
|
||||
Math = enum.auto()
|
||||
|
||||
## Utilities / Converters
|
||||
WaveConverter = enum.auto()
|
||||
|
||||
## Utilities / Operations
|
||||
ArrayOperation = enum.auto()
|
|
@ -0,0 +1,4 @@
|
|||
from .preset_def import PresetDef
|
||||
from .socket_def import SocketDef
|
||||
from .managed_obj import ManagedObj
|
||||
from .managed_obj_def import ManagedObjDef
|
|
@ -0,0 +1,33 @@
|
|||
import typing as typ
|
||||
import typing as typx
|
||||
|
||||
import pydantic as pyd
|
||||
|
||||
import bpy
|
||||
|
||||
from ..bl import ManagedObjName, SocketName
|
||||
from ..managed_obj_type import ManagedObjType
|
||||
|
||||
class ManagedObj(typ.Protocol):
|
||||
managed_obj_type: ManagedObjType
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: ManagedObjName,
|
||||
):
|
||||
...
|
||||
|
||||
@property
|
||||
def name(self) -> str: ...
|
||||
@name.setter
|
||||
def name(self, value: str): ...
|
||||
|
||||
def free(self):
|
||||
...
|
||||
|
||||
def bl_select(self):
|
||||
"""If this is a managed Blender object, and the operation "select this in Blender" makes sense, then do so.
|
||||
|
||||
Else, do nothing.
|
||||
"""
|
||||
pass
|
|
@ -0,0 +1,11 @@
|
|||
import typing as typ
|
||||
from dataclasses import dataclass
|
||||
|
||||
import pydantic as pyd
|
||||
|
||||
from ..bl import PresetName, SocketName, BLEnumID
|
||||
from .managed_obj import ManagedObj
|
||||
|
||||
class ManagedObjDef(pyd.BaseModel):
|
||||
mk: typ.Callable[[str], ManagedObj]
|
||||
name_prefix: str = ""
|
|
@ -0,0 +1,9 @@
|
|||
import typing as typ
|
||||
|
||||
import pydantic as pyd
|
||||
|
||||
from ..bl import ManagedObjName, SocketName
|
||||
from ..managed_obj_type import ManagedObjType
|
||||
|
||||
class MaxwellSimNode(typ.Protocol):
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import typing as typ
|
||||
|
||||
import pydantic as pyd
|
||||
|
||||
from ..bl import PresetName, SocketName, BLEnumID
|
||||
|
||||
class PresetDef(pyd.BaseModel):
|
||||
label: PresetName
|
||||
description: str
|
||||
values: dict[SocketName, typ.Any]
|
|
@ -0,0 +1,12 @@
|
|||
import typing as typ
|
||||
|
||||
import bpy
|
||||
|
||||
from ..socket_types import SocketType
|
||||
|
||||
@typ.runtime_checkable
|
||||
class SocketDef(typ.Protocol):
|
||||
socket_type: SocketType
|
||||
|
||||
def init(self, bl_socket: bpy.types.NodeSocket) -> None:
|
||||
...
|
|
@ -0,0 +1,72 @@
|
|||
import sympy.physics.units as spu
|
||||
from ....utils import extra_sympy_units as spuex
|
||||
|
||||
from .socket_types import SocketType as ST
|
||||
|
||||
## TODO: Don't just presume sRGB.
|
||||
SOCKET_COLORS = {
|
||||
# Basic
|
||||
ST.Any: (0.8, 0.8, 0.8, 1.0), # Light Grey
|
||||
ST.Bool: (0.7, 0.7, 0.7, 1.0), # Medium Light Grey
|
||||
ST.String: (0.7, 0.7, 0.7, 1.0), # Medium Light Grey
|
||||
ST.FilePath: (0.6, 0.6, 0.6, 1.0), # Medium Grey
|
||||
|
||||
# Number
|
||||
ST.IntegerNumber: (0.5, 0.5, 1.0, 1.0), # Light Blue
|
||||
ST.RationalNumber: (0.4, 0.4, 0.9, 1.0), # Medium Light Blue
|
||||
ST.RealNumber: (0.3, 0.3, 0.8, 1.0), # Medium Blue
|
||||
ST.ComplexNumber: (0.2, 0.2, 0.7, 1.0), # Dark Blue
|
||||
|
||||
# Vector
|
||||
ST.Integer2DVector: (0.5, 1.0, 0.5, 1.0), # Light Green
|
||||
ST.Real2DVector: (0.5, 1.0, 0.5, 1.0), # Light Green
|
||||
ST.Complex2DVector: (0.4, 0.9, 0.4, 1.0), # Medium Light Green
|
||||
ST.Integer3DVector: (0.3, 0.8, 0.3, 1.0), # Medium Green
|
||||
ST.Real3DVector: (0.3, 0.8, 0.3, 1.0), # Medium Green
|
||||
ST.Complex3DVector: (0.2, 0.7, 0.2, 1.0), # Dark Green
|
||||
|
||||
# Physical
|
||||
ST.PhysicalUnitSystem: (1.0, 0.5, 0.5, 1.0), # Light Red
|
||||
ST.PhysicalTime: (1.0, 0.5, 0.5, 1.0), # Light Red
|
||||
ST.PhysicalAngle: (0.9, 0.45, 0.45, 1.0), # Medium Light Red
|
||||
ST.PhysicalLength: (0.8, 0.4, 0.4, 1.0), # Medium Red
|
||||
ST.PhysicalArea: (0.7, 0.35, 0.35, 1.0), # Medium Dark Red
|
||||
ST.PhysicalVolume: (0.6, 0.3, 0.3, 1.0), # Dark Red
|
||||
ST.PhysicalPoint2D: (0.7, 0.35, 0.35, 1.0), # Medium Dark Red
|
||||
ST.PhysicalPoint3D: (0.6, 0.3, 0.3, 1.0), # Dark Red
|
||||
ST.PhysicalSize2D: (0.7, 0.35, 0.35, 1.0), # Medium Dark Red
|
||||
ST.PhysicalSize3D: (0.6, 0.3, 0.3, 1.0), # Dark Red
|
||||
ST.PhysicalMass: (0.9, 0.6, 0.4, 1.0), # Light Orange
|
||||
ST.PhysicalSpeed: (0.8, 0.55, 0.35, 1.0), # Medium Light Orange
|
||||
ST.PhysicalAccelScalar: (0.7, 0.5, 0.3, 1.0), # Medium Orange
|
||||
ST.PhysicalForceScalar: (0.6, 0.45, 0.25, 1.0), # Medium Dark Orange
|
||||
ST.PhysicalAccel3D: (0.7, 0.5, 0.3, 1.0), # Medium Orange
|
||||
ST.PhysicalForce3D: (0.6, 0.45, 0.25, 1.0), # Medium Dark Orange
|
||||
ST.PhysicalPol: (0.5, 0.4, 0.2, 1.0), # Dark Orange
|
||||
ST.PhysicalFreq: (1.0, 0.7, 0.5, 1.0), # Light Peach
|
||||
|
||||
# Blender
|
||||
ST.BlenderObject: (0.7, 0.5, 1.0, 1.0), # Light Purple
|
||||
ST.BlenderCollection: (0.6, 0.45, 0.9, 1.0), # Medium Light Purple
|
||||
ST.BlenderImage: (0.5, 0.4, 0.8, 1.0), # Medium Purple
|
||||
ST.BlenderGeoNodes: (0.3, 0.3, 0.6, 1.0), # Dark Purple
|
||||
ST.BlenderText: (0.5, 0.5, 0.75, 1.0), # Light Lavender
|
||||
|
||||
# Maxwell
|
||||
ST.MaxwellSource: (1.0, 1.0, 0.5, 1.0), # Light Yellow
|
||||
ST.MaxwellTemporalShape: (0.9, 0.9, 0.45, 1.0), # Medium Light Yellow
|
||||
ST.MaxwellMedium: (0.8, 0.8, 0.4, 1.0), # Medium Yellow
|
||||
ST.MaxwellMediumNonLinearity: (0.7, 0.7, 0.35, 1.0), # Medium Dark Yellow
|
||||
ST.MaxwellStructure: (0.6, 0.6, 0.3, 1.0), # Dark Yellow
|
||||
ST.MaxwellBoundConds: (0.9, 0.8, 0.5, 1.0), # Light Gold
|
||||
ST.MaxwellBoundCond: (0.8, 0.7, 0.45, 1.0), # Medium Light Gold
|
||||
ST.MaxwellMonitor: (0.7, 0.6, 0.4, 1.0), # Medium Gold
|
||||
ST.MaxwellFDTDSim: (0.6, 0.5, 0.35, 1.0), # Medium Dark Gold
|
||||
ST.MaxwellSimGrid: (0.5, 0.4, 0.3, 1.0), # Dark Gold
|
||||
ST.MaxwellSimGridAxis: (0.4, 0.3, 0.25, 1.0), # Darkest Gold
|
||||
ST.MaxwellSimDomain: (0.4, 0.3, 0.25, 1.0), # Darkest Gold
|
||||
|
||||
# Tidy3D
|
||||
ST.Tidy3DCloudTask: (0.4, 0.3, 0.25, 1.0), # Darkest Gold
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
from .socket_types import SocketType as ST
|
||||
|
||||
BL_SOCKET_DESCR_ANNOT_STRING = ":: "
|
||||
BL_SOCKET_DESCR_TYPE_MAP = {
|
||||
("Time", "NodeSocketFloat", 1): ST.PhysicalTime,
|
||||
|
||||
("Angle", "NodeSocketFloat", 1): ST.PhysicalAngle,
|
||||
("SolidAngle", "NodeSocketFloat", 1): ST.PhysicalSolidAngle,
|
||||
|
||||
("Rotation", "NodeSocketVector", 2): ST.PhysicalRot2D,
|
||||
("Rotation", "NodeSocketVector", 3): ST.PhysicalRot3D,
|
||||
|
||||
("Freq", "NodeSocketFloat", 1): ST.PhysicalFreq,
|
||||
("AngFreq", "NodeSocketFloat", 1): ST.PhysicalAngFreq,
|
||||
|
||||
## Cartesian
|
||||
("Length", "NodeSocketFloat", 1): ST.PhysicalLength,
|
||||
("Area", "NodeSocketFloat", 1): ST.PhysicalArea,
|
||||
("Volume", "NodeSocketFloat", 1): ST.PhysicalVolume,
|
||||
|
||||
("Disp", "NodeSocketVector", 2): ST.PhysicalDisp2D,
|
||||
("Disp", "NodeSocketVector", 3): ST.PhysicalDisp3D,
|
||||
|
||||
("Point", "NodeSocketFloat", 1): ST.PhysicalPoint1D,
|
||||
("Point", "NodeSocketVector", 2): ST.PhysicalPoint2D,
|
||||
("Point", "NodeSocketVector", 3): ST.PhysicalPoint3D,
|
||||
|
||||
("Size", "NodeSocketVector", 2): ST.PhysicalSize2D,
|
||||
("Size", "NodeSocketVector", 3): ST.PhysicalSize3D,
|
||||
|
||||
## Mechanical
|
||||
("Mass", "NodeSocketFloat", 1): ST.PhysicalMass,
|
||||
|
||||
("Speed", "NodeSocketFloat", 1): ST.PhysicalSpeed,
|
||||
("Vel", "NodeSocketVector", 2): ST.PhysicalVel2D,
|
||||
("Vel", "NodeSocketVector", 3): ST.PhysicalVel3D,
|
||||
("Accel", "NodeSocketFloat", 1): ST.PhysicalAccelScalar,
|
||||
("Accel", "NodeSocketVector", 2): ST.PhysicalAccel2D,
|
||||
("Accel", "NodeSocketVector", 3): ST.PhysicalAccel3D,
|
||||
("Force", "NodeSocketFloat", 1): ST.PhysicalForceScalar,
|
||||
("Force", "NodeSocketVector", 2): ST.PhysicalForce2D,
|
||||
("Force", "NodeSocketVector", 3): ST.PhysicalForce3D,
|
||||
("Pressure", "NodeSocketFloat", 1): ST.PhysicalPressure,
|
||||
|
||||
## Energetic
|
||||
("Energy", "NodeSocketFloat", 1): ST.PhysicalEnergy,
|
||||
("Power", "NodeSocketFloat", 1): ST.PhysicalPower,
|
||||
("Temp", "NodeSocketFloat", 1): ST.PhysicalTemp,
|
||||
|
||||
## ELectrodynamical
|
||||
("Curr", "NodeSocketFloat", 1): ST.PhysicalCurr,
|
||||
("CurrDens", "NodeSocketVector", 2): ST.PhysicalCurrDens2D,
|
||||
("CurrDens", "NodeSocketVector", 3): ST.PhysicalCurrDens3D,
|
||||
|
||||
("Charge", "NodeSocketFloat", 1): ST.PhysicalCharge,
|
||||
("Voltage", "NodeSocketFloat", 1): ST.PhysicalVoltage,
|
||||
("Capacitance", "NodeSocketFloat", 1): ST.PhysicalCapacitance,
|
||||
("Resistance", "NodeSocketFloat", 1): ST.PhysicalResistance,
|
||||
("Conductance", "NodeSocketFloat", 1): ST.PhysicalConductance,
|
||||
|
||||
("MagFlux", "NodeSocketFloat", 1): ST.PhysicalMagFlux,
|
||||
("MagFluxDens", "NodeSocketFloat", 1): ST.PhysicalMagFluxDens,
|
||||
("Inductance", "NodeSocketFloat", 1): ST.PhysicalInductance,
|
||||
|
||||
("EField", "NodeSocketFloat", 2): ST.PhysicalEField3D,
|
||||
("EField", "NodeSocketFloat", 3): ST.PhysicalEField2D,
|
||||
("HField", "NodeSocketFloat", 2): ST.PhysicalHField3D,
|
||||
("HField", "NodeSocketFloat", 3): ST.PhysicalHField2D,
|
||||
|
||||
## Luminal
|
||||
("LumIntensity", "NodeSocketFloat", 1): ST.PhysicalLumIntensity,
|
||||
("LumFlux", "NodeSocketFloat", 1): ST.PhysicalLumFlux,
|
||||
("Illuminance", "NodeSocketFloat", 1): ST.PhysicalIlluminance,
|
||||
|
||||
## Optical
|
||||
("PolJones", "NodeSocketFloat", 2): ST.PhysicalPolJones,
|
||||
("Pol", "NodeSocketFloat", 4): ST.PhysicalPol,
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
from .socket_types import SocketType as ST
|
||||
|
||||
BL_SOCKET_DIRECT_TYPE_MAP = {
|
||||
("NodeSocketString", 1): ST.String,
|
||||
("NodeSocketBool", 1): ST.Bool,
|
||||
("NodeSocketCollection", 1): ST.BlenderCollection,
|
||||
("NodeSocketImage", 1): ST.BlenderImage,
|
||||
("NodeSocketObject", 1): ST.BlenderObject,
|
||||
|
||||
("NodeSocketFloat", 1): ST.RealNumber,
|
||||
#("NodeSocketFloatAngle", 1): ST.PhysicalAngle,
|
||||
#("NodeSocketFloatDistance", 1): ST.PhysicalLength,
|
||||
("NodeSocketFloatFactor", 1): ST.RealNumber,
|
||||
("NodeSocketFloatPercentage", 1): ST.RealNumber,
|
||||
#("NodeSocketFloatTime", 1): ST.PhysicalTime,
|
||||
#("NodeSocketFloatTimeAbsolute", 1): ST.PhysicalTime,
|
||||
|
||||
("NodeSocketInt", 1): ST.IntegerNumber,
|
||||
("NodeSocketIntFactor", 1): ST.IntegerNumber,
|
||||
("NodeSocketIntPercentage", 1): ST.IntegerNumber,
|
||||
("NodeSocketIntUnsigned", 1): ST.IntegerNumber,
|
||||
|
||||
("NodeSocketRotation", 2): ST.PhysicalRot2D,
|
||||
("NodeSocketColor", 3): ST.Color,
|
||||
("NodeSocketVector", 2): ST.Real2DVector,
|
||||
("NodeSocketVector", 3): ST.Real3DVector,
|
||||
#("NodeSocketVectorAcceleration", 2): ST.PhysicalAccel2D,
|
||||
#("NodeSocketVectorAcceleration", 3): ST.PhysicalAccel3D,
|
||||
#("NodeSocketVectorDirection", 2): ST.Real2DVectorDir,
|
||||
#("NodeSocketVectorDirection", 3): ST.Real3DVectorDir,
|
||||
("NodeSocketVectorEuler", 2): ST.PhysicalRot2D,
|
||||
("NodeSocketVectorEuler", 3): ST.PhysicalRot3D,
|
||||
#("NodeSocketVectorTranslation", 3): ST.PhysicalDisp3D,
|
||||
#("NodeSocketVectorVelocity", 3): ST.PhysicalVel3D,
|
||||
#("NodeSocketVectorXYZ", 3): ST.PhysicalPoint3D,
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
from .socket_types import SocketType as ST
|
||||
|
||||
SOCKET_SHAPES = {
|
||||
# Basic
|
||||
ST.Any: "CIRCLE",
|
||||
ST.Bool: "CIRCLE",
|
||||
ST.String: "SQUARE",
|
||||
ST.FilePath: "SQUARE",
|
||||
|
||||
# Number
|
||||
ST.IntegerNumber: "CIRCLE",
|
||||
ST.RationalNumber: "CIRCLE",
|
||||
ST.RealNumber: "CIRCLE",
|
||||
ST.ComplexNumber: "CIRCLE_DOT",
|
||||
|
||||
# Vector
|
||||
ST.Integer2DVector: "SQUARE_DOT",
|
||||
ST.Real2DVector: "SQUARE_DOT",
|
||||
ST.Complex2DVector: "DIAMOND_DOT",
|
||||
ST.Integer3DVector: "SQUARE_DOT",
|
||||
ST.Real3DVector: "SQUARE_DOT",
|
||||
ST.Complex3DVector: "DIAMOND_DOT",
|
||||
|
||||
# Physical
|
||||
ST.PhysicalUnitSystem: "CIRCLE",
|
||||
ST.PhysicalTime: "CIRCLE",
|
||||
ST.PhysicalAngle: "DIAMOND",
|
||||
ST.PhysicalLength: "SQUARE",
|
||||
ST.PhysicalArea: "SQUARE",
|
||||
ST.PhysicalVolume: "SQUARE",
|
||||
ST.PhysicalPoint2D: "DIAMOND",
|
||||
ST.PhysicalPoint3D: "DIAMOND",
|
||||
ST.PhysicalSize2D: "SQUARE",
|
||||
ST.PhysicalSize3D: "SQUARE",
|
||||
ST.PhysicalMass: "CIRCLE",
|
||||
ST.PhysicalSpeed: "CIRCLE",
|
||||
ST.PhysicalAccelScalar: "CIRCLE",
|
||||
ST.PhysicalForceScalar: "CIRCLE",
|
||||
ST.PhysicalAccel3D: "SQUARE_DOT",
|
||||
ST.PhysicalForce3D: "SQUARE_DOT",
|
||||
ST.PhysicalPol: "DIAMOND",
|
||||
ST.PhysicalFreq: "CIRCLE",
|
||||
|
||||
# Blender
|
||||
ST.BlenderObject: "SQUARE",
|
||||
ST.BlenderCollection: "SQUARE",
|
||||
ST.BlenderImage: "DIAMOND",
|
||||
ST.BlenderGeoNodes: "DIAMOND",
|
||||
ST.BlenderText: "SQUARE",
|
||||
|
||||
# Maxwell
|
||||
ST.MaxwellSource: "CIRCLE",
|
||||
ST.MaxwellTemporalShape: "CIRCLE",
|
||||
ST.MaxwellMedium: "CIRCLE",
|
||||
ST.MaxwellMediumNonLinearity: "CIRCLE",
|
||||
ST.MaxwellStructure: "SQUARE",
|
||||
ST.MaxwellBoundConds: "SQUARE",
|
||||
ST.MaxwellBoundCond: "DIAMOND",
|
||||
ST.MaxwellMonitor: "CIRCLE",
|
||||
ST.MaxwellFDTDSim: "SQUARE",
|
||||
ST.MaxwellSimGrid: "SQUARE",
|
||||
ST.MaxwellSimGridAxis: "DIAMOND",
|
||||
ST.MaxwellSimDomain: "SQUARE",
|
||||
|
||||
# Tidy3D
|
||||
ST.Tidy3DCloudTask: "CIRCLE",
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
import enum
|
||||
|
||||
from ....utils.blender_type_enum import (
|
||||
BlenderTypeEnum, append_cls_name_to_values, wrap_values_in_MT
|
||||
)
|
||||
|
||||
@append_cls_name_to_values
|
||||
class SocketType(BlenderTypeEnum):
|
||||
# Base
|
||||
Any = enum.auto()
|
||||
Bool = enum.auto()
|
||||
String = enum.auto()
|
||||
FilePath = enum.auto()
|
||||
Color = enum.auto()
|
||||
|
||||
# Number
|
||||
IntegerNumber = enum.auto()
|
||||
RationalNumber = enum.auto()
|
||||
RealNumber = enum.auto()
|
||||
ComplexNumber = enum.auto()
|
||||
|
||||
# Vector
|
||||
Integer2DVector = enum.auto()
|
||||
Real2DVector = enum.auto()
|
||||
Real2DVectorDir = enum.auto()
|
||||
Complex2DVector = enum.auto()
|
||||
|
||||
Integer3DVector = enum.auto()
|
||||
Real3DVector = enum.auto()
|
||||
Real3DVectorDir = enum.auto()
|
||||
Complex3DVector = enum.auto()
|
||||
|
||||
# Blender
|
||||
BlenderObject = enum.auto()
|
||||
BlenderCollection = enum.auto()
|
||||
|
||||
BlenderImage = enum.auto()
|
||||
|
||||
BlenderGeoNodes = enum.auto()
|
||||
BlenderText = enum.auto()
|
||||
|
||||
# Maxwell
|
||||
MaxwellBoundConds = enum.auto()
|
||||
MaxwellBoundCond = enum.auto()
|
||||
|
||||
MaxwellMedium = enum.auto()
|
||||
MaxwellMediumNonLinearity = enum.auto()
|
||||
|
||||
MaxwellSource = enum.auto()
|
||||
MaxwellTemporalShape = enum.auto()
|
||||
|
||||
MaxwellStructure = enum.auto()
|
||||
MaxwellMonitor = enum.auto()
|
||||
|
||||
MaxwellFDTDSim = enum.auto()
|
||||
MaxwellSimDomain = enum.auto()
|
||||
MaxwellSimGrid = enum.auto()
|
||||
MaxwellSimGridAxis = enum.auto()
|
||||
|
||||
# Tidy3D
|
||||
Tidy3DCloudTask = enum.auto()
|
||||
|
||||
# Physical
|
||||
PhysicalUnitSystem = enum.auto()
|
||||
|
||||
PhysicalTime = enum.auto()
|
||||
|
||||
PhysicalAngle = enum.auto()
|
||||
PhysicalSolidAngle = enum.auto()
|
||||
|
||||
PhysicalRot2D = enum.auto()
|
||||
PhysicalRot3D = enum.auto()
|
||||
|
||||
PhysicalFreq = enum.auto()
|
||||
PhysicalAngFreq = enum.auto()
|
||||
|
||||
## Cartesian
|
||||
PhysicalLength = enum.auto()
|
||||
PhysicalArea = enum.auto()
|
||||
PhysicalVolume = enum.auto()
|
||||
|
||||
PhysicalDisp2D = enum.auto()
|
||||
PhysicalDisp3D = enum.auto()
|
||||
|
||||
PhysicalPoint1D = enum.auto()
|
||||
PhysicalPoint2D = enum.auto()
|
||||
PhysicalPoint3D = enum.auto()
|
||||
|
||||
PhysicalSize2D = enum.auto()
|
||||
PhysicalSize3D = enum.auto()
|
||||
|
||||
## Mechanical
|
||||
PhysicalMass = enum.auto()
|
||||
|
||||
PhysicalSpeed = enum.auto()
|
||||
PhysicalVel2D = enum.auto()
|
||||
PhysicalVel3D = enum.auto()
|
||||
PhysicalAccelScalar = enum.auto()
|
||||
PhysicalAccel2D = enum.auto()
|
||||
PhysicalAccel3D = enum.auto()
|
||||
PhysicalForceScalar = enum.auto()
|
||||
PhysicalForce2D = enum.auto()
|
||||
PhysicalForce3D = enum.auto()
|
||||
PhysicalPressure = enum.auto()
|
||||
|
||||
## Energetic
|
||||
PhysicalEnergy = enum.auto()
|
||||
PhysicalPower = enum.auto()
|
||||
PhysicalTemp = enum.auto()
|
||||
|
||||
## Electrodynamical
|
||||
PhysicalCurr = enum.auto()
|
||||
PhysicalCurrDens2D = enum.auto()
|
||||
PhysicalCurrDens3D = enum.auto()
|
||||
|
||||
PhysicalCharge = enum.auto()
|
||||
PhysicalVoltage = enum.auto()
|
||||
PhysicalCapacitance = enum.auto()
|
||||
PhysicalResistance = enum.auto()
|
||||
PhysicalConductance = enum.auto()
|
||||
|
||||
PhysicalMagFlux = enum.auto()
|
||||
PhysicalMagFluxDens = enum.auto()
|
||||
PhysicalInductance = enum.auto()
|
||||
|
||||
PhysicalEField2D = enum.auto()
|
||||
PhysicalEField3D = enum.auto()
|
||||
PhysicalHField2D = enum.auto()
|
||||
PhysicalHField3D = enum.auto()
|
||||
|
||||
## Luminal
|
||||
PhysicalLumIntensity = enum.auto()
|
||||
PhysicalLumFlux = enum.auto()
|
||||
PhysicalIlluminance = enum.auto()
|
||||
|
||||
## Optical
|
||||
PhysicalPolJones = enum.auto()
|
||||
PhysicalPol = enum.auto()
|
|
@ -0,0 +1,266 @@
|
|||
import sympy.physics.units as spu
|
||||
from ....utils import extra_sympy_units as spux
|
||||
|
||||
from .socket_types import SocketType as ST
|
||||
|
||||
SOCKET_UNITS = {
|
||||
ST.PhysicalTime: {
|
||||
"default": "PS",
|
||||
"values": {
|
||||
"FS": spux.femtosecond,
|
||||
"PS": spu.picosecond,
|
||||
"NS": spu.nanosecond,
|
||||
"MS": spu.microsecond,
|
||||
"MLSEC": spu.millisecond,
|
||||
"SEC": spu.second,
|
||||
"MIN": spu.minute,
|
||||
"HOUR": spu.hour,
|
||||
"DAY": spu.day,
|
||||
},
|
||||
},
|
||||
|
||||
ST.PhysicalAngle: {
|
||||
"default": "RADIAN",
|
||||
"values": {
|
||||
"RADIAN": spu.radian,
|
||||
"DEGREE": spu.degree,
|
||||
"STERAD": spu.steradian,
|
||||
"ANGMIL": spu.angular_mil,
|
||||
},
|
||||
},
|
||||
|
||||
ST.PhysicalLength: {
|
||||
"default": "UM",
|
||||
"values": {
|
||||
"PM": spu.picometer,
|
||||
"A": spu.angstrom,
|
||||
"NM": spu.nanometer,
|
||||
"UM": spu.micrometer,
|
||||
"MM": spu.millimeter,
|
||||
"CM": spu.centimeter,
|
||||
"M": spu.meter,
|
||||
"INCH": spu.inch,
|
||||
"FOOT": spu.foot,
|
||||
"YARD": spu.yard,
|
||||
"MILE": spu.mile,
|
||||
},
|
||||
},
|
||||
ST.PhysicalArea: {
|
||||
"default": "UM_SQ",
|
||||
"values": {
|
||||
"PM_SQ": spu.picometer**2,
|
||||
"A_SQ": spu.angstrom**2,
|
||||
"NM_SQ": spu.nanometer**2,
|
||||
"UM_SQ": spu.micrometer**2,
|
||||
"MM_SQ": spu.millimeter**2,
|
||||
"CM_SQ": spu.centimeter**2,
|
||||
"M_SQ": spu.meter**2,
|
||||
"INCH_SQ": spu.inch**2,
|
||||
"FOOT_SQ": spu.foot**2,
|
||||
"YARD_SQ": spu.yard**2,
|
||||
"MILE_SQ": spu.mile**2,
|
||||
},
|
||||
},
|
||||
ST.PhysicalVolume: {
|
||||
"default": "UM_CB",
|
||||
"values": {
|
||||
"PM_CB": spu.picometer**3,
|
||||
"A_CB": spu.angstrom**3,
|
||||
"NM_CB": spu.nanometer**3,
|
||||
"UM_CB": spu.micrometer**3,
|
||||
"MM_CB": spu.millimeter**3,
|
||||
"CM_CB": spu.centimeter**3,
|
||||
"M_CB": spu.meter**3,
|
||||
"ML": spu.milliliter,
|
||||
"L": spu.liter,
|
||||
"INCH_CB": spu.inch**3,
|
||||
"FOOT_CB": spu.foot**3,
|
||||
"YARD_CB": spu.yard**3,
|
||||
"MILE_CB": spu.mile**3,
|
||||
},
|
||||
},
|
||||
|
||||
ST.PhysicalPoint2D: {
|
||||
"default": "UM",
|
||||
"values": {
|
||||
"PM": spu.picometer,
|
||||
"A": spu.angstrom,
|
||||
"NM": spu.nanometer,
|
||||
"UM": spu.micrometer,
|
||||
"MM": spu.millimeter,
|
||||
"CM": spu.centimeter,
|
||||
"M": spu.meter,
|
||||
"INCH": spu.inch,
|
||||
"FOOT": spu.foot,
|
||||
"YARD": spu.yard,
|
||||
"MILE": spu.mile,
|
||||
},
|
||||
},
|
||||
ST.PhysicalPoint3D: {
|
||||
"default": "UM",
|
||||
"values": {
|
||||
"PM": spu.picometer,
|
||||
"A": spu.angstrom,
|
||||
"NM": spu.nanometer,
|
||||
"UM": spu.micrometer,
|
||||
"MM": spu.millimeter,
|
||||
"CM": spu.centimeter,
|
||||
"M": spu.meter,
|
||||
"INCH": spu.inch,
|
||||
"FOOT": spu.foot,
|
||||
"YARD": spu.yard,
|
||||
"MILE": spu.mile,
|
||||
},
|
||||
},
|
||||
|
||||
ST.PhysicalSize2D: {
|
||||
"default": "UM",
|
||||
"values": {
|
||||
"PM": spu.picometer,
|
||||
"A": spu.angstrom,
|
||||
"NM": spu.nanometer,
|
||||
"UM": spu.micrometer,
|
||||
"MM": spu.millimeter,
|
||||
"CM": spu.centimeter,
|
||||
"M": spu.meter,
|
||||
"INCH": spu.inch,
|
||||
"FOOT": spu.foot,
|
||||
"YARD": spu.yard,
|
||||
"MILE": spu.mile,
|
||||
},
|
||||
},
|
||||
ST.PhysicalSize3D: {
|
||||
"default": "UM",
|
||||
"values": {
|
||||
"PM": spu.picometer,
|
||||
"A": spu.angstrom,
|
||||
"NM": spu.nanometer,
|
||||
"UM": spu.micrometer,
|
||||
"MM": spu.millimeter,
|
||||
"CM": spu.centimeter,
|
||||
"M": spu.meter,
|
||||
"INCH": spu.inch,
|
||||
"FOOT": spu.foot,
|
||||
"YARD": spu.yard,
|
||||
"MILE": spu.mile,
|
||||
},
|
||||
},
|
||||
|
||||
ST.PhysicalMass: {
|
||||
"default": "UG",
|
||||
"values": {
|
||||
"E_REST": spu.electron_rest_mass,
|
||||
"DAL": spu.dalton,
|
||||
"UG": spu.microgram,
|
||||
"MG": spu.milligram,
|
||||
"G": spu.gram,
|
||||
"KG": spu.kilogram,
|
||||
"TON": spu.metric_ton,
|
||||
},
|
||||
},
|
||||
|
||||
ST.PhysicalSpeed: {
|
||||
"default": "UM_S",
|
||||
"values": {
|
||||
"PM_S": spu.picometer / spu.second,
|
||||
"NM_S": spu.nanometer / spu.second,
|
||||
"UM_S": spu.micrometer / spu.second,
|
||||
"MM_S": spu.millimeter / spu.second,
|
||||
"M_S": spu.meter / spu.second,
|
||||
"KM_S": spu.kilometer / spu.second,
|
||||
"KM_H": spu.kilometer / spu.hour,
|
||||
"FT_S": spu.feet / spu.second,
|
||||
"MI_H": spu.mile / spu.hour,
|
||||
},
|
||||
},
|
||||
ST.PhysicalAccelScalar: {
|
||||
"default": "UM_S_SQ",
|
||||
"values": {
|
||||
"PM_S_SQ": spu.picometer / spu.second**2,
|
||||
"NM_S_SQ": spu.nanometer / spu.second**2,
|
||||
"UM_S_SQ": spu.micrometer / spu.second**2,
|
||||
"MM_S_SQ": spu.millimeter / spu.second**2,
|
||||
"M_S_SQ": spu.meter / spu.second**2,
|
||||
"KM_S_SQ": spu.kilometer / spu.second**2,
|
||||
"FT_S_SQ": spu.feet / spu.second**2,
|
||||
},
|
||||
},
|
||||
ST.PhysicalForceScalar: {
|
||||
"default": "UNEWT",
|
||||
"values": {
|
||||
"KG_M_S_SQ": spu.kg * spu.m/spu.second**2,
|
||||
"NNEWT": spux.nanonewton,
|
||||
"UNEWT": spux.micronewton,
|
||||
"MNEWT": spux.millinewton,
|
||||
"NEWT": spu.newton,
|
||||
},
|
||||
},
|
||||
ST.PhysicalAccel3D: {
|
||||
"default": "UM_S_SQ",
|
||||
"values": {
|
||||
"PM_S_SQ": spu.picometer / spu.second**2,
|
||||
"NM_S_SQ": spu.nanometer / spu.second**2,
|
||||
"UM_S_SQ": spu.micrometer / spu.second**2,
|
||||
"MM_S_SQ": spu.millimeter / spu.second**2,
|
||||
"M_S_SQ": spu.meter / spu.second**2,
|
||||
"KM_S_SQ": spu.kilometer / spu.second**2,
|
||||
"FT_S_SQ": spu.feet / spu.second**2,
|
||||
},
|
||||
},
|
||||
ST.PhysicalForce3D: {
|
||||
"default": "UNEWT",
|
||||
"values": {
|
||||
"KG_M_S_SQ": spu.kg * spu.m/spu.second**2,
|
||||
"NNEWT": spux.nanonewton,
|
||||
"UNEWT": spux.micronewton,
|
||||
"MNEWT": spux.millinewton,
|
||||
"NEWT": spu.newton,
|
||||
},
|
||||
},
|
||||
|
||||
ST.PhysicalFreq: {
|
||||
"default": "THZ",
|
||||
"values": {
|
||||
"HZ": spu.hertz,
|
||||
"KHZ": spux.kilohertz,
|
||||
"MHZ": spux.megahertz,
|
||||
"GHZ": spux.gigahertz,
|
||||
"THZ": spux.terahertz,
|
||||
"PHZ": spux.petahertz,
|
||||
"EHZ": spux.exahertz,
|
||||
},
|
||||
},
|
||||
ST.PhysicalPol: {
|
||||
"default": "RADIAN",
|
||||
"values": {
|
||||
"RADIAN": spu.radian,
|
||||
"DEGREE": spu.degree,
|
||||
"STERAD": spu.steradian,
|
||||
"ANGMIL": spu.angular_mil,
|
||||
},
|
||||
},
|
||||
ST.MaxwellMedium: {
|
||||
"default": "NM",
|
||||
"values": {
|
||||
"PM": spu.picometer, ## c(vac) = wl*freq
|
||||
"A": spu.angstrom,
|
||||
"NM": spu.nanometer,
|
||||
"UM": spu.micrometer,
|
||||
"MM": spu.millimeter,
|
||||
"CM": spu.centimeter,
|
||||
"M": spu.meter,
|
||||
},
|
||||
},
|
||||
ST.MaxwellMonitor: {
|
||||
"default": "NM",
|
||||
"values": {
|
||||
"PM": spu.picometer, ## c(vac) = wl*freq
|
||||
"A": spu.angstrom,
|
||||
"NM": spu.nanometer,
|
||||
"UM": spu.micrometer,
|
||||
"MM": spu.millimeter,
|
||||
"CM": spu.centimeter,
|
||||
"M": spu.meter,
|
||||
},
|
||||
},
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import enum
|
||||
|
||||
from ....utils.blender_type_enum import (
|
||||
BlenderTypeEnum, append_cls_name_to_values
|
||||
)
|
||||
|
||||
@append_cls_name_to_values
|
||||
class TreeType(BlenderTypeEnum):
|
||||
MaxwellSim = enum.auto()
|
|
@ -0,0 +1,2 @@
|
|||
from .managed_bl_image import ManagedBLImage
|
||||
from .managed_bl_object import ManagedBLObject
|
|
@ -0,0 +1,205 @@
|
|||
import typing as typ
|
||||
import typing_extensions as typx
|
||||
import io
|
||||
|
||||
import numpy as np
|
||||
import pydantic as pyd
|
||||
import matplotlib.axis as mpl_ax
|
||||
|
||||
import bpy
|
||||
|
||||
from .. import contracts as ct
|
||||
|
||||
AREA_TYPE = "IMAGE_EDITOR"
|
||||
SPACE_TYPE = "IMAGE_EDITOR"
|
||||
|
||||
class ManagedBLImage(ct.schemas.ManagedObj):
|
||||
managed_obj_type = ct.ManagedObjType.ManagedBLImage
|
||||
_bl_image_name: str
|
||||
|
||||
def __init__(self, name: str):
|
||||
## TODO: Check that blender doesn't have any other images by the same name.
|
||||
self._bl_image_name = name
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._bl_image_name
|
||||
|
||||
@name.setter
|
||||
def name(self, value: str):
|
||||
# Image Doesn't Exist
|
||||
if not (bl_image := bpy.data.images.get(self._bl_image_name)):
|
||||
# ...AND Desired Image Name is Not Taken
|
||||
if not bpy.data.objects.get(value):
|
||||
self._bl_image_name = value
|
||||
return
|
||||
|
||||
# ...AND Desired Image Name is Taken
|
||||
else:
|
||||
msg = f"Desired name {value} for BL image is taken"
|
||||
raise ValueError(msg)
|
||||
|
||||
# Object DOES Exist
|
||||
bl_image.name = value
|
||||
self._bl_image_name = bl_image.name
|
||||
## - When name exists, Blender adds .### to prevent overlap.
|
||||
## - `set_name` is allowed to change the name; nodes account for this.
|
||||
|
||||
def free(self):
|
||||
if not (bl_image := bpy.data.images.get(self.name)):
|
||||
msg = "Can't free BL image that doesn't exist"
|
||||
raise ValueError(msg)
|
||||
|
||||
bpy.data.images.remove(bl_image)
|
||||
|
||||
####################
|
||||
# - Managed Object Management
|
||||
####################
|
||||
def bl_image(
|
||||
self,
|
||||
width_px: int,
|
||||
height_px: int,
|
||||
color_model: typx.Literal["RGB", "RGBA"],
|
||||
dtype: typx.Literal["uint8", "float32"],
|
||||
):
|
||||
"""Returns the managed blender image.
|
||||
|
||||
If the requested image properties are different from the image's, then delete the old image make a new image with correct geometry.
|
||||
"""
|
||||
channels = 4 if color_model == "RGBA" else 3
|
||||
|
||||
# Remove Image (if mismatch)
|
||||
if (
|
||||
(bl_image := bpy.data.images.get(self.name))
|
||||
and (
|
||||
bl_image.size[0] != width_px
|
||||
or bl_image.size[1] != height_px
|
||||
or bl_image.channels != channels
|
||||
or bl_image.is_float ^ (dtype == "float32")
|
||||
)
|
||||
):
|
||||
self.free()
|
||||
|
||||
# Create Image w/Geometry (if none exists)
|
||||
if not (bl_image := bpy.data.images.get(self.name)):
|
||||
bl_image = bpy.data.images.new(
|
||||
self.name,
|
||||
width=width_px,
|
||||
height=height_px,
|
||||
)
|
||||
|
||||
return bl_image
|
||||
|
||||
####################
|
||||
# - Editor UX Manipulation
|
||||
####################
|
||||
@property
|
||||
def preview_area(self) -> bpy.types.Area:
|
||||
"""Returns the visible preview area in the Blender UI.
|
||||
If none are valid, return None.
|
||||
"""
|
||||
valid_areas = [
|
||||
area
|
||||
for area in bpy.context.screen.areas
|
||||
if area.type == AREA_TYPE
|
||||
]
|
||||
if valid_areas:
|
||||
return valid_areas[0]
|
||||
|
||||
@property
|
||||
def preview_space(self) -> bpy.types.SpaceProperties:
|
||||
"""Returns the visible preview space in the visible preview area of
|
||||
the Blender UI
|
||||
"""
|
||||
if (preview_area := self.preview_area):
|
||||
return next(
|
||||
space
|
||||
for space in preview_area.spaces
|
||||
if space.type == SPACE_TYPE
|
||||
)
|
||||
|
||||
####################
|
||||
# - Actions
|
||||
####################
|
||||
def bl_select(self) -> None:
|
||||
"""Synchronizes the managed object to the preview, by manipulating
|
||||
relevant editors.
|
||||
"""
|
||||
if (bl_image := bpy.data.images.get(self.name)):
|
||||
self.preview_space.image = bl_image
|
||||
|
||||
####################
|
||||
# - Special Methods
|
||||
####################
|
||||
def mpl_plot_to_image(
|
||||
self,
|
||||
func_plotter: typ.Callable[[mpl_ax.Axis], None],
|
||||
width_inches: float | None = None,
|
||||
height_inches: float | None = None,
|
||||
dpi: int | None = None,
|
||||
bl_select: bool = False,
|
||||
):
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# Compute Image Geometry
|
||||
if (preview_area := self.preview_area):
|
||||
# Retrieve DPI from Blender Preferences
|
||||
_dpi = bpy.context.preferences.system.dpi
|
||||
|
||||
# Retrieve Image Geometry from Area
|
||||
width_px = preview_area.width
|
||||
height_px = preview_area.height
|
||||
|
||||
# Compute Inches
|
||||
_width_inches = width_px / _dpi
|
||||
_height_inches = height_px / _dpi
|
||||
|
||||
elif width_inches and height_inches and dpi:
|
||||
# Copy Parameters
|
||||
_dpi = dpi
|
||||
_width_inches = height_inches
|
||||
_height_inches = height_inches
|
||||
|
||||
# Compute Pixel Geometry
|
||||
width_px = int(_width_inches * _dpi)
|
||||
height_px = int(_height_inches * _dpi)
|
||||
|
||||
else:
|
||||
msg = f"There must either be a preview area, or defined `width_inches`, `height_inches`, and `dpi`"
|
||||
raise ValueError(msg)
|
||||
|
||||
# Compute Plot Dimensions
|
||||
aspect_ratio = _width_inches / _height_inches
|
||||
|
||||
# Create MPL Figure, Axes, and Compute Figure Geometry
|
||||
fig, ax = plt.subplots(
|
||||
figsize=[_width_inches, _height_inches],
|
||||
dpi=_dpi,
|
||||
)
|
||||
ax.set_aspect(aspect_ratio)
|
||||
cmp_width_px, cmp_height_px = fig.canvas.get_width_height()
|
||||
## Use computed pixel w/h to preempt off-by-one size errors.
|
||||
|
||||
# Plot w/User Parameter
|
||||
func_plotter(ax)
|
||||
|
||||
# Save Figure to BytesIO
|
||||
with io.BytesIO() as buff:
|
||||
fig.savefig(buff, format='raw', dpi=dpi)
|
||||
buff.seek(0)
|
||||
image_data = np.frombuffer(
|
||||
buff.getvalue(),
|
||||
dtype=np.uint8,
|
||||
).reshape([cmp_height_px, cmp_width_px, -1])
|
||||
|
||||
image_data = np.flipud(image_data).astype(np.float32) / 255
|
||||
plt.close(fig)
|
||||
|
||||
# Optimized Write to Blender Image
|
||||
bl_image = self.bl_image(cmp_width_px, cmp_height_px, "RGBA", "uint8")
|
||||
bl_image.pixels.foreach_set(image_data.ravel())
|
||||
bl_image.update()
|
||||
|
||||
if bl_select:
|
||||
self.bl_select()
|
||||
|
|
@ -0,0 +1,392 @@
|
|||
import typing as typ
|
||||
import typing_extensions as typx
|
||||
import functools
|
||||
import contextlib
|
||||
import io
|
||||
|
||||
import numpy as np
|
||||
import pydantic as pyd
|
||||
import matplotlib.axis as mpl_ax
|
||||
|
||||
import bpy
|
||||
import bmesh
|
||||
|
||||
from .. import contracts as ct
|
||||
|
||||
ModifierType = typx.Literal["NODES", "ARRAY"]
|
||||
MODIFIER_NAMES = {
|
||||
"NODES": "BLMaxwell_GeoNodes",
|
||||
"ARRAY": "BLMaxwell_Array",
|
||||
}
|
||||
MANAGED_COLLECTION_NAME = "BLMaxwell"
|
||||
PREVIEW_COLLECTION_NAME = "BLMaxwell Visible"
|
||||
|
||||
def bl_collection(
|
||||
collection_name: str, view_layer_exclude: bool
|
||||
) -> bpy.types.Collection:
|
||||
# Init the "Managed Collection"
|
||||
# Ensure Collection exists (and is in the Scene collection)
|
||||
if collection_name not in bpy.data.collections:
|
||||
collection = bpy.data.collections.new(collection_name)
|
||||
bpy.context.scene.collection.children.link(collection)
|
||||
else:
|
||||
collection = bpy.data.collections[collection_name]
|
||||
|
||||
## Ensure synced View Layer exclusion
|
||||
if (layer_collection := bpy.context.view_layer.layer_collection.children[
|
||||
collection_name
|
||||
]).exclude != view_layer_exclude:
|
||||
layer_collection.exclude = view_layer_exclude
|
||||
|
||||
return collection
|
||||
|
||||
class ManagedBLObject(ct.schemas.ManagedObj):
|
||||
managed_obj_type = ct.ManagedObjType.ManagedBLObject
|
||||
_bl_object_name: str
|
||||
|
||||
def __init__(self, name: str):
|
||||
self._bl_object_name = name
|
||||
|
||||
# Object Name
|
||||
@property
|
||||
def name(self):
|
||||
return self._bl_object_name
|
||||
|
||||
@name.setter
|
||||
def set_name(self, value: str) -> None:
|
||||
# Object Doesn't Exist
|
||||
if not (bl_object := bpy.data.objects.get(self._bl_object_name)):
|
||||
# ...AND Desired Object Name is Not Taken
|
||||
if not bpy.data.objects.get(value):
|
||||
self._bl_object_name = value
|
||||
return
|
||||
|
||||
# ...AND Desired Object Name is Taken
|
||||
else:
|
||||
msg = f"Desired name {value} for BL object is taken"
|
||||
raise ValueError(msg)
|
||||
|
||||
# Object DOES Exist
|
||||
bl_object.name = value
|
||||
self._bl_object_name = bl_object.name
|
||||
## - When name exists, Blender adds .### to prevent overlap.
|
||||
## - `set_name` is allowed to change the name; nodes account for this.
|
||||
|
||||
# Object Datablock Name
|
||||
@property
|
||||
def bl_mesh_name(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def bl_volume_name(self):
|
||||
return self.name
|
||||
|
||||
# Deallocation
|
||||
def free(self):
|
||||
if not (bl_object := bpy.data.objects.get(self.name)):
|
||||
return ## Nothing to do
|
||||
|
||||
# Delete the Underlying Datablock
|
||||
## This automatically deletes the object too
|
||||
if bl_object.type == "MESH":
|
||||
bpy.data.meshes.remove(bl_object.data)
|
||||
elif bl_object.type == "EMPTY":
|
||||
bpy.data.meshes.remove(bl_object.data)
|
||||
elif bl_object.type == "VOLUME":
|
||||
bpy.data.volumes.remove(bl_object.data)
|
||||
else:
|
||||
msg = f"Type of to-delete `bl_object`, {bl_object.type}, is not valid"
|
||||
raise ValueError(msg)
|
||||
|
||||
####################
|
||||
# - Actions
|
||||
####################
|
||||
def show_preview(
|
||||
self,
|
||||
kind: typx.Literal["MESH", "EMPTY", "VOLUME"],
|
||||
empty_display_type: typx.Literal[
|
||||
"PLAIN_AXES", "ARROWS", "SINGLE_ARROW", "CIRCLE", "CUBE",
|
||||
"SPHERE", "CONE", "IMAGE",
|
||||
] | None = None,
|
||||
) -> None:
|
||||
"""Moves the managed Blender object to the preview collection.
|
||||
|
||||
If it's already included, do nothing.
|
||||
"""
|
||||
bl_object = self.bl_object(kind)
|
||||
if bl_object.name not in (preview_collection := bl_collection(
|
||||
PREVIEW_COLLECTION_NAME, view_layer_exclude=False
|
||||
)).objects:
|
||||
preview_collection.objects.link(bl_object)
|
||||
|
||||
if kind == "EMPTY" and empty_display_type is not None:
|
||||
bl_object.empty_display_type = empty_display_type
|
||||
|
||||
def hide_preview(
|
||||
self,
|
||||
kind: typx.Literal["MESH", "EMPTY", "VOLUME"],
|
||||
) -> None:
|
||||
"""Removes the managed Blender object from the preview collection.
|
||||
|
||||
If it's already removed, do nothing.
|
||||
"""
|
||||
bl_object = self.bl_object(kind)
|
||||
if bl_object.name not in (preview_collection := bl_collection(
|
||||
PREVIEW_COLLECTION_NAME, view_layer_exclude=False
|
||||
)).objects:
|
||||
preview_collection.objects.unlink(bl_object)
|
||||
|
||||
def bl_select(self) -> None:
|
||||
"""Selects the managed Blender object globally, causing it to be ex.
|
||||
outlined in the 3D viewport.
|
||||
"""
|
||||
if not (bl_object := bpy.data.objects.get(self.name)):
|
||||
msg = "Managed BLObject does not exist"
|
||||
raise ValueError(msg)
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
bl_object.select_set(True)
|
||||
|
||||
####################
|
||||
# - Managed Object Management
|
||||
####################
|
||||
def bl_object(
|
||||
self,
|
||||
kind: typx.Literal["MESH", "EMPTY", "VOLUME"],
|
||||
):
|
||||
"""Returns the managed blender object.
|
||||
|
||||
If the requested object data type is different, then delete the old
|
||||
object and recreate.
|
||||
"""
|
||||
# Remove Object (if mismatch)
|
||||
if (
|
||||
(bl_object := bpy.data.objects.get(self.name))
|
||||
and bl_object.type != kind
|
||||
):
|
||||
self.free()
|
||||
|
||||
# Create Object w/Appropriate Data Block
|
||||
if not (bl_object := bpy.data.objects.get(self.name)):
|
||||
if kind == "MESH":
|
||||
bl_data = bpy.data.meshes.new(self.bl_mesh_name)
|
||||
elif kind == "EMPTY":
|
||||
bl_data = None
|
||||
elif kind == "VOLUME":
|
||||
raise NotImplementedError
|
||||
else:
|
||||
msg = f"Requested `bl_object` type {bl_object.type} is not valid"
|
||||
raise ValueError(msg)
|
||||
|
||||
bl_object = bpy.data.objects.new(self.name, bl_data)
|
||||
bl_collection(
|
||||
MANAGED_COLLECTION_NAME, view_layer_exclude=True
|
||||
).objects.link(bl_object)
|
||||
|
||||
return bl_object
|
||||
|
||||
####################
|
||||
# - Mesh Data Properties
|
||||
####################
|
||||
@property
|
||||
def raw_mesh(self) -> bpy.types.Mesh:
|
||||
"""Returns the object's raw mesh data.
|
||||
|
||||
Raises an error if the object has no mesh data.
|
||||
"""
|
||||
if (
|
||||
(bl_object := bpy.data.objects.get(self.name))
|
||||
and bl_object.type == "MESH"
|
||||
):
|
||||
return bl_object.data
|
||||
|
||||
msg = f"Requested MESH data from `bl_object` of type {bl_object.type}"
|
||||
raise ValueError(msg)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def mesh_as_bmesh(
|
||||
self,
|
||||
evaluate: bool = True,
|
||||
triangulate: bool = False,
|
||||
) -> bpy.types.Mesh:
|
||||
if (
|
||||
(bl_object := bpy.data.objects.get(self.name))
|
||||
and bl_object.type == "MESH"
|
||||
):
|
||||
bmesh_mesh = None
|
||||
try:
|
||||
bmesh_mesh = bmesh.new()
|
||||
if evaluate:
|
||||
bmesh_mesh.from_object(
|
||||
bl_object,
|
||||
bpy.context.evaluated_depsgraph_get(),
|
||||
)
|
||||
else:
|
||||
bmesh_mesh.from_object(bl_object)
|
||||
|
||||
if triangulate:
|
||||
bmesh.ops.triangulate(bmesh_mesh, faces=bmesh_mesh.faces)
|
||||
|
||||
yield bmesh_mesh
|
||||
|
||||
finally:
|
||||
if bmesh_mesh: bmesh_mesh.free()
|
||||
|
||||
else:
|
||||
msg = f"Requested BMesh from `bl_object` of type {bl_object.type}"
|
||||
raise ValueError(msg)
|
||||
|
||||
@property
|
||||
def mesh_as_arrays(self) -> dict:
|
||||
## TODO: Cached
|
||||
|
||||
# Ensure Updated Geometry
|
||||
bpy.context.view_layer.update()
|
||||
## TODO: Must we?
|
||||
|
||||
# Compute Evaluted + Triangulated Mesh
|
||||
_mesh = bpy.data.meshes.new(name="TemporaryMesh")
|
||||
with self.mesh_as_bmesh(evaluate=True, triangulate=True) as bmesh_mesh:
|
||||
bmesh_mesh.to_mesh(_mesh)
|
||||
|
||||
# Optimized Vertex Copy
|
||||
## See <https://blog.michelanders.nl/2016/02/copying-vertices-to-numpy-arrays-in_4.html>
|
||||
verts = np.zeros(3 * len(_mesh.vertices), dtype=np.float64)
|
||||
_mesh.vertices.foreach_get('co', verts)
|
||||
verts.shape = (-1, 3)
|
||||
|
||||
# Optimized Triangle Copy
|
||||
## To understand, read it, **carefully**.
|
||||
faces = np.zeros(3 * len(_mesh.polygons), dtype=np.uint64)
|
||||
_mesh.polygons.foreach_get('vertices', faces)
|
||||
faces.shape = (-1, 3)
|
||||
|
||||
# Remove Temporary Mesh
|
||||
bpy.data.meshes.remove(_mesh)
|
||||
|
||||
return {
|
||||
"verts": verts,
|
||||
"faces": faces,
|
||||
}
|
||||
|
||||
####################
|
||||
# - Modifier Methods
|
||||
####################
|
||||
def bl_modifier(
|
||||
self,
|
||||
modifier_type: ModifierType,
|
||||
):
|
||||
"""Creates a new modifier for the current `bl_object`.
|
||||
|
||||
For all Blender modifier type names, see: <https://docs.blender.org/api/current/bpy_types_enum_items/object_modifier_type_items.html#rna-enum-object-modifier-type-items>
|
||||
"""
|
||||
if not (bl_object := bpy.data.objects.get(self.name)):
|
||||
msg = "Can't add modifier to BL object that doesn't exist"
|
||||
raise ValueError(msg)
|
||||
|
||||
# (Create and) Return Modifier
|
||||
bl_modifier_name = MODIFIER_NAMES[modifier_type]
|
||||
if bl_modifier_name not in bl_object.modifiers:
|
||||
return bl_object.modifiers.new(
|
||||
name=bl_modifier_name,
|
||||
type=modifier_type,
|
||||
)
|
||||
return bl_object.modifiers[bl_modifier_name]
|
||||
|
||||
def modifier_attrs(self, modifier_type: ModifierType) -> dict:
|
||||
"""Based on the modifier type, retrieve a representative dictionary of modifier attributes.
|
||||
The attributes can then easily be set using `setattr`.
|
||||
"""
|
||||
bl_modifier = self.bl_modifier(modifier_type)
|
||||
|
||||
if modifier_type == "NODES":
|
||||
return {
|
||||
"node_group": bl_modifier.node_group,
|
||||
}
|
||||
elif modifier_type == "ARRAY":
|
||||
raise NotImplementedError
|
||||
|
||||
def s_modifier_attrs(
|
||||
self,
|
||||
modifier_type: ModifierType,
|
||||
modifier_attrs: dict,
|
||||
):
|
||||
bl_modifier = self.bl_modifier(modifier_type)
|
||||
|
||||
if modifier_type == "NODES":
|
||||
if bl_modifier.node_group != modifier_attrs["node_group"]:
|
||||
bl_modifier.node_group = modifier_attrs["node_group"]
|
||||
elif modifier_type == "ARRAY":
|
||||
raise NotImplementedError
|
||||
|
||||
####################
|
||||
# - GeoNodes Modifier
|
||||
####################
|
||||
def sync_geonodes_modifier(
|
||||
self,
|
||||
geonodes_node_group,
|
||||
geonodes_identifier_to_value: dict,
|
||||
):
|
||||
"""Push the given GeoNodes Interface values to a GeoNodes modifier attached to a managed MESH object.
|
||||
|
||||
The values must be compatible with the `default_value`s of the interface sockets.
|
||||
|
||||
If there is no object, it is created.
|
||||
If the object isn't a MESH object, it is made so.
|
||||
If the GeoNodes modifier doesn't exist, it is created.
|
||||
If the GeoNodes node group doesn't match, it is changed.
|
||||
Only differing interface values are actually changed.
|
||||
"""
|
||||
bl_object = self.bl_object("MESH")
|
||||
|
||||
# Get (/make) a GeoModes Modifier
|
||||
bl_modifier = self.bl_modifier("NODES")
|
||||
|
||||
# Set GeoNodes Modifier Attributes (specifically, the 'node_group')
|
||||
self.s_modifier_attrs("NODES", {"node_group": geonodes_node_group})
|
||||
|
||||
# Set GeoNodes Values
|
||||
modifier_altered = False
|
||||
for interface_identifier, value in (
|
||||
geonodes_identifier_to_value.items()
|
||||
):
|
||||
if bl_modifier[interface_identifier] != value:
|
||||
# Quickly Determine if IDPropertyArray is Equal
|
||||
if hasattr(
|
||||
bl_modifier[interface_identifier],
|
||||
"to_list"
|
||||
) and tuple(
|
||||
bl_modifier[interface_identifier].to_list()
|
||||
) == value:
|
||||
continue
|
||||
|
||||
# Quickly Determine int/float Mismatch
|
||||
if isinstance(
|
||||
bl_modifier[interface_identifier],
|
||||
float,
|
||||
) and isinstance(value, int):
|
||||
value = float(value)
|
||||
|
||||
bl_modifier[interface_identifier] = value
|
||||
|
||||
modifier_altered = True
|
||||
|
||||
# Update DepGraph (if anything changed)
|
||||
if modifier_altered:
|
||||
bl_object.data.update()
|
||||
|
||||
|
||||
#@property
|
||||
#def volume(self) -> bpy.types.Volume:
|
||||
# """Returns the object's volume data.
|
||||
#
|
||||
# Raises an error if the object has no volume data.
|
||||
# """
|
||||
# if (
|
||||
# (bl_object := bpy.data.objects.get(self.bl_object_name))
|
||||
# and bl_object.type == "VOLUME"
|
||||
# ):
|
||||
# return bl_object.data
|
||||
#
|
||||
# msg = f"Requested VOLUME data from `bl_object` of type {bl_object.type}"
|
||||
# raise ValueError(msg)
|
|
@ -0,0 +1,185 @@
|
|||
import typing as typ
|
||||
|
||||
import bpy
|
||||
|
||||
from . import contracts as ct
|
||||
|
||||
####################
|
||||
# - Cache Management
|
||||
####################
|
||||
MemAddr = int
|
||||
|
||||
class DeltaNodeLinkCache(typ.TypedDict):
|
||||
added: set[MemAddr]
|
||||
removed: set[MemAddr]
|
||||
|
||||
class NodeLinkCache:
|
||||
def __init__(self, node_tree: bpy.types.NodeTree):
|
||||
# Initialize Parameters
|
||||
self._node_tree = node_tree
|
||||
self.link_ptrs_to_links = {}
|
||||
self.link_ptrs = set()
|
||||
self.link_ptrs_from_sockets = {}
|
||||
self.link_ptrs_to_sockets = {}
|
||||
|
||||
# Fill Cache
|
||||
self.regenerate()
|
||||
|
||||
def remove(self, link_ptrs: set[MemAddr]) -> None:
|
||||
for link_ptr in link_ptrs:
|
||||
self.link_ptrs.remove(link_ptr)
|
||||
self.link_ptrs_to_links.pop(link_ptr, None)
|
||||
|
||||
def regenerate(self) -> DeltaNodeLinkCache:
|
||||
current_link_ptrs_to_links = {
|
||||
link.as_pointer(): link for link in self._node_tree.links
|
||||
}
|
||||
current_link_ptrs = set(current_link_ptrs_to_links.keys())
|
||||
|
||||
# Compute Delta
|
||||
added_link_ptrs = current_link_ptrs - self.link_ptrs
|
||||
removed_link_ptrs = self.link_ptrs - current_link_ptrs
|
||||
|
||||
# Update Caches Incrementally
|
||||
self.remove(removed_link_ptrs)
|
||||
|
||||
self.link_ptrs |= added_link_ptrs
|
||||
for link_ptr in added_link_ptrs:
|
||||
link = current_link_ptrs_to_links[link_ptr]
|
||||
|
||||
self.link_ptrs_to_links[link_ptr] = link
|
||||
self.link_ptrs_from_sockets[link_ptr] = link.from_socket
|
||||
self.link_ptrs_to_sockets[link_ptr] = link.to_socket
|
||||
|
||||
return {"added": added_link_ptrs, "removed": removed_link_ptrs}
|
||||
|
||||
####################
|
||||
# - Node Tree Definition
|
||||
####################
|
||||
class MaxwellSimTree(bpy.types.NodeTree):
|
||||
bl_idname = ct.TreeType.MaxwellSim.value
|
||||
bl_label = "Maxwell Sim Editor"
|
||||
bl_icon = ct.Icon.SimNodeEditor.value
|
||||
|
||||
####################
|
||||
# - Lock Methods
|
||||
####################
|
||||
def unlock_all(self):
|
||||
for node in self.nodes:
|
||||
node.locked = False
|
||||
for bl_socket in [*node.inputs, *node.outputs]:
|
||||
bl_socket.locked = False
|
||||
|
||||
####################
|
||||
# - Init Methods
|
||||
####################
|
||||
def on_load(self):
|
||||
"""Run by Blender when loading the NodeSimTree, ex. on file load, on creation, etc. .
|
||||
|
||||
It's a bit of a "fake" function - in practicality, it's triggered on the first update() function.
|
||||
"""
|
||||
## TODO: Consider tying this to an "on_load" handler
|
||||
self._node_link_cache = NodeLinkCache(self)
|
||||
|
||||
|
||||
####################
|
||||
# - Update Methods
|
||||
####################
|
||||
def sync_node_removed(self, node: bpy.types.Node):
|
||||
"""Run by `Node.free()` when a node is being removed.
|
||||
|
||||
Removes node input links from the internal cache (so we don't attempt to update non-existant sockets).
|
||||
"""
|
||||
for bl_socket in node.inputs.values():
|
||||
# Retrieve Socket Links (if any)
|
||||
self._node_link_cache.remove({
|
||||
link.as_pointer()
|
||||
for link in bl_socket.links
|
||||
})
|
||||
## ONLY Input Socket Links are Removed from the NodeLink Cache
|
||||
## - update() handles link-removal from still-existing node just fine.
|
||||
## - update() does NOT handle link-removal of non-existant nodes.
|
||||
|
||||
def update(self):
|
||||
"""Run by Blender when 'something changes' in the node tree.
|
||||
|
||||
Updates an internal node link cache, then updates sockets that just lost/gained an input link.
|
||||
"""
|
||||
if not hasattr(self, "_node_link_cache"):
|
||||
self.on_load()
|
||||
## We presume update() is run before the first link is altered.
|
||||
## - Else, the first link of the session will not update caches.
|
||||
## - We remain slightly unsure of the semantics.
|
||||
## - More testing needed to prevent this 'first-link bug'.
|
||||
return
|
||||
|
||||
# Compute Changes to NodeLink Cache
|
||||
delta_links = self._node_link_cache.regenerate()
|
||||
|
||||
link_alterations = {
|
||||
"to_remove": [],
|
||||
"to_add": [],
|
||||
}
|
||||
for link_ptr in delta_links["removed"]:
|
||||
from_socket = self._node_link_cache.link_ptrs_from_sockets[link_ptr]
|
||||
to_socket = self._node_link_cache.link_ptrs_to_sockets[link_ptr]
|
||||
|
||||
# Update Socket Caches
|
||||
self._node_link_cache.link_ptrs_from_sockets.pop(link_ptr, None)
|
||||
self._node_link_cache.link_ptrs_to_sockets.pop(link_ptr, None)
|
||||
|
||||
# Trigger Report Chain on Socket that Just Lost a Link
|
||||
## Aka. Forward-Refresh Caches Relying on Linkage
|
||||
if not (
|
||||
consent_removal := to_socket.sync_link_removed(from_socket)
|
||||
):
|
||||
# Did Not Consent to Removal: Queue Add Link
|
||||
link_alterations["to_add"].append((from_socket, to_socket))
|
||||
|
||||
for link_ptr in delta_links["added"]:
|
||||
link = self._node_link_cache.link_ptrs_to_links.get(link_ptr)
|
||||
if link is None: continue
|
||||
|
||||
# Trigger Report Chain on Socket that Just Gained a Link
|
||||
## Aka. Forward-Refresh Caches Relying on Linkage
|
||||
|
||||
if not (
|
||||
consent_added := link.to_socket.sync_link_added(link)
|
||||
):
|
||||
# Did Not Consent to Addition: Queue Remove Link
|
||||
link_alterations["to_remove"].append(link)
|
||||
|
||||
# Execute Queued Operations
|
||||
## - Especially undoing undesirable link changes.
|
||||
## - This is important for locked graphs, whose links must not change.
|
||||
for link in link_alterations["to_remove"]:
|
||||
self.links.remove(link)
|
||||
for from_socket, to_socket in link_alterations["to_add"]:
|
||||
self.links.new(from_socket, to_socket)
|
||||
|
||||
# If Queued Operations: Regenerate Cache
|
||||
## - This prevents the next update() from picking up on alterations.
|
||||
if link_alterations["to_remove"] or link_alterations["to_add"]:
|
||||
self._node_link_cache.regenerate()
|
||||
|
||||
####################
|
||||
# - Post-Load Handler
|
||||
####################
|
||||
def initialize_sim_tree_node_link_cache(scene: bpy.types.Scene):
|
||||
"""Whenever a file is loaded, create/regenerate the NodeLinkCache in all trees.
|
||||
"""
|
||||
for node_tree in bpy.data.node_groups:
|
||||
if node_tree.bl_idname == "MaxwellSimTree":
|
||||
if not hasattr(node_tree, "_node_link_cache"):
|
||||
node_tree._node_link_cache = NodeLinkCache(node_tree)
|
||||
else:
|
||||
node_tree._node_link_cache.regenerate()
|
||||
|
||||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
bpy.app.handlers.load_post.append(initialize_sim_tree_node_link_cache)
|
||||
|
||||
BL_REGISTER = [
|
||||
MaxwellSimTree,
|
||||
]
|
|
@ -0,0 +1,36 @@
|
|||
#from . import kitchen_sink
|
||||
|
||||
from . import inputs
|
||||
from . import outputs
|
||||
from . import sources
|
||||
from . import mediums
|
||||
from . import structures
|
||||
#from . import bounds
|
||||
from . import monitors
|
||||
from . import simulations
|
||||
#from . import utilities
|
||||
|
||||
BL_REGISTER = [
|
||||
#*kitchen_sink.BL_REGISTER,
|
||||
*inputs.BL_REGISTER,
|
||||
*outputs.BL_REGISTER,
|
||||
*sources.BL_REGISTER,
|
||||
*mediums.BL_REGISTER,
|
||||
*structures.BL_REGISTER,
|
||||
# *bounds.BL_REGISTER,
|
||||
*monitors.BL_REGISTER,
|
||||
*simulations.BL_REGISTER,
|
||||
# *utilities.BL_REGISTER,
|
||||
]
|
||||
BL_NODES = {
|
||||
#**kitchen_sink.BL_NODES,
|
||||
**inputs.BL_NODES,
|
||||
**outputs.BL_NODES,
|
||||
**sources.BL_NODES,
|
||||
**mediums.BL_NODES,
|
||||
**structures.BL_NODES,
|
||||
# **bounds.BL_NODES,
|
||||
**monitors.BL_NODES,
|
||||
**simulations.BL_NODES,
|
||||
# **utilities.BL_NODES,
|
||||
}
|
|
@ -0,0 +1,940 @@
|
|||
import uuid
|
||||
|
||||
import typing as typ
|
||||
import typing_extensions as typx
|
||||
import json
|
||||
import inspect
|
||||
|
||||
import bpy
|
||||
import pydantic as pyd
|
||||
|
||||
from .. import contracts as ct
|
||||
from .. import sockets
|
||||
|
||||
CACHE: dict[str, typ.Any] = {} ## By Instance UUID
|
||||
## NOTE: CACHE does not persist between file loads.
|
||||
|
||||
class MaxwellSimNode(bpy.types.Node):
|
||||
# Fundamentals
|
||||
node_type: ct.NodeType
|
||||
bl_idname: str
|
||||
use_sim_node_name: bool = False
|
||||
bl_label: str
|
||||
#draw_label(self) -> str: pass
|
||||
|
||||
# Style
|
||||
bl_description: str = ""
|
||||
|
||||
#bl_width_default: float = 0.0
|
||||
#bl_width_min: float = 0.0
|
||||
#bl_width_max: float = 0.0
|
||||
|
||||
# Sockets
|
||||
_output_socket_methods: dict
|
||||
|
||||
input_sockets: dict[str, ct.schemas.SocketDef] = {}
|
||||
output_sockets: dict[str, ct.schemas.SocketDef] = {}
|
||||
input_socket_sets: dict[str, dict[str, ct.schemas.SocketDef]] = {}
|
||||
output_socket_sets: dict[str, dict[str, ct.schemas.SocketDef]] = {}
|
||||
|
||||
# Presets
|
||||
presets = {}
|
||||
|
||||
# Managed Objects
|
||||
managed_obj_defs: dict[ct.ManagedObjName, ct.schemas.ManagedObjDef] = {}
|
||||
|
||||
####################
|
||||
# - Initialization
|
||||
####################
|
||||
def __init_subclass__(cls, **kwargs: typ.Any):
|
||||
super().__init_subclass__(**kwargs)
|
||||
|
||||
# Setup Blender ID for Node
|
||||
if not hasattr(cls, "node_type"):
|
||||
msg = f"Node class {cls} does not define 'node_type', or it is does not have the type {ct.NodeType}"
|
||||
raise ValueError(msg)
|
||||
cls.bl_idname = str(cls.node_type.value)
|
||||
|
||||
# Setup Instance ID for Node
|
||||
cls.__annotations__["instance_id"] = bpy.props.StringProperty(
|
||||
name="Instance ID",
|
||||
description="The instance ID of a particular MaxwellSimNode instance, used to index caches",
|
||||
default="",
|
||||
)
|
||||
|
||||
# Setup Name Property for Node
|
||||
cls.__annotations__["sim_node_name"] = bpy.props.StringProperty(
|
||||
name="Sim Node Name",
|
||||
description="The name of a particular MaxwellSimNode node, which can be used to help identify data managed by the node",
|
||||
default="",
|
||||
update=(lambda self, context: self.sync_sim_node_name(context))
|
||||
)
|
||||
|
||||
# Setup Locked Property for Node
|
||||
cls.__annotations__["locked"] = bpy.props.BoolProperty(
|
||||
name="Locked State",
|
||||
description="The lock-state of a particular MaxwellSimNode instance, which determines the node's user editability",
|
||||
default=False,
|
||||
)
|
||||
|
||||
# Setup Blender Label for Node
|
||||
if not hasattr(cls, "bl_label"):
|
||||
msg = f"Node class {cls} does not define 'bl_label'"
|
||||
raise ValueError(msg)
|
||||
|
||||
# Setup Callback Methods
|
||||
cls._output_socket_methods = {
|
||||
method._index_by: method
|
||||
for attr_name in dir(cls)
|
||||
if hasattr(
|
||||
method := getattr(cls, attr_name),
|
||||
"_callback_type"
|
||||
) and method._callback_type == "computes_output_socket"
|
||||
}
|
||||
cls._on_value_changed_methods = {
|
||||
method
|
||||
for attr_name in dir(cls)
|
||||
if hasattr(
|
||||
method := getattr(cls, attr_name),
|
||||
"_callback_type"
|
||||
) and method._callback_type == "on_value_changed"
|
||||
}
|
||||
cls._on_show_preview = {
|
||||
method
|
||||
for attr_name in dir(cls)
|
||||
if hasattr(
|
||||
method := getattr(cls, attr_name),
|
||||
"_callback_type"
|
||||
) and method._callback_type == "on_show_preview"
|
||||
}
|
||||
cls._on_show_plot = {
|
||||
method
|
||||
for attr_name in dir(cls)
|
||||
if hasattr(
|
||||
method := getattr(cls, attr_name),
|
||||
"_callback_type"
|
||||
) and method._callback_type == "on_show_plot"
|
||||
}
|
||||
|
||||
# Setup Socket Set Dropdown
|
||||
if not len(cls.input_socket_sets) + len(cls.output_socket_sets) > 0:
|
||||
cls.active_socket_set = None
|
||||
else:
|
||||
## Add Active Socket Set Enum
|
||||
socket_set_names = (
|
||||
(_input_socket_set_names := list(cls.input_socket_sets.keys()))
|
||||
+ [
|
||||
output_socket_set_name
|
||||
for output_socket_set_name in cls.output_socket_sets.keys()
|
||||
if output_socket_set_name not in _input_socket_set_names
|
||||
]
|
||||
)
|
||||
socket_set_ids = [
|
||||
socket_set_name.replace(" ", "_").upper()
|
||||
for socket_set_name in socket_set_names
|
||||
]
|
||||
## TODO: Better deriv. of sock.set. ID, ex. ( is currently invalid.
|
||||
|
||||
## Add Active Socket Set Enum
|
||||
cls.__annotations__["active_socket_set"] = bpy.props.EnumProperty(
|
||||
name="Active Socket Set",
|
||||
description="The active socket set",
|
||||
items=[
|
||||
(
|
||||
socket_set_name,
|
||||
socket_set_name,
|
||||
socket_set_name,
|
||||
)
|
||||
for socket_set_id, socket_set_name in zip(
|
||||
socket_set_ids,
|
||||
socket_set_names,
|
||||
)
|
||||
],
|
||||
default=socket_set_names[0],
|
||||
update=(lambda self, _: self.sync_sockets()),
|
||||
)
|
||||
|
||||
# Setup Preset Dropdown
|
||||
if not cls.presets:
|
||||
cls.active_preset = None
|
||||
else:
|
||||
## TODO: Check that presets are represented in a socket that is guaranteed to be always available, specifically either a static socket or ALL static socket sets.
|
||||
cls.__annotations__["active_preset"] = bpy.props.EnumProperty(
|
||||
name="Active Preset",
|
||||
description="The active preset",
|
||||
items=[
|
||||
(
|
||||
preset_name,
|
||||
preset_def.label,
|
||||
preset_def.description,
|
||||
)
|
||||
for preset_name, preset_def in cls.presets.items()
|
||||
],
|
||||
default=list(cls.presets.keys())[0],
|
||||
update=lambda self, context: (
|
||||
self.sync_active_preset()()
|
||||
),
|
||||
)
|
||||
|
||||
####################
|
||||
# - Generic Properties
|
||||
####################
|
||||
def sync_sim_node_name(self, context):
|
||||
if (mobjs := CACHE[self.instance_id].get("managed_objs")) is None:
|
||||
return
|
||||
|
||||
for mobj_id, mobj in mobjs.items():
|
||||
# Retrieve Managed Obj Definition
|
||||
mobj_def = self.managed_obj_defs[mobj_id]
|
||||
|
||||
# Set Managed Obj Name
|
||||
mobj.name = mobj_def.name_prefix + self.sim_node_name
|
||||
## ManagedObj is allowed to alter the name when setting it.
|
||||
## - This will happen whenever the name is taken.
|
||||
## - If altered, set the 'sim_node_name' to the altered name.
|
||||
## - This will cause recursion, but only once.
|
||||
|
||||
####################
|
||||
# - Managed Object Properties
|
||||
####################
|
||||
@property
|
||||
def managed_objs(self):
|
||||
global CACHE
|
||||
if not CACHE.get(self.instance_id):
|
||||
CACHE[self.instance_id] = {}
|
||||
|
||||
# If No Managed Objects in CACHE: Initialize Managed Objects
|
||||
## - This happens on every ex. file load, init(), etc. .
|
||||
## - ManagedObjects MUST the same object by name.
|
||||
## - We sync our 'sim_node_name' with all managed objects.
|
||||
## - (There is also a class-defined 'name_prefix' to differentiate)
|
||||
## - See the 'sim_node_name' w/its sync function.
|
||||
if CACHE[self.instance_id].get("managed_objs") is None:
|
||||
# Initialize the Managed Object Instance Cache
|
||||
CACHE[self.instance_id]["managed_objs"] = {}
|
||||
|
||||
# Fill w/Managed Objects by Name Socket
|
||||
for mobj_id, mobj_def in self.managed_obj_defs.items():
|
||||
name = mobj_def.name_prefix + self.sim_node_name
|
||||
CACHE[self.instance_id]["managed_objs"][mobj_id] = (
|
||||
mobj_def.mk(name)
|
||||
)
|
||||
|
||||
return CACHE[self.instance_id]["managed_objs"]
|
||||
|
||||
return CACHE[self.instance_id]["managed_objs"]
|
||||
|
||||
####################
|
||||
# - Socket Properties
|
||||
####################
|
||||
def active_bl_sockets(self, direc: typx.Literal["input", "output"]):
|
||||
return self.inputs if direc == "input" else self.outputs
|
||||
|
||||
def active_socket_set_sockets(
|
||||
self,
|
||||
direc: typx.Literal["input", "output"],
|
||||
) -> dict:
|
||||
# No Active Socket Set: Return Nothing
|
||||
if not self.active_socket_set: return {}
|
||||
|
||||
# Retrieve Active Socket Set Sockets
|
||||
socket_sets = (
|
||||
self.input_socket_sets
|
||||
if direc == "input" else self.output_socket_sets
|
||||
)
|
||||
active_socket_set_sockets = socket_sets.get(
|
||||
self.active_socket_set
|
||||
)
|
||||
|
||||
# Return Active Socket Set Sockets (if any)
|
||||
if not active_socket_set_sockets: return {}
|
||||
return active_socket_set_sockets
|
||||
|
||||
def active_sockets(self, direc: typx.Literal["input", "output"]):
|
||||
static_sockets = (
|
||||
self.input_sockets
|
||||
if direc == "input"
|
||||
else self.output_sockets
|
||||
)
|
||||
socket_sets = (
|
||||
self.input_socket_sets
|
||||
if direc == "input"
|
||||
else self.output_socket_sets
|
||||
)
|
||||
loose_sockets = (
|
||||
self.loose_input_sockets
|
||||
if direc == "input"
|
||||
else self.loose_output_sockets
|
||||
)
|
||||
|
||||
return (
|
||||
static_sockets
|
||||
| self.active_socket_set_sockets(direc=direc)
|
||||
| loose_sockets
|
||||
)
|
||||
|
||||
####################
|
||||
# - Loose Sockets
|
||||
####################
|
||||
_DEFAULT_LOOSE_SOCKET_SER = json.dumps({
|
||||
"socket_names": [],
|
||||
"socket_def_names": [],
|
||||
"models": [],
|
||||
})
|
||||
# Loose Sockets
|
||||
## Only Blender props persist as instance data
|
||||
ser_loose_input_sockets: bpy.props.StringProperty(
|
||||
name="Serialized Loose Input Sockets",
|
||||
description="JSON-serialized representation of loose input sockets.",
|
||||
default=_DEFAULT_LOOSE_SOCKET_SER,
|
||||
)
|
||||
ser_loose_output_sockets: bpy.props.StringProperty(
|
||||
name="Serialized Loose Input Sockets",
|
||||
description="JSON-serialized representation of loose input sockets.",
|
||||
default=_DEFAULT_LOOSE_SOCKET_SER,
|
||||
)
|
||||
|
||||
## Internal Serialization/Deserialization Methods (yuck)
|
||||
def _ser_loose_sockets(self, deser: dict[str, ct.schemas.SocketDef]) -> str:
|
||||
if not all(isinstance(model, pyd.BaseModel) for model in deser.values()):
|
||||
msg = "Trying to deserialize loose sockets with invalid SocketDefs (they must be `pydantic` BaseModels)."
|
||||
raise ValueError(msg)
|
||||
|
||||
return json.dumps({
|
||||
"socket_names": list(deser.keys()),
|
||||
"socket_def_names": [
|
||||
model.__class__.__name__
|
||||
for model in deser.values()
|
||||
],
|
||||
"models": [
|
||||
model.model_dump()
|
||||
for model in deser.values()
|
||||
if isinstance(model, pyd.BaseModel)
|
||||
],
|
||||
}) ## Big reliance on order-preservation of dicts here.)
|
||||
def _deser_loose_sockets(self, ser: str) -> dict[str, ct.schemas.SocketDef]:
|
||||
semi_deser = json.loads(ser)
|
||||
return {
|
||||
socket_name: getattr(sockets, socket_def_name)(**model_kwargs)
|
||||
for socket_name, socket_def_name, model_kwargs in zip(
|
||||
semi_deser["socket_names"],
|
||||
semi_deser["socket_def_names"],
|
||||
semi_deser["models"],
|
||||
)
|
||||
if hasattr(sockets, socket_def_name)
|
||||
}
|
||||
|
||||
@property
|
||||
def loose_input_sockets(self) -> dict[str, ct.schemas.SocketDef]:
|
||||
return self._deser_loose_sockets(self.ser_loose_input_sockets)
|
||||
@property
|
||||
def loose_output_sockets(self) -> dict[str, ct.schemas.SocketDef]:
|
||||
return self._deser_loose_sockets(self.ser_loose_output_sockets)
|
||||
## TODO: Some caching may play a role if this is all too slow.
|
||||
|
||||
@loose_input_sockets.setter
|
||||
def loose_input_sockets(
|
||||
self, value: dict[str, ct.schemas.SocketDef],
|
||||
) -> None:
|
||||
self.ser_loose_input_sockets = self._ser_loose_sockets(value)
|
||||
|
||||
# Synchronize Sockets
|
||||
self.sync_sockets()
|
||||
## TODO: Perhaps re-init() all loose sockets anyway?
|
||||
|
||||
@loose_output_sockets.setter
|
||||
def loose_output_sockets(
|
||||
self, value: dict[str, ct.schemas.SocketDef],
|
||||
) -> None:
|
||||
self.ser_loose_output_sockets = self._ser_loose_sockets(value)
|
||||
|
||||
# Synchronize Sockets
|
||||
self.sync_sockets()
|
||||
## TODO: Perhaps re-init() all loose sockets anyway?
|
||||
|
||||
####################
|
||||
# - Socket Management
|
||||
####################
|
||||
def _prune_inactive_sockets(self):
|
||||
"""Remove all inactive sockets from the node.
|
||||
|
||||
**NOTE**: Socket names must be unique within direction, active socket set, and loose socket set.
|
||||
"""
|
||||
for direc in ["input", "output"]:
|
||||
sockets = self.active_sockets(direc)
|
||||
bl_sockets = self.active_bl_sockets(direc)
|
||||
|
||||
# Determine Sockets to Remove
|
||||
bl_sockets_to_remove = [
|
||||
bl_socket
|
||||
for socket_name, bl_socket in bl_sockets.items()
|
||||
if socket_name not in sockets
|
||||
]
|
||||
|
||||
# Remove Sockets
|
||||
for bl_socket in bl_sockets_to_remove:
|
||||
bl_sockets.remove(bl_socket)
|
||||
|
||||
def _add_new_active_sockets(self):
|
||||
"""Add and initialize all non-existing active sockets to the node.
|
||||
|
||||
Existing sockets within the given direction are not re-created.
|
||||
"""
|
||||
for direc in ["input", "output"]:
|
||||
sockets = self.active_sockets(direc)
|
||||
bl_sockets = self.active_bl_sockets(direc)
|
||||
|
||||
# Define BL Sockets
|
||||
created_sockets = {}
|
||||
for socket_name, socket_def in sockets.items():
|
||||
# Skip Existing Sockets
|
||||
if socket_name in bl_sockets: continue
|
||||
|
||||
# Create BL Socket from Socket
|
||||
bl_socket = bl_sockets.new(
|
||||
str(socket_def.socket_type.value),
|
||||
socket_name,
|
||||
)
|
||||
bl_socket.display_shape = bl_socket.socket_shape
|
||||
## `display_shape` needs to be dynamically set
|
||||
|
||||
# Record Created Socket
|
||||
created_sockets[socket_name] = socket_def
|
||||
|
||||
# Initialize Just-Created BL Sockets
|
||||
for socket_name, socket_def in created_sockets.items():
|
||||
socket_def.init(bl_sockets[socket_name])
|
||||
|
||||
def sync_sockets(self) -> None:
|
||||
"""Synchronize the node's sockets with the active sockets.
|
||||
|
||||
- Any non-existing active socket will be added and initialized.
|
||||
- Any existing active socket will not be changed.
|
||||
- Any existing inactive socket will be removed.
|
||||
|
||||
Must be called after any change to socket definitions, including loose
|
||||
sockets.
|
||||
"""
|
||||
self._prune_inactive_sockets()
|
||||
self._add_new_active_sockets()
|
||||
|
||||
####################
|
||||
# - Preset Management
|
||||
####################
|
||||
def sync_active_preset(self) -> None:
|
||||
"""Applies the active preset by overwriting the value of
|
||||
preset-defined input sockets.
|
||||
"""
|
||||
if not (preset_def := self.presets.get(self.active_preset)):
|
||||
msg = f"Tried to apply active preset, but the active preset ({self.active_preset}) is not in presets ({self.presets})"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
for socket_name, socket_value in preset_def.values.items():
|
||||
if not (bl_socket := self.inputs.get(socket_name)):
|
||||
msg = f"Tried to set preset socket/value pair ({socket_name}={socket_value}), but socket is not in active input sockets ({self.inputs})"
|
||||
raise ValueError(msg)
|
||||
|
||||
bl_socket.value = socket_value
|
||||
## TODO: Lazy-valued presets?
|
||||
|
||||
####################
|
||||
# - UI Methods
|
||||
####################
|
||||
def draw_buttons(
|
||||
self,
|
||||
context: bpy.types.Context,
|
||||
layout: bpy.types.UILayout,
|
||||
) -> None:
|
||||
if self.locked: layout.enabled = False
|
||||
|
||||
if self.active_preset:
|
||||
layout.prop(self, "active_preset", text="")
|
||||
|
||||
if self.active_socket_set:
|
||||
layout.prop(self, "active_socket_set", text="")
|
||||
|
||||
# Draw Name
|
||||
col = layout.column(align=False)
|
||||
if self.use_sim_node_name:
|
||||
row = col.row(align=True)
|
||||
row.label(text="", icon="EVENT_N")
|
||||
row.prop(self, "sim_node_name", text="")
|
||||
|
||||
# Draw Name
|
||||
self.draw_props(context, col)
|
||||
self.draw_operators(context, col)
|
||||
self.draw_info(context, col)
|
||||
|
||||
## TODO: Managed Operators instead of this shit
|
||||
def draw_props(self, context, layout): pass
|
||||
def draw_operators(self, context, layout): pass
|
||||
def draw_info(self, context, layout): pass
|
||||
|
||||
def draw_buttons_ext(self, context, layout): pass
|
||||
## TODO: Side panel buttons for fanciness.
|
||||
|
||||
def draw_plot_settings(self, context, layout):
|
||||
if self.locked: layout.enabled = False
|
||||
|
||||
####################
|
||||
# - Data Flow
|
||||
####################
|
||||
def _compute_input(
|
||||
self,
|
||||
input_socket_name: ct.SocketName,
|
||||
kind: ct.DataFlowKind = ct.DataFlowKind.Value,
|
||||
) -> typ.Any | None:
|
||||
"""Computes the data of an input socket, by socket name and data flow kind, by asking the socket nicely via `bl_socket.compute_data`.
|
||||
|
||||
Args:
|
||||
input_socket_name: The name of the input socket, as defined in
|
||||
`self.input_sockets`.
|
||||
kind: The data flow kind to compute retrieve.
|
||||
"""
|
||||
if not (bl_socket := self.inputs.get(input_socket_name)):
|
||||
return None
|
||||
#msg = f"Input socket name {input_socket_name} is not an active input sockets."
|
||||
#raise ValueError(msg)
|
||||
|
||||
return bl_socket.compute_data(kind=kind)
|
||||
|
||||
def compute_output(
|
||||
self,
|
||||
output_socket_name: ct.SocketName,
|
||||
kind: ct.DataFlowKind = ct.DataFlowKind.Value,
|
||||
) -> typ.Any:
|
||||
"""Computes the value of an output socket name, from its socket name.
|
||||
|
||||
Searches methods decorated with `@computes_output_socket(output_socket_name, kind=..., ...)`, for a perfect match to the pair `socket_name..kind`.
|
||||
This method is run to produce the value.
|
||||
|
||||
Args:
|
||||
output_socket_name: The name declaring the output socket,
|
||||
for which this method computes the output.
|
||||
|
||||
Returns:
|
||||
The value of the output socket, as computed by the dedicated method
|
||||
registered using the `@computes_output_socket` decorator.
|
||||
"""
|
||||
if not (
|
||||
output_socket_method := self._output_socket_methods.get(
|
||||
(output_socket_name, kind)
|
||||
)
|
||||
):
|
||||
msg = f"No output method for ({output_socket_name}, {str(kind.value)}"
|
||||
raise ValueError(msg)
|
||||
|
||||
return output_socket_method(self)
|
||||
|
||||
####################
|
||||
# - Action Chain
|
||||
####################
|
||||
def sync_prop(self, prop_name: str, context: bpy.types.Context):
|
||||
"""Called when a property has been updated.
|
||||
"""
|
||||
if not hasattr(self, prop_name):
|
||||
msg = f"Property {prop_name} not defined on socket {self}"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
self.trigger_action("value_changed", prop_name=prop_name)
|
||||
|
||||
def trigger_action(
|
||||
self,
|
||||
action: typx.Literal["enable_lock", "disable_lock", "value_changed", "show_preview", "show_plot"],
|
||||
socket_name: ct.SocketName | None = None,
|
||||
prop_name: ct.SocketName | None = None,
|
||||
) -> None:
|
||||
"""Reports that the input socket is changed.
|
||||
|
||||
Invalidates (recursively) the cache of any managed object or
|
||||
output socket method that implicitly depends on this input socket.
|
||||
"""
|
||||
# Forwards Chains
|
||||
if action == "value_changed":
|
||||
# Run User Callbacks
|
||||
## Careful with these, they run BEFORE propagation...
|
||||
## ...because later-chain methods may rely on the results of this.
|
||||
for method in self._on_value_changed_methods:
|
||||
if (
|
||||
socket_name
|
||||
and socket_name in method._extra_data.get("changed_sockets")
|
||||
) or (
|
||||
prop_name
|
||||
and prop_name in method._extra_data.get("changed_props")
|
||||
) or (
|
||||
socket_name
|
||||
and method._extra_data["changed_loose_input"]
|
||||
and socket_name in self.loose_input_sockets
|
||||
):
|
||||
method(self)
|
||||
|
||||
# Propagate via Output Sockets
|
||||
for bl_socket in self.active_bl_sockets("output"):
|
||||
bl_socket.trigger_action(action)
|
||||
|
||||
# Backwards Chains
|
||||
elif action == "enable_lock":
|
||||
self.locked = True
|
||||
|
||||
## Propagate via Input Sockets
|
||||
for bl_socket in self.active_bl_sockets("input"):
|
||||
bl_socket.trigger_action(action)
|
||||
|
||||
elif action == "disable_lock":
|
||||
self.locked = False
|
||||
|
||||
## Propagate via Input Sockets
|
||||
for bl_socket in self.active_bl_sockets("input"):
|
||||
bl_socket.trigger_action(action)
|
||||
|
||||
elif action == "show_preview":
|
||||
# Run User Callbacks
|
||||
for method in self._on_show_preview:
|
||||
method(self)
|
||||
|
||||
## Propagate via Input Sockets
|
||||
for bl_socket in self.active_bl_sockets("input"):
|
||||
bl_socket.trigger_action(action)
|
||||
|
||||
elif action == "show_plot":
|
||||
# Run User Callbacks
|
||||
## These shouldn't change any data, BUT...
|
||||
## ...because they can stop propagation, they should go first.
|
||||
for method in self._on_show_plot:
|
||||
method(self)
|
||||
if method._extra_data["stop_propagation"]:
|
||||
return
|
||||
|
||||
## Propagate via Input Sockets
|
||||
for bl_socket in self.active_bl_sockets("input"):
|
||||
bl_socket.trigger_action(action)
|
||||
|
||||
####################
|
||||
# - Blender Node Methods
|
||||
####################
|
||||
@classmethod
|
||||
def poll(cls, node_tree: bpy.types.NodeTree) -> bool:
|
||||
"""Run (by Blender) to determine instantiability.
|
||||
|
||||
Restricted to the MaxwellSimTreeType.
|
||||
"""
|
||||
|
||||
return node_tree.bl_idname == ct.TreeType.MaxwellSim.value
|
||||
|
||||
def init(self, context: bpy.types.Context):
|
||||
"""Run (by Blender) on node creation.
|
||||
"""
|
||||
global CACHE
|
||||
|
||||
# Initialize Cache and Instance ID
|
||||
self.instance_id = str(uuid.uuid4())
|
||||
CACHE[self.instance_id] = {}
|
||||
|
||||
# Initialize Name
|
||||
self.sim_node_name = self.name
|
||||
## Only shown in draw_buttons if 'self.use_sim_node_name'
|
||||
|
||||
# Initialize Sockets
|
||||
self.sync_sockets()
|
||||
|
||||
# Apply Default Preset
|
||||
if self.active_preset:
|
||||
self.sync_active_preset()
|
||||
|
||||
def update(self) -> None:
|
||||
pass
|
||||
|
||||
def free(self) -> None:
|
||||
"""Run (by Blender) when deleting the node.
|
||||
"""
|
||||
global CACHE
|
||||
if not CACHE.get(self.instance_id):
|
||||
CACHE[self.instance_id] = {}
|
||||
node_tree = self.id_data
|
||||
|
||||
# Free Managed Objects
|
||||
for managed_obj in self.managed_objs.values():
|
||||
managed_obj.free()
|
||||
|
||||
# Update NodeTree Caches
|
||||
## The NodeTree keeps caches to for optimized event triggering.
|
||||
## However, ex. deleted nodes also deletes links, without cache update.
|
||||
## By reporting that we're deleting the node, the cache stays happy.
|
||||
node_tree.sync_node_removed(self)
|
||||
|
||||
# Finally: Free Instance Cache
|
||||
if self.instance_id in CACHE:
|
||||
del CACHE[self.instance_id]
|
||||
|
||||
|
||||
|
||||
def chain_event_decorator(
|
||||
callback_type: typ.Literal[
|
||||
"computes_output_socket",
|
||||
"on_value_changed",
|
||||
"on_show_preview",
|
||||
"on_show_plot",
|
||||
],
|
||||
index_by: typ.Any | None = None,
|
||||
extra_data: dict[str, typ.Any] | None = None,
|
||||
|
||||
kind: ct.DataFlowKind = ct.DataFlowKind.Value,
|
||||
input_sockets: set[str] = set(), ## For now, presume
|
||||
output_sockets: set[str] = set(), ## For now, presume
|
||||
loose_input_sockets: bool = False,
|
||||
loose_output_sockets: bool = False,
|
||||
props: set[str] = set(),
|
||||
managed_objs: set[str] = set(),
|
||||
|
||||
req_params: set[str] = set()
|
||||
):
|
||||
def decorator(method: typ.Callable) -> typ.Callable:
|
||||
# Check Function Signature Validity
|
||||
func_sig = set(inspect.signature(method).parameters.keys())
|
||||
|
||||
## Too Little
|
||||
if func_sig != req_params and func_sig.issubset(req_params):
|
||||
msg = f"Decorated method {method.__name__} is missing arguments {req_params - func_sig}"
|
||||
|
||||
## Too Much
|
||||
if func_sig != req_params and func_sig.issuperset(req_params):
|
||||
msg = f"Decorated method {method.__name__} has superfluous arguments {func_sig - req_params}"
|
||||
raise ValueError(msg)
|
||||
|
||||
## Just Right :)
|
||||
|
||||
# TODO: Check Function Annotation Validity
|
||||
# - w/pydantic and/or socket capabilities
|
||||
|
||||
def decorated(node: MaxwellSimNode):
|
||||
# Assemble Keyword Arguments
|
||||
method_kw_args = {}
|
||||
|
||||
## Add Input Sockets
|
||||
if input_sockets:
|
||||
_input_sockets = {
|
||||
input_socket_name: node._compute_input(input_socket_name, kind)
|
||||
for input_socket_name in input_sockets
|
||||
}
|
||||
method_kw_args |= dict(input_sockets=_input_sockets)
|
||||
|
||||
## Add Output Sockets
|
||||
if output_sockets:
|
||||
_output_sockets = {
|
||||
output_socket_name: node.compute_output(output_socket_name, kind)
|
||||
for output_socket_name in output_sockets
|
||||
}
|
||||
method_kw_args |= dict(output_sockets=_output_sockets)
|
||||
|
||||
## Add Loose Sockets
|
||||
if loose_input_sockets:
|
||||
_loose_input_sockets = {
|
||||
input_socket_name: node._compute_input(input_socket_name, kind)
|
||||
for input_socket_name in node.loose_input_sockets
|
||||
}
|
||||
method_kw_args |= dict(
|
||||
loose_input_sockets=_loose_input_sockets
|
||||
)
|
||||
if loose_output_sockets:
|
||||
_loose_output_sockets = {
|
||||
output_socket_name: node.compute_output(output_socket_name, kind)
|
||||
for output_socket_name in node.loose_output_sockets
|
||||
}
|
||||
method_kw_args |= dict(
|
||||
loose_output_sockets=_loose_output_sockets
|
||||
)
|
||||
|
||||
## Add Props
|
||||
if props:
|
||||
_props = {
|
||||
prop_name: getattr(node, prop_name)
|
||||
for prop_name in props
|
||||
}
|
||||
method_kw_args |= dict(props=_props)
|
||||
|
||||
## Add Managed Object
|
||||
if managed_objs:
|
||||
_managed_objs = {
|
||||
managed_obj_name: node.managed_objs[managed_obj_name]
|
||||
for managed_obj_name in managed_objs
|
||||
}
|
||||
method_kw_args |= dict(managed_objs=_managed_objs)
|
||||
|
||||
# Call Method
|
||||
return method(
|
||||
node,
|
||||
**method_kw_args,
|
||||
)
|
||||
|
||||
# Set Attributes for Discovery
|
||||
decorated._callback_type = callback_type
|
||||
if index_by:
|
||||
decorated._index_by = index_by
|
||||
if extra_data:
|
||||
decorated._extra_data = extra_data
|
||||
|
||||
return decorated
|
||||
return decorator
|
||||
|
||||
|
||||
####################
|
||||
# - Decorator: Output Socket
|
||||
####################
|
||||
def computes_output_socket(
|
||||
output_socket_name: ct.SocketName,
|
||||
kind: ct.DataFlowKind = ct.DataFlowKind.Value,
|
||||
input_sockets: set[str] = set(),
|
||||
props: set[str] = set(),
|
||||
managed_objs: set[str] = set(),
|
||||
cacheable: bool = True,
|
||||
):
|
||||
"""Given a socket name, defines a function-that-makes-a-function (aka.
|
||||
decorator) which has the name of the socket attached.
|
||||
|
||||
Must be used as a decorator, ex. `@compute_output_socket("name")`.
|
||||
|
||||
Args:
|
||||
output_socket_name: The name of the output socket to attach the
|
||||
decorated method to.
|
||||
input_sockets: The values of these input sockets will be computed
|
||||
using `_compute_input`, then passed to the decorated function
|
||||
as `input_sockets: list[Any]`. If the input socket doesn't exist (ex. is contained in an inactive loose socket or socket set), then None is returned.
|
||||
managed_objs: These managed objects will be passed to the
|
||||
function as `managed_objs: list[Any]`.
|
||||
kind: Requests for this `output_socket_name, DataFlowKind` pair will
|
||||
be returned by the decorated function.
|
||||
cacheable: The output of th
|
||||
be returned by the decorated function.
|
||||
|
||||
Returns:
|
||||
The decorator, which takes the output-socket-computing method
|
||||
and returns a new output-socket-computing method, now annotated
|
||||
and discoverable by the `MaxwellSimTreeNode`.
|
||||
"""
|
||||
req_params = {"self"} | (
|
||||
{"input_sockets"} if input_sockets else set()
|
||||
) | (
|
||||
{"props"} if props else set()
|
||||
) | (
|
||||
{"managed_objs"} if managed_objs else set()
|
||||
)
|
||||
|
||||
return chain_event_decorator(
|
||||
callback_type="computes_output_socket",
|
||||
index_by=(output_socket_name, kind),
|
||||
kind=kind,
|
||||
input_sockets=input_sockets,
|
||||
props=props,
|
||||
managed_objs=managed_objs,
|
||||
req_params=req_params,
|
||||
)
|
||||
|
||||
|
||||
|
||||
####################
|
||||
# - Decorator: On Show Preview
|
||||
####################
|
||||
def on_value_changed(
|
||||
socket_name: set[ct.SocketName] | ct.SocketName | None = None,
|
||||
prop_name: set[str] | str | None = None,
|
||||
any_loose_input_socket: bool = False,
|
||||
|
||||
kind: ct.DataFlowKind = ct.DataFlowKind.Value,
|
||||
input_sockets: set[str] = set(),
|
||||
props: set[str] = set(),
|
||||
managed_objs: set[str] = set(),
|
||||
):
|
||||
if sum([
|
||||
int(socket_name is not None),
|
||||
int(prop_name is not None),
|
||||
int(any_loose_input_socket),
|
||||
]) > 1:
|
||||
msg = "Define only one of socket_name, prop_name or any_loose_input_socket"
|
||||
raise ValueError(msg)
|
||||
|
||||
req_params = {"self"} | (
|
||||
{"input_sockets"} if input_sockets else set()
|
||||
) | (
|
||||
{"loose_input_sockets"} if any_loose_input_socket else set()
|
||||
) | (
|
||||
{"props"} if props else set()
|
||||
) | (
|
||||
{"managed_objs"} if managed_objs else set()
|
||||
)
|
||||
|
||||
return chain_event_decorator(
|
||||
callback_type="on_value_changed",
|
||||
extra_data={
|
||||
"changed_sockets": (
|
||||
socket_name if isinstance(socket_name, set) else {socket_name}
|
||||
),
|
||||
"changed_props": (
|
||||
prop_name if isinstance(prop_name, set) else {prop_name}
|
||||
),
|
||||
"changed_loose_input": any_loose_input_socket,
|
||||
},
|
||||
kind=kind,
|
||||
input_sockets=input_sockets,
|
||||
loose_input_sockets=any_loose_input_socket,
|
||||
props=props,
|
||||
managed_objs=managed_objs,
|
||||
req_params=req_params,
|
||||
)
|
||||
|
||||
def on_show_preview(
|
||||
kind: ct.DataFlowKind = ct.DataFlowKind.Value,
|
||||
input_sockets: set[str] = set(), ## For now, presume only same kind
|
||||
output_sockets: set[str] = set(), ## For now, presume only same kind
|
||||
props: set[str] = set(),
|
||||
managed_objs: set[str] = set(),
|
||||
):
|
||||
req_params = {"self"} | (
|
||||
{"input_sockets"} if input_sockets else set()
|
||||
) | (
|
||||
{"output_sockets"} if output_sockets else set()
|
||||
) | (
|
||||
{"props"} if props else set()
|
||||
) | (
|
||||
{"managed_objs"} if managed_objs else set()
|
||||
)
|
||||
|
||||
return chain_event_decorator(
|
||||
callback_type="on_show_preview",
|
||||
kind=kind,
|
||||
input_sockets=input_sockets,
|
||||
output_sockets=output_sockets,
|
||||
props=props,
|
||||
managed_objs=managed_objs,
|
||||
req_params=req_params,
|
||||
)
|
||||
|
||||
def on_show_plot(
|
||||
kind: ct.DataFlowKind = ct.DataFlowKind.Value,
|
||||
input_sockets: set[str] = set(),
|
||||
output_sockets: set[str] = set(),
|
||||
props: set[str] = set(),
|
||||
managed_objs: set[str] = set(),
|
||||
stop_propagation: bool = False,
|
||||
):
|
||||
req_params = {"self"} | (
|
||||
{"input_sockets"} if input_sockets else set()
|
||||
) | (
|
||||
{"output_sockets"} if output_sockets else set()
|
||||
) | (
|
||||
{"props"} if props else set()
|
||||
) | (
|
||||
{"managed_objs"} if managed_objs else set()
|
||||
)
|
||||
|
||||
return chain_event_decorator(
|
||||
callback_type="on_show_plot",
|
||||
extra_data={
|
||||
"stop_propagation": stop_propagation,
|
||||
},
|
||||
kind=kind,
|
||||
input_sockets=input_sockets,
|
||||
output_sockets=output_sockets,
|
||||
props=props,
|
||||
managed_objs=managed_objs,
|
||||
req_params=req_params,
|
||||
)
|
|
@ -0,0 +1,11 @@
|
|||
from . import bound_box
|
||||
from . import bound_faces
|
||||
|
||||
BL_REGISTER = [
|
||||
*bound_box.BL_REGISTER,
|
||||
*bound_faces.BL_REGISTER,
|
||||
]
|
||||
BL_NODES = {
|
||||
**bound_box.BL_NODES,
|
||||
**bound_faces.BL_NODES,
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
import tidy3d as td
|
||||
import sympy as sp
|
||||
import sympy.physics.units as spu
|
||||
|
||||
from ... import contracts as ct
|
||||
from ... import sockets
|
||||
from .. import base
|
||||
|
||||
class BoundCondsNode(base.MaxwellSimNode):
|
||||
node_type = ct.NodeType.BoundConds
|
||||
bl_label = "Bound Box"
|
||||
#bl_icon = ...
|
||||
|
||||
####################
|
||||
# - Sockets
|
||||
####################
|
||||
input_sockets = {
|
||||
"+X": sockets.MaxwellBoundCondSocketDef(),
|
||||
"-X": sockets.MaxwellBoundCondSocketDef(),
|
||||
"+Y": sockets.MaxwellBoundCondSocketDef(),
|
||||
"-Y": sockets.MaxwellBoundCondSocketDef(),
|
||||
"+Z": sockets.MaxwellBoundCondSocketDef(),
|
||||
"-Z": sockets.MaxwellBoundCondSocketDef(),
|
||||
}
|
||||
output_sockets = {
|
||||
"BCs": sockets.MaxwellBoundCondsSocketDef(),
|
||||
}
|
||||
|
||||
####################
|
||||
# - Output Socket Computation
|
||||
####################
|
||||
@base.computes_output_socket(
|
||||
"BCs",
|
||||
input_sockets={"+X", "-X", "+Y", "-Y", "+Z", "-Z"}
|
||||
)
|
||||
def compute_simulation(self, input_sockets) -> td.BoundarySpec:
|
||||
x_pos = input_sockets["+X"]
|
||||
x_neg = input_sockets["-X"]
|
||||
y_pos = input_sockets["+Y"]
|
||||
y_neg = input_sockets["-Y"]
|
||||
z_pos = input_sockets["+Z"]
|
||||
z_neg = input_sockets["-Z"]
|
||||
|
||||
return td.BoundarySpec(
|
||||
x=td.Boundary(
|
||||
plus=x_pos,
|
||||
minus=x_neg,
|
||||
),
|
||||
y=td.Boundary(
|
||||
plus=y_pos,
|
||||
minus=y_neg,
|
||||
),
|
||||
z=td.Boundary(
|
||||
plus=z_pos,
|
||||
minus=z_neg,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = [
|
||||
BoundCondsNode,
|
||||
]
|
||||
BL_NODES = {
|
||||
ct.NodeType.BoundConds: (
|
||||
ct.NodeCategory.MAXWELLSIM_BOUNDS
|
||||
)
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
from . import pml_bound_face
|
||||
from . import pec_bound_face
|
||||
from . import pmc_bound_face
|
||||
|
||||
from . import bloch_bound_face
|
||||
from . import periodic_bound_face
|
||||
from . import absorbing_bound_face
|
||||
|
||||
BL_REGISTER = [
|
||||
*pml_bound_face.BL_REGISTER,
|
||||
*pec_bound_face.BL_REGISTER,
|
||||
*pmc_bound_face.BL_REGISTER,
|
||||
|
||||
*bloch_bound_face.BL_REGISTER,
|
||||
*periodic_bound_face.BL_REGISTER,
|
||||
*absorbing_bound_face.BL_REGISTER,
|
||||
]
|
||||
BL_NODES = {
|
||||
**pml_bound_face.BL_NODES,
|
||||
**pec_bound_face.BL_NODES,
|
||||
**pmc_bound_face.BL_NODES,
|
||||
|
||||
**bloch_bound_face.BL_NODES,
|
||||
**periodic_bound_face.BL_NODES,
|
||||
**absorbing_bound_face.BL_NODES,
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
|
@ -0,0 +1,5 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
|
@ -0,0 +1,5 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
|
@ -0,0 +1,5 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
|
@ -0,0 +1,5 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
|
@ -0,0 +1,5 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
|
@ -0,0 +1,26 @@
|
|||
from . import wave_constant
|
||||
#from . import unit_system
|
||||
|
||||
from . import constants
|
||||
|
||||
from . import web_importers
|
||||
#from . import file_importers
|
||||
|
||||
BL_REGISTER = [
|
||||
*wave_constant.BL_REGISTER,
|
||||
# *unit_system.BL_REGISTER,
|
||||
|
||||
*constants.BL_REGISTER,
|
||||
|
||||
*web_importers.BL_REGISTER,
|
||||
# *file_importers.BL_REGISTER,
|
||||
]
|
||||
BL_NODES = {
|
||||
**wave_constant.BL_NODES,
|
||||
# **unit_system.BL_NODES,
|
||||
|
||||
**constants.BL_NODES,
|
||||
|
||||
**web_importers.BL_NODES,
|
||||
# *file_importers.BL_REGISTER,
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
#from . import scientific_constant
|
||||
from . import number_constant
|
||||
#from . import physical_constant
|
||||
from . import blender_constant
|
||||
|
||||
BL_REGISTER = [
|
||||
# *scientific_constant.BL_REGISTER,
|
||||
*number_constant.BL_REGISTER,
|
||||
# *physical_constant.BL_REGISTER,
|
||||
*blender_constant.BL_REGISTER,
|
||||
]
|
||||
BL_NODES = {
|
||||
# **scientific_constant.BL_NODES,
|
||||
**number_constant.BL_NODES,
|
||||
# **physical_constant.BL_NODES,
|
||||
**blender_constant.BL_NODES,
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
import typing as typ
|
||||
|
||||
from .... import contracts as ct
|
||||
from .... import sockets
|
||||
from ... import base
|
||||
|
||||
class BlenderConstantNode(base.MaxwellSimNode):
|
||||
node_type = ct.NodeType.BlenderConstant
|
||||
bl_label = "Blender Constant"
|
||||
|
||||
input_socket_sets = {
|
||||
"Object": {
|
||||
"Value": sockets.BlenderObjectSocketDef(),
|
||||
},
|
||||
"Collection": {
|
||||
"Value": sockets.BlenderCollectionSocketDef(),
|
||||
},
|
||||
"Text": {
|
||||
"Value": sockets.BlenderTextSocketDef(),
|
||||
},
|
||||
"Image": {
|
||||
"Value": sockets.BlenderImageSocketDef(),
|
||||
},
|
||||
"GeoNode Tree": {
|
||||
"Value": sockets.BlenderGeoNodesSocketDef(),
|
||||
},
|
||||
}
|
||||
output_socket_sets = input_socket_sets
|
||||
|
||||
####################
|
||||
# - Callbacks
|
||||
####################
|
||||
@base.computes_output_socket(
|
||||
"Value",
|
||||
input_sockets={"Value"}
|
||||
)
|
||||
def compute_value(self, input_sockets) -> typ.Any:
|
||||
return input_sockets["Value"]
|
||||
|
||||
|
||||
|
||||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = [
|
||||
BlenderConstantNode,
|
||||
]
|
||||
BL_NODES = {
|
||||
ct.NodeType.BlenderConstant: (
|
||||
ct.NodeCategory.MAXWELLSIM_INPUTS_CONSTANTS
|
||||
)
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
import typing as typ
|
||||
|
||||
import bpy
|
||||
import sympy as sp
|
||||
|
||||
from .... import contracts as ct
|
||||
from .... import sockets
|
||||
from ... import base
|
||||
|
||||
class NumberConstantNode(base.MaxwellSimNode):
|
||||
node_type = ct.NodeType.NumberConstant
|
||||
bl_label = "Numerical Constant"
|
||||
|
||||
input_socket_sets = {
|
||||
"Integer": {
|
||||
"Value": sockets.IntegerNumberSocketDef(),
|
||||
},
|
||||
"Rational": {
|
||||
"Value": sockets.RationalNumberSocketDef(),
|
||||
},
|
||||
"Real": {
|
||||
"Value": sockets.RealNumberSocketDef(),
|
||||
},
|
||||
"Complex": {
|
||||
"Value": sockets.ComplexNumberSocketDef(),
|
||||
},
|
||||
}
|
||||
output_socket_sets = input_socket_sets
|
||||
|
||||
####################
|
||||
# - Callbacks
|
||||
####################
|
||||
@base.computes_output_socket(
|
||||
"Value",
|
||||
input_sockets={"Value"}
|
||||
)
|
||||
def compute_value(self, input_sockets) -> typ.Any:
|
||||
return input_sockets["Value"]
|
||||
|
||||
|
||||
|
||||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = [
|
||||
NumberConstantNode,
|
||||
]
|
||||
BL_NODES = {
|
||||
ct.NodeType.NumberConstant: (
|
||||
ct.NodeCategory.MAXWELLSIM_INPUTS_CONSTANTS
|
||||
)
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
import bpy
|
||||
import sympy as sp
|
||||
|
||||
from .... import contracts
|
||||
from .... import sockets
|
||||
from ... import base
|
||||
|
||||
class PhysicalConstantNode(base.MaxwellSimTreeNode):
|
||||
node_type = contracts.NodeType.PhysicalConstant
|
||||
|
||||
bl_label = "Physical Constant"
|
||||
#bl_icon = constants.ICON_SIM_INPUT
|
||||
|
||||
input_sockets = {}
|
||||
input_socket_sets = {
|
||||
"time": {
|
||||
"value": sockets.PhysicalTimeSocketDef(
|
||||
label="Time",
|
||||
),
|
||||
},
|
||||
"angle": {
|
||||
"value": sockets.PhysicalAngleSocketDef(
|
||||
label="Angle",
|
||||
),
|
||||
},
|
||||
"length": {
|
||||
"value": sockets.PhysicalLengthSocketDef(
|
||||
label="Length",
|
||||
),
|
||||
},
|
||||
"area": {
|
||||
"value": sockets.PhysicalAreaSocketDef(
|
||||
label="Area",
|
||||
),
|
||||
},
|
||||
"volume": {
|
||||
"value": sockets.PhysicalVolumeSocketDef(
|
||||
label="Volume",
|
||||
),
|
||||
},
|
||||
"point_3d": {
|
||||
"value": sockets.PhysicalPoint3DSocketDef(
|
||||
label="3D Point",
|
||||
),
|
||||
},
|
||||
"size_3d": {
|
||||
"value": sockets.PhysicalSize3DSocketDef(
|
||||
label="3D Size",
|
||||
),
|
||||
},
|
||||
## I got bored so maybe the rest later
|
||||
}
|
||||
output_sockets = {}
|
||||
output_socket_sets = input_socket_sets
|
||||
|
||||
####################
|
||||
# - Callbacks
|
||||
####################
|
||||
@base.computes_output_socket("value")
|
||||
def compute_value(self: contracts.NodeTypeProtocol) -> sp.Expr:
|
||||
return self.compute_input("value")
|
||||
|
||||
|
||||
|
||||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = [
|
||||
PhysicalConstantNode,
|
||||
]
|
||||
BL_NODES = {
|
||||
contracts.NodeType.PhysicalConstant: (
|
||||
contracts.NodeCategory.MAXWELLSIM_INPUTS_CONSTANTS
|
||||
)
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
|
@ -0,0 +1,43 @@
|
|||
import bpy
|
||||
import sympy as sp
|
||||
|
||||
from ... import contracts as ct
|
||||
from ... import sockets
|
||||
from .. import base
|
||||
|
||||
class PhysicalUnitSystemNode(base.MaxwellSimNode):
|
||||
node_type = ct.NodeType.UnitSystem
|
||||
bl_label = "Unit System"
|
||||
|
||||
input_sockets = {
|
||||
"Unit System": sockets.PhysicalUnitSystemSocketDef(
|
||||
show_by_default=True,
|
||||
),
|
||||
}
|
||||
output_sockets = {
|
||||
"Unit System": sockets.PhysicalUnitSystemSocketDef(),
|
||||
}
|
||||
|
||||
####################
|
||||
# - Callbacks
|
||||
####################
|
||||
@base.computes_output_socket(
|
||||
"Unit System",
|
||||
input_sockets = {"Unit System"},
|
||||
)
|
||||
def compute_value(self, input_sockets) -> dict:
|
||||
return input_sockets["Unit System"]
|
||||
|
||||
|
||||
|
||||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = [
|
||||
PhysicalUnitSystemNode,
|
||||
]
|
||||
BL_NODES = {
|
||||
ct.NodeType.UnitSystem: (
|
||||
ct.NodeCategory.MAXWELLSIM_INPUTS
|
||||
)
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
import bpy
|
||||
import sympy as sp
|
||||
import sympy.physics.units as spu
|
||||
import scipy as sc
|
||||
|
||||
from ... import contracts as ct
|
||||
from ... import sockets
|
||||
from .. import base
|
||||
|
||||
VAC_SPEED_OF_LIGHT = (
|
||||
sc.constants.speed_of_light
|
||||
* spu.meter/spu.second
|
||||
)
|
||||
|
||||
class WaveConstantNode(base.MaxwellSimNode):
|
||||
node_type = ct.NodeType.WaveConstant
|
||||
|
||||
bl_label = "Wave Constant"
|
||||
|
||||
input_socket_sets = {
|
||||
"Vacuum WL": {
|
||||
"WL": sockets.PhysicalLengthSocketDef(),
|
||||
},
|
||||
"Frequency": {
|
||||
"Freq": sockets.PhysicalFreqSocketDef(),
|
||||
},
|
||||
}
|
||||
output_sockets = {
|
||||
"WL": sockets.PhysicalLengthSocketDef(),
|
||||
"Freq": sockets.PhysicalFreqSocketDef(),
|
||||
}
|
||||
|
||||
####################
|
||||
# - Callbacks
|
||||
####################
|
||||
@base.computes_output_socket(
|
||||
"WL",
|
||||
kind=ct.DataFlowKind.Value,
|
||||
input_sockets={"WL", "Freq"},
|
||||
)
|
||||
def compute_vac_wl(self, input_sockets: dict) -> sp.Expr:
|
||||
if (vac_wl := input_sockets["WL"]):
|
||||
return vac_wl
|
||||
elif (freq := input_sockets["Freq"]):
|
||||
return spu.convert_to(
|
||||
VAC_SPEED_OF_LIGHT / freq,
|
||||
spu.meter,
|
||||
)
|
||||
|
||||
raise RuntimeError("Vac WL and Freq are both non-truthy")
|
||||
|
||||
@base.computes_output_socket(
|
||||
"Freq",
|
||||
input_sockets={"WL", "Freq"},
|
||||
)
|
||||
def compute_freq(self, input_sockets: dict) -> sp.Expr:
|
||||
if (vac_wl := input_sockets["WL"]):
|
||||
return spu.convert_to(
|
||||
VAC_SPEED_OF_LIGHT / vac_wl,
|
||||
spu.hertz,
|
||||
)
|
||||
elif (freq := input_sockets["Freq"]):
|
||||
return freq
|
||||
|
||||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = [
|
||||
WaveConstantNode,
|
||||
]
|
||||
BL_NODES = {
|
||||
ct.NodeType.WaveConstant: (
|
||||
ct.NodeCategory.MAXWELLSIM_INPUTS_CONSTANTS
|
||||
)
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
from . import tidy_3d_web_importer
|
||||
|
||||
BL_REGISTER = [
|
||||
*tidy_3d_web_importer.BL_REGISTER,
|
||||
]
|
||||
BL_NODES = {
|
||||
**tidy_3d_web_importer.BL_NODES,
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
import functools
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
import typing as typ
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
import sympy as sp
|
||||
import pydantic as pyd
|
||||
import tidy3d as td
|
||||
import tidy3d.web as _td_web
|
||||
|
||||
from ......utils.auth_td_web import g_td_web, is_td_web_authed
|
||||
from .... import contracts as ct
|
||||
from .... import sockets
|
||||
from ... import base
|
||||
|
||||
@functools.cache
|
||||
def task_status(task_id: str):
|
||||
task = _td_web.api.webapi.get_info(task_id)
|
||||
return task.status
|
||||
|
||||
####################
|
||||
# - Node
|
||||
####################
|
||||
class Tidy3DWebImporterNode(base.MaxwellSimNode):
|
||||
node_type = ct.NodeType.Tidy3DWebImporter
|
||||
bl_label = "Tidy3DWebImporter"
|
||||
|
||||
input_sockets = {
|
||||
"Cloud Task": sockets.Tidy3DCloudTaskSocketDef(
|
||||
task_exists=True,
|
||||
),
|
||||
}
|
||||
output_sockets = {}
|
||||
|
||||
####################
|
||||
# - UI
|
||||
####################
|
||||
def draw_info(self, context, layout): pass
|
||||
|
||||
####################
|
||||
# - Output Methods
|
||||
####################
|
||||
@base.computes_output_socket(
|
||||
"FDTD Sim",
|
||||
input_sockets={"Cloud Task"},
|
||||
)
|
||||
def compute_cloud_task(self, input_sockets: dict) -> str:
|
||||
if not isinstance(task_id := input_sockets["Cloud Task"], str):
|
||||
msg ="Input task does not exist"
|
||||
raise ValueError(msg)
|
||||
|
||||
# Load the Simulation
|
||||
td_web = g_td_web(None) ## Presume already auth'ed
|
||||
with tempfile.NamedTemporaryFile(delete=False) as f:
|
||||
_path_tmp = Path(f.name)
|
||||
_path_tmp.rename(f.name + ".json")
|
||||
path_tmp = Path(f.name + ".json")
|
||||
|
||||
cloud_sim = _td_web.api.webapi.load_simulation(
|
||||
task_id,
|
||||
path=str(path_tmp),
|
||||
)
|
||||
Path(path_tmp).unlink()
|
||||
|
||||
return cloud_sim
|
||||
|
||||
####################
|
||||
# - Update
|
||||
####################
|
||||
@base.on_value_changed(
|
||||
socket_name="Cloud Task",
|
||||
input_sockets={"Cloud Task"}
|
||||
)
|
||||
def on_value_changed__cloud_task(self, input_sockets: dict):
|
||||
task_status.cache_clear()
|
||||
if (
|
||||
(task_id := input_sockets["Cloud Task"]) is None
|
||||
or isinstance(task_id, dict)
|
||||
or task_status(task_id) != "success"
|
||||
or not is_td_web_authed
|
||||
):
|
||||
if self.loose_output_sockets: self.loose_output_sockets = {}
|
||||
return
|
||||
|
||||
td_web = g_td_web(None) ## Presume already auth'ed
|
||||
|
||||
self.loose_output_sockets = {
|
||||
"FDTD Sim": sockets.MaxwellFDTDSimSocketDef(),
|
||||
"FDTD Sim Data": sockets.AnySocketDef(),
|
||||
}
|
||||
|
||||
|
||||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = [
|
||||
Tidy3DWebImporterNode,
|
||||
]
|
||||
BL_NODES = {
|
||||
ct.NodeType.Tidy3DWebImporter: (
|
||||
ct.NodeCategory.MAXWELLSIM_INPUTS_IMPORTERS
|
||||
)
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
from pathlib import Path
|
||||
|
||||
import tidy3d as td
|
||||
import sympy as sp
|
||||
import sympy.physics.units as spu
|
||||
|
||||
from .. import contracts as ct
|
||||
from .. import sockets
|
||||
from . import base
|
||||
|
||||
class KitchenSinkNode(base.MaxwellSimNode):
|
||||
node_type = ct.NodeType.KitchenSink
|
||||
|
||||
bl_label = "Kitchen Sink"
|
||||
#bl_icon = ...
|
||||
|
||||
####################
|
||||
# - Sockets
|
||||
####################
|
||||
input_sockets = {
|
||||
"Static Data": sockets.AnySocketDef(),
|
||||
}
|
||||
input_socket_sets = {
|
||||
"Basic": {
|
||||
"Any": sockets.AnySocketDef(),
|
||||
"Bool": sockets.BoolSocketDef(),
|
||||
"FilePath": sockets.FilePathSocketDef(),
|
||||
"Text": sockets.TextSocketDef(),
|
||||
},
|
||||
"Number": {
|
||||
"Integer": sockets.IntegerNumberSocketDef(),
|
||||
"Rational": sockets.RationalNumberSocketDef(),
|
||||
"Real": sockets.RealNumberSocketDef(),
|
||||
"Complex": sockets.ComplexNumberSocketDef(),
|
||||
},
|
||||
"Vector": {
|
||||
"Real 2D": sockets.Real2DVectorSocketDef(),
|
||||
"Real 3D": sockets.Real3DVectorSocketDef(
|
||||
default_value=sp.Matrix([0.0, 0.0, 0.0])
|
||||
),
|
||||
"Complex 2D": sockets.Complex2DVectorSocketDef(),
|
||||
"Complex 3D": sockets.Complex3DVectorSocketDef(),
|
||||
},
|
||||
"Physical": {
|
||||
"Time": sockets.PhysicalTimeSocketDef(),
|
||||
#"physical_point_2d": sockets.PhysicalPoint2DSocketDef(),
|
||||
"Angle": sockets.PhysicalAngleSocketDef(),
|
||||
"Length": sockets.PhysicalLengthSocketDef(),
|
||||
"Area": sockets.PhysicalAreaSocketDef(),
|
||||
"Volume": sockets.PhysicalVolumeSocketDef(),
|
||||
"Point 3D": sockets.PhysicalPoint3DSocketDef(),
|
||||
##"physical_size_2d": sockets.PhysicalSize2DSocketDef(),
|
||||
"Size 3D": sockets.PhysicalSize3DSocketDef(),
|
||||
"Mass": sockets.PhysicalMassSocketDef(),
|
||||
"Speed": sockets.PhysicalSpeedSocketDef(),
|
||||
"Accel Scalar": sockets.PhysicalAccelScalarSocketDef(),
|
||||
"Force Scalar": sockets.PhysicalForceScalarSocketDef(),
|
||||
#"physical_accel_3dvector": sockets.PhysicalAccel3DVectorSocketDef(),
|
||||
##"physical_force_3dvector": sockets.PhysicalForce3DVectorSocketDef(),
|
||||
"Pol": sockets.PhysicalPolSocketDef(),
|
||||
"Freq": sockets.PhysicalFreqSocketDef(),
|
||||
},
|
||||
"Blender": {
|
||||
"Object": sockets.BlenderObjectSocketDef(),
|
||||
"Collection": sockets.BlenderCollectionSocketDef(),
|
||||
"Image": sockets.BlenderImageSocketDef(),
|
||||
"GeoNodes": sockets.BlenderGeoNodesSocketDef(),
|
||||
"Text": sockets.BlenderTextSocketDef(),
|
||||
},
|
||||
"Maxwell": {
|
||||
"Source": sockets.MaxwellSourceSocketDef(),
|
||||
"Temporal Shape": sockets.MaxwellTemporalShapeSocketDef(),
|
||||
"Medium": sockets.MaxwellMediumSocketDef(),
|
||||
"Medium Non-Linearity": sockets.MaxwellMediumNonLinearitySocketDef(),
|
||||
"Structure": sockets.MaxwellStructureSocketDef(),
|
||||
"Bound Box": sockets.MaxwellBoundBoxSocketDef(),
|
||||
"Bound Face": sockets.MaxwellBoundFaceSocketDef(),
|
||||
"Monitor": sockets.MaxwellMonitorSocketDef(),
|
||||
"FDTD Sim": sockets.MaxwellFDTDSimSocketDef(),
|
||||
"Sim Grid": sockets.MaxwellSimGridSocketDef(),
|
||||
"Sim Grid Axis": sockets.MaxwellSimGridAxisSocketDef(),
|
||||
},
|
||||
}
|
||||
|
||||
output_sockets = {
|
||||
"Static Data": sockets.AnySocketDef(),
|
||||
}
|
||||
output_socket_sets = input_socket_sets
|
||||
|
||||
|
||||
|
||||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = [
|
||||
KitchenSinkNode,
|
||||
]
|
||||
BL_NODES = {
|
||||
ct.NodeType.KitchenSink: (
|
||||
ct.NodeCategory.MAXWELLSIM_INPUTS
|
||||
)
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
from . import library_medium
|
||||
|
||||
#from . import pec_medium
|
||||
#from . import isotropic_medium
|
||||
#from . import anisotropic_medium
|
||||
#
|
||||
#from . import triple_sellmeier_medium
|
||||
#from . import sellmeier_medium
|
||||
#from . import pole_residue_medium
|
||||
#from . import drude_medium
|
||||
#from . import drude_lorentz_medium
|
||||
#from . import debye_medium
|
||||
#
|
||||
#from . import non_linearities
|
||||
|
||||
BL_REGISTER = [
|
||||
*library_medium.BL_REGISTER,
|
||||
|
||||
# *pec_medium.BL_REGISTER,
|
||||
# *isotropic_medium.BL_REGISTER,
|
||||
# *anisotropic_medium.BL_REGISTER,
|
||||
#
|
||||
# *triple_sellmeier_medium.BL_REGISTER,
|
||||
# *sellmeier_medium.BL_REGISTER,
|
||||
# *pole_residue_medium.BL_REGISTER,
|
||||
# *drude_medium.BL_REGISTER,
|
||||
# *drude_lorentz_medium.BL_REGISTER,
|
||||
# *debye_medium.BL_REGISTER,
|
||||
#
|
||||
# *non_linearities.BL_REGISTER,
|
||||
]
|
||||
BL_NODES = {
|
||||
**library_medium.BL_NODES,
|
||||
|
||||
# **pec_medium.BL_NODES,
|
||||
# **isotropic_medium.BL_NODES,
|
||||
# **anisotropic_medium.BL_NODES,
|
||||
#
|
||||
# **triple_sellmeier_medium.BL_NODES,
|
||||
# **sellmeier_medium.BL_NODES,
|
||||
# **pole_residue_medium.BL_NODES,
|
||||
# **drude_medium.BL_NODES,
|
||||
# **drude_lorentz_medium.BL_NODES,
|
||||
# **debye_medium.BL_NODES,
|
||||
#
|
||||
# **non_linearities.BL_NODES,
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
|
@ -0,0 +1,6 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
import tidy3d as td
|
||||
import sympy as sp
|
||||
import sympy.physics.units as spu
|
||||
|
||||
from ... import contracts
|
||||
from ... import sockets
|
||||
from .. import base
|
||||
|
||||
class DrudeLorentzMediumNode(base.MaxwellSimTreeNode):
|
||||
node_type = contracts.NodeType.DrudeLorentzMedium
|
||||
|
||||
bl_label = "Drude-Lorentz Medium"
|
||||
#bl_icon = ...
|
||||
|
||||
####################
|
||||
# - Sockets
|
||||
####################
|
||||
input_sockets = {
|
||||
"eps_inf": sockets.RealNumberSocketDef(
|
||||
label=f"εr_∞",
|
||||
),
|
||||
} | {
|
||||
f"del_eps{i}": sockets.RealNumberSocketDef(
|
||||
label=f"Δεr_{i}",
|
||||
)
|
||||
for i in [1, 2, 3]
|
||||
} | {
|
||||
f"f{i}": sockets.PhysicalFreqSocketDef(
|
||||
label=f"f_{i}",
|
||||
)
|
||||
for i in [1, 2, 3]
|
||||
} | {
|
||||
f"delta{i}": sockets.PhysicalFreqSocketDef(
|
||||
label=f"δ_{i}",
|
||||
)
|
||||
for i in [1, 2, 3]
|
||||
}
|
||||
output_sockets = {
|
||||
"medium": sockets.MaxwellMediumSocketDef(
|
||||
label="Medium"
|
||||
),
|
||||
}
|
||||
|
||||
####################
|
||||
# - Output Socket Computation
|
||||
####################
|
||||
@base.computes_output_socket("medium")
|
||||
def compute_medium(self: contracts.NodeTypeProtocol) -> td.Sellmeier:
|
||||
## Retrieval
|
||||
return td.Lorentz(
|
||||
eps_inf=self.compute_input(f"eps_inf"),
|
||||
coeffs = [
|
||||
(
|
||||
self.compute_input(f"del_eps{i}"),
|
||||
spu.convert_to(
|
||||
self.compute_input(f"f{i}"),
|
||||
spu.hertz,
|
||||
) / spu.hertz,
|
||||
spu.convert_to(
|
||||
self.compute_input(f"delta{i}"),
|
||||
spu.hertz,
|
||||
) / spu.hertz,
|
||||
)
|
||||
for i in [1, 2, 3]
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
|
||||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = [
|
||||
DrudeLorentzMediumNode,
|
||||
]
|
||||
BL_NODES = {
|
||||
contracts.NodeType.DrudeLorentzMedium: (
|
||||
contracts.NodeCategory.MAXWELLSIM_MEDIUMS
|
||||
)
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
|
@ -0,0 +1,6 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
import typing as typ
|
||||
import functools
|
||||
|
||||
import bpy
|
||||
import tidy3d as td
|
||||
import sympy as sp
|
||||
import sympy.physics.units as spu
|
||||
import numpy as np
|
||||
import scipy as sc
|
||||
|
||||
from .....utils import extra_sympy_units as spuex
|
||||
from ... import contracts as ct
|
||||
from ... import sockets
|
||||
from ... import managed_objs
|
||||
from .. import base
|
||||
|
||||
VAC_SPEED_OF_LIGHT = (
|
||||
sc.constants.speed_of_light
|
||||
* spu.meter/spu.second
|
||||
)
|
||||
|
||||
class LibraryMediumNode(base.MaxwellSimNode):
|
||||
node_type = ct.NodeType.LibraryMedium
|
||||
bl_label = "Library Medium"
|
||||
|
||||
####################
|
||||
# - Sockets
|
||||
####################
|
||||
input_sockets = {}
|
||||
output_sockets = {
|
||||
"Medium": sockets.MaxwellMediumSocketDef(),
|
||||
}
|
||||
|
||||
managed_obj_defs = {
|
||||
"nk_plot": ct.schemas.ManagedObjDef(
|
||||
mk=lambda name: managed_objs.ManagedBLImage(name),
|
||||
name_prefix="nkplot_",
|
||||
)
|
||||
}
|
||||
|
||||
####################
|
||||
# - Properties
|
||||
####################
|
||||
material: bpy.props.EnumProperty(
|
||||
name="",
|
||||
description="",
|
||||
#icon="NODE_MATERIAL",
|
||||
items=[
|
||||
(
|
||||
mat_key,
|
||||
td.material_library[mat_key].name,
|
||||
", ".join([
|
||||
ref.journal
|
||||
for ref in td.material_library[mat_key].variants[
|
||||
td.material_library[mat_key].default
|
||||
].reference
|
||||
])
|
||||
)
|
||||
for mat_key in td.material_library
|
||||
if mat_key != "graphene" ## For some reason, it's unique...
|
||||
],
|
||||
default="Au",
|
||||
update=(lambda self, context: self.sync_prop("material", context)),
|
||||
)
|
||||
|
||||
@property
|
||||
def freq_range_str(self) -> tuple[sp.Expr, sp.Expr]:
|
||||
## TODO: Cache (node instances don't seem able to keep data outside of properties, not even cached_property)
|
||||
mat = td.material_library[self.material]
|
||||
freq_range = [
|
||||
spu.convert_to(
|
||||
val * spu.hertz,
|
||||
spuex.terahertz,
|
||||
) / spuex.terahertz
|
||||
for val in mat.medium.frequency_range
|
||||
]
|
||||
return sp.pretty(
|
||||
[freq_range[0].n(4), freq_range[1].n(4)],
|
||||
use_unicode=True
|
||||
)
|
||||
|
||||
@property
|
||||
def nm_range_str(self) -> str:
|
||||
## TODO: Cache (node instances don't seem able to keep data outside of properties, not even cached_property)
|
||||
mat = td.material_library[self.material]
|
||||
nm_range = [
|
||||
spu.convert_to(
|
||||
VAC_SPEED_OF_LIGHT / (val * spu.hertz),
|
||||
spu.nanometer,
|
||||
) / spu.nanometer
|
||||
for val in reversed(mat.medium.frequency_range)
|
||||
]
|
||||
return sp.pretty(
|
||||
[nm_range[0].n(4), nm_range[1].n(4)],
|
||||
use_unicode=True
|
||||
)
|
||||
|
||||
####################
|
||||
# - UI
|
||||
####################
|
||||
def draw_props(self, context, layout):
|
||||
layout.prop(self, "material", text="")
|
||||
|
||||
def draw_info(self, context, col):
|
||||
# UI Drawing
|
||||
split = col.split(factor=0.23, align=True)
|
||||
|
||||
_col = split.column(align=True)
|
||||
_col.alignment = "LEFT"
|
||||
_col.label(text="nm")
|
||||
_col.label(text="THz")
|
||||
|
||||
_col = split.column(align=True)
|
||||
_col.alignment = "RIGHT"
|
||||
_col.label(text=self.nm_range_str)
|
||||
_col.label(text=self.freq_range_str)
|
||||
|
||||
####################
|
||||
# - Output Sockets
|
||||
####################
|
||||
@base.computes_output_socket("Medium")
|
||||
def compute_vac_wl(self) -> sp.Expr:
|
||||
return td.material_library[self.material].medium
|
||||
|
||||
####################
|
||||
# - Event Callbacks
|
||||
####################
|
||||
@base.on_show_plot(
|
||||
managed_objs={"nk_plot"},
|
||||
props={"material"},
|
||||
stop_propagation=True, ## Plot only the first plottable node
|
||||
)
|
||||
def on_show_plot(
|
||||
self,
|
||||
managed_objs: dict[str, ct.schemas.ManagedObj],
|
||||
props: dict[str, typ.Any],
|
||||
):
|
||||
medium = td.material_library[props["material"]].medium
|
||||
freq_range = [
|
||||
spu.convert_to(
|
||||
val * spu.hertz,
|
||||
spuex.terahertz,
|
||||
) / spu.hertz
|
||||
for val in medium.frequency_range
|
||||
]
|
||||
|
||||
managed_objs["nk_plot"].mpl_plot_to_image(
|
||||
lambda ax: medium.plot(medium.frequency_range, ax=ax),
|
||||
bl_select=True,
|
||||
)
|
||||
|
||||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = [
|
||||
LibraryMediumNode,
|
||||
]
|
||||
BL_NODES = {
|
||||
ct.NodeType.LibraryMedium: (
|
||||
ct.NodeCategory.MAXWELLSIM_MEDIUMS
|
||||
)
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
from . import add_non_linearity
|
||||
from . import chi_3_susceptibility_non_linearity
|
||||
from . import kerr_non_linearity
|
||||
from . import two_photon_absorption_non_linearity
|
||||
|
||||
BL_REGISTER = [
|
||||
*add_non_linearity.BL_REGISTER,
|
||||
*chi_3_susceptibility_non_linearity.BL_REGISTER,
|
||||
*kerr_non_linearity.BL_REGISTER,
|
||||
*two_photon_absorption_non_linearity.BL_REGISTER,
|
||||
]
|
||||
BL_NODES = {
|
||||
**add_non_linearity.BL_NODES,
|
||||
**chi_3_susceptibility_non_linearity.BL_NODES,
|
||||
**kerr_non_linearity.BL_NODES,
|
||||
**two_photon_absorption_non_linearity.BL_NODES,
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
|
@ -0,0 +1,5 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
|
@ -0,0 +1,5 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
|
@ -0,0 +1,5 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
|
@ -0,0 +1,6 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
import tidy3d as td
|
||||
import sympy as sp
|
||||
import sympy.physics.units as spu
|
||||
|
||||
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 = {
|
||||
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=spu.um**2
|
||||
)
|
||||
for i in [1, 2, 3]
|
||||
}
|
||||
output_sockets = {
|
||||
"medium": sockets.MaxwellMediumSocketDef(
|
||||
label="Medium"
|
||||
),
|
||||
}
|
||||
|
||||
####################
|
||||
# - Presets
|
||||
####################
|
||||
presets = {
|
||||
"BK7": contracts.PresetDef(
|
||||
label="BK7 Glass",
|
||||
description="Borosilicate crown glass (known as BK7)",
|
||||
values={
|
||||
"B1": 1.03961212,
|
||||
"B2": 0.231792344,
|
||||
"B3": 1.01046945,
|
||||
"C1": 6.00069867e-3 * 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")
|
||||
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]
|
||||
])
|
||||
|
||||
|
||||
|
||||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = [
|
||||
TripleSellmeierMediumNode,
|
||||
]
|
||||
BL_NODES = {
|
||||
contracts.NodeType.TripleSellmeierMedium: (
|
||||
contracts.NodeCategory.MAXWELLSIM_MEDIUMS
|
||||
)
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
from . import eh_field_monitor
|
||||
#from . import field_power_flux_monitor
|
||||
#from . import epsilon_tensor_monitor
|
||||
#from . import diffraction_monitor
|
||||
|
||||
BL_REGISTER = [
|
||||
*eh_field_monitor.BL_REGISTER,
|
||||
# *field_power_flux_monitor.BL_REGISTER,
|
||||
# *epsilon_tensor_monitor.BL_REGISTER,
|
||||
# *diffraction_monitor.BL_REGISTER,
|
||||
]
|
||||
BL_NODES = {
|
||||
**eh_field_monitor.BL_NODES,
|
||||
# **field_power_flux_monitor.BL_NODES,
|
||||
# **epsilon_tensor_monitor.BL_NODES,
|
||||
# **diffraction_monitor.BL_NODES,
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
import typing as typ
|
||||
import functools
|
||||
|
||||
import bpy
|
||||
import tidy3d as td
|
||||
import sympy as sp
|
||||
import sympy.physics.units as spu
|
||||
import numpy as np
|
||||
import scipy as sc
|
||||
|
||||
from .....utils import analyze_geonodes
|
||||
from .....utils import extra_sympy_units as spux
|
||||
from ... import contracts as ct
|
||||
from ... import sockets
|
||||
from ... import managed_objs
|
||||
from .. import base
|
||||
|
||||
GEONODES_MONITOR_BOX = "monitor_box"
|
||||
|
||||
class EHFieldMonitorNode(base.MaxwellSimNode):
|
||||
node_type = ct.NodeType.EHFieldMonitor
|
||||
bl_label = "E/H Field Monitor"
|
||||
use_sim_node_name = True
|
||||
|
||||
####################
|
||||
# - Sockets
|
||||
####################
|
||||
input_sockets = {
|
||||
"Rec Start": sockets.PhysicalTimeSocketDef(),
|
||||
"Rec Stop": sockets.PhysicalTimeSocketDef(
|
||||
default_value=200*spux.fs
|
||||
),
|
||||
"Center": sockets.PhysicalPoint3DSocketDef(),
|
||||
"Size": sockets.PhysicalSize3DSocketDef(),
|
||||
"Samples/Space": sockets.Integer3DVectorSocketDef(
|
||||
default_value=sp.Matrix([10, 10, 10])
|
||||
),
|
||||
"Samples/Time": sockets.IntegerNumberSocketDef(
|
||||
default_value=100,
|
||||
),
|
||||
}
|
||||
output_sockets = {
|
||||
"Monitor": sockets.MaxwellMonitorSocketDef(),
|
||||
}
|
||||
|
||||
managed_obj_defs = {
|
||||
"monitor_box": ct.schemas.ManagedObjDef(
|
||||
mk=lambda name: managed_objs.ManagedBLObject(name),
|
||||
name_prefix="",
|
||||
)
|
||||
}
|
||||
|
||||
####################
|
||||
# - Properties
|
||||
####################
|
||||
|
||||
####################
|
||||
# - UI
|
||||
####################
|
||||
def draw_props(self, context, layout):
|
||||
pass
|
||||
|
||||
def draw_info(self, context, col):
|
||||
pass
|
||||
|
||||
####################
|
||||
# - Output Sockets
|
||||
####################
|
||||
@base.computes_output_socket(
|
||||
"Monitor",
|
||||
input_sockets={
|
||||
"Rec Start", "Rec Stop", "Center", "Size", "Samples/Space",
|
||||
"Samples/Time",
|
||||
},
|
||||
props={"sim_node_name"}
|
||||
)
|
||||
def compute_monitor(self, input_sockets: dict, props: dict) -> td.FieldTimeMonitor:
|
||||
_rec_start = input_sockets["Rec Start"]
|
||||
_rec_stop = input_sockets["Rec Stop"]
|
||||
_center = input_sockets["Center"]
|
||||
_size = input_sockets["Size"]
|
||||
_samples_space = input_sockets["Samples/Space"]
|
||||
samples_time = input_sockets["Samples/Time"]
|
||||
|
||||
rec_start = spu.convert_to(_rec_start, spu.second) / spu.second
|
||||
rec_stop = spu.convert_to(_rec_stop, spu.second) / spu.second
|
||||
center = tuple(spu.convert_to(_center, spu.um) / spu.um)
|
||||
size = tuple(spu.convert_to(_size, spu.um) / spu.um)
|
||||
samples_space = tuple(_samples_space)
|
||||
|
||||
return td.FieldTimeMonitor(
|
||||
center=center,
|
||||
size=size,
|
||||
name=props["sim_node_name"],
|
||||
start=rec_start,
|
||||
stop=rec_stop,
|
||||
interval=samples_time,
|
||||
interval_space=samples_space,
|
||||
)
|
||||
|
||||
####################
|
||||
# - Preview - Changes to Input Sockets
|
||||
####################
|
||||
@base.on_value_changed(
|
||||
socket_name={"Center", "Size"},
|
||||
input_sockets={"Center", "Size"},
|
||||
managed_objs={"monitor_box"},
|
||||
)
|
||||
def on_value_changed__center_size(
|
||||
self,
|
||||
input_sockets: dict,
|
||||
managed_objs: dict[str, ct.schemas.ManagedObj],
|
||||
):
|
||||
_center = input_sockets["Center"]
|
||||
center = tuple([
|
||||
float(el)
|
||||
for el in spu.convert_to(_center, spu.um) / spu.um
|
||||
])
|
||||
|
||||
_size = input_sockets["Size"]
|
||||
size = tuple([
|
||||
float(el)
|
||||
for el in spu.convert_to(_size, spu.um) / spu.um
|
||||
])
|
||||
## TODO: Preview unit system?? Presume um for now
|
||||
|
||||
# Retrieve Hard-Coded GeoNodes and Analyze Input
|
||||
geo_nodes = bpy.data.node_groups[GEONODES_MONITOR_BOX]
|
||||
geonodes_interface = analyze_geonodes.interface(
|
||||
geo_nodes, direc="INPUT"
|
||||
)
|
||||
|
||||
# Sync Modifier Inputs
|
||||
managed_objs["monitor_box"].sync_geonodes_modifier(
|
||||
geonodes_node_group=geo_nodes,
|
||||
geonodes_identifier_to_value={
|
||||
geonodes_interface["Size"].identifier: size,
|
||||
## TODO: Use 'bl_socket_map.value_to_bl`!
|
||||
## - This accounts for auto-conversion, unit systems, etc. .
|
||||
## - We could keep it in the node base class...
|
||||
## - ...But it needs aligning with Blender, too. Hmm.
|
||||
}
|
||||
)
|
||||
|
||||
# Sync Object Position
|
||||
managed_objs["monitor_box"].bl_object("MESH").location = center
|
||||
|
||||
####################
|
||||
# - Preview - Show Preview
|
||||
####################
|
||||
@base.on_show_preview(
|
||||
managed_objs={"monitor_box"},
|
||||
)
|
||||
def on_show_preview(
|
||||
self,
|
||||
managed_objs: dict[str, ct.schemas.ManagedObj],
|
||||
):
|
||||
managed_objs["monitor_box"].show_preview("MESH")
|
||||
self.on_value_changed__center_size()
|
||||
|
||||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = [
|
||||
EHFieldMonitorNode,
|
||||
]
|
||||
BL_NODES = {
|
||||
ct.NodeType.EHFieldMonitor: (
|
||||
ct.NodeCategory.MAXWELLSIM_MONITORS
|
||||
)
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
from . import viewer
|
||||
from . import exporters
|
||||
|
||||
BL_REGISTER = [
|
||||
*viewer.BL_REGISTER,
|
||||
*exporters.BL_REGISTER,
|
||||
]
|
||||
BL_NODES = {
|
||||
**viewer.BL_NODES,
|
||||
**exporters.BL_NODES,
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
from . import json_file_exporter
|
||||
from . import tidy3d_web_exporter
|
||||
|
||||
BL_REGISTER = [
|
||||
*json_file_exporter.BL_REGISTER,
|
||||
*tidy3d_web_exporter.BL_REGISTER,
|
||||
]
|
||||
BL_NODES = {
|
||||
**json_file_exporter.BL_NODES,
|
||||
**tidy3d_web_exporter.BL_NODES,
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
import typing as typ
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
import sympy as sp
|
||||
import pydantic as pyd
|
||||
import tidy3d as td
|
||||
|
||||
from .... import contracts as ct
|
||||
from .... import sockets
|
||||
from ... import base
|
||||
|
||||
####################
|
||||
# - Operators
|
||||
####################
|
||||
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.MaxwellSimNode):
|
||||
node_type = ct.NodeType.JSONFileExporter
|
||||
|
||||
bl_label = "JSON File Exporter"
|
||||
#bl_icon = constants.ICON_SIM_INPUT
|
||||
|
||||
input_sockets = {
|
||||
"Data": sockets.AnySocketDef(),
|
||||
"JSON Path": sockets.FilePathSocketDef(
|
||||
default_path=Path("simulation.json")
|
||||
),
|
||||
"JSON Indent": sockets.IntegerNumberSocketDef(
|
||||
default_value=4,
|
||||
),
|
||||
}
|
||||
output_sockets = {
|
||||
"JSON String": sockets.StringSocketDef(),
|
||||
}
|
||||
|
||||
####################
|
||||
# - UI Layout
|
||||
####################
|
||||
def draw_operators(
|
||||
self,
|
||||
context: bpy.types.Context,
|
||||
layout: bpy.types.UILayout,
|
||||
) -> None:
|
||||
layout.operator(JSONFileExporterSaveJSON.bl_idname, text="Save JSON")
|
||||
|
||||
####################
|
||||
# - Methods
|
||||
####################
|
||||
def export_data_as_json(self) -> None:
|
||||
if (json_str := self.compute_output("JSON String")):
|
||||
data_dict = json.loads(json_str)
|
||||
with self._compute_input("JSON Path").open("w") as f:
|
||||
indent = self._compute_input("JSON Indent")
|
||||
json.dump(data_dict, f, ensure_ascii=False, indent=indent)
|
||||
|
||||
####################
|
||||
# - Output Sockets
|
||||
####################
|
||||
@base.computes_output_socket(
|
||||
"JSON String",
|
||||
input_sockets={"Data"},
|
||||
)
|
||||
def compute_json_string(self, input_sockets: dict[str, typ.Any]) -> str | None:
|
||||
if not (data := input_sockets["Data"]):
|
||||
return None
|
||||
|
||||
# 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()
|
||||
|
||||
else:
|
||||
json.dumps(data)
|
||||
|
||||
|
||||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = [
|
||||
JSONFileExporterSaveJSON,
|
||||
JSONFileExporterNode,
|
||||
]
|
||||
BL_NODES = {
|
||||
ct.NodeType.JSONFileExporter: (
|
||||
ct.NodeCategory.MAXWELLSIM_OUTPUTS_EXPORTERS
|
||||
)
|
||||
}
|
|
@ -0,0 +1,393 @@
|
|||
import json
|
||||
import tempfile
|
||||
import functools
|
||||
import typing as typ
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
import sympy as sp
|
||||
import pydantic as pyd
|
||||
import tidy3d as td
|
||||
import tidy3d.web as _td_web
|
||||
|
||||
from ......utils.auth_td_web import g_td_web, is_td_web_authed
|
||||
from .... import contracts as ct
|
||||
from .... import sockets
|
||||
from ... import base
|
||||
|
||||
####################
|
||||
# - Task Getters
|
||||
####################
|
||||
## TODO: We should probably refactor this setup.
|
||||
@functools.cache
|
||||
def estimated_task_cost(task_id: str):
|
||||
return _td_web.api.webapi.estimate_cost(task_id)
|
||||
|
||||
@functools.cache
|
||||
def billed_task_cost(task_id: str):
|
||||
return _td_web.api.webapi.real_cost(task_id)
|
||||
|
||||
@functools.cache
|
||||
def task_status(task_id: str):
|
||||
task = _td_web.api.webapi.get_info(task_id)
|
||||
return task.status
|
||||
|
||||
####################
|
||||
# - Progress Timer
|
||||
####################
|
||||
## TODO: We should probably refactor this too.
|
||||
class Tidy3DTaskStatusModalOperator(bpy.types.Operator):
|
||||
bl_idname = "blender_maxwell.tidy_3d_task_status_modal_operator"
|
||||
bl_label = "Tidy3D Task Status Modal Operator"
|
||||
|
||||
_timer = None
|
||||
_task_id = None
|
||||
_node = None
|
||||
_status = None
|
||||
_reported_done = False
|
||||
|
||||
def modal(self, context, event):
|
||||
# Retrieve New Status
|
||||
task_status.cache_clear()
|
||||
new_status = task_status(self._task_id)
|
||||
if new_status != self._status:
|
||||
task_status.cache_clear()
|
||||
self._status = new_status
|
||||
|
||||
# Check Done Status
|
||||
if self._status in {"success", "error"}:
|
||||
# Report Done
|
||||
if not self._reported_done:
|
||||
self._node.trigger_action("value_changed")
|
||||
self._reported_done = True
|
||||
|
||||
# Finish when Billing is Known
|
||||
if not billed_task_cost(self._task_id):
|
||||
billed_task_cost.cache_clear()
|
||||
else:
|
||||
return {'FINISHED'}
|
||||
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
def execute(self, context):
|
||||
node = context.node
|
||||
wm = context.window_manager
|
||||
|
||||
self._timer = wm.event_timer_add(2.0, window=context.window)
|
||||
self._task_id = node.uploaded_task_id
|
||||
self._node = node
|
||||
self._status = task_status(self._task_id)
|
||||
|
||||
wm.modal_handler_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
####################
|
||||
# - Web Uploader / Loader / Runner / Releaser
|
||||
####################
|
||||
## TODO: We should probably refactor this too.
|
||||
class Tidy3DWebUploadOperator(bpy.types.Operator):
|
||||
bl_idname = "blender_maxwell.tidy_3d_web_upload_operator"
|
||||
bl_label = "Tidy3D Web Upload Operator"
|
||||
bl_description = "Upload the attached (locked) simulation, such that it is ready to run on the Tidy3D cloud"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
space = context.space_data
|
||||
return (
|
||||
space.type == 'NODE_EDITOR'
|
||||
and space.node_tree is not None
|
||||
and space.node_tree.bl_idname == "MaxwellSimTreeType"
|
||||
and is_td_web_authed()
|
||||
and hasattr(context, "node")
|
||||
and context.node.lock_tree
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
node = context.node
|
||||
node.web_upload()
|
||||
return {'FINISHED'}
|
||||
|
||||
class Tidy3DLoadUploadedOperator(bpy.types.Operator):
|
||||
bl_idname = "blender_maxwell.tidy_3d_load_uploaded_operator"
|
||||
bl_label = "Tidy3D Load Uploaded Operator"
|
||||
bl_description = "Load an already-uploaded simulation, as selected in the dropdown of the 'Cloud Task' socket"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
space = context.space_data
|
||||
return (
|
||||
space.type == 'NODE_EDITOR'
|
||||
and space.node_tree is not None
|
||||
and space.node_tree.bl_idname == "MaxwellSimTreeType"
|
||||
and is_td_web_authed()
|
||||
and hasattr(context, "node")
|
||||
and context.node.lock_tree
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
node = context.node
|
||||
node.load_uploaded_task()
|
||||
|
||||
# Load Simulation to Compare
|
||||
## Load Local Sim
|
||||
local_sim = node._compute_input("FDTD Sim")
|
||||
|
||||
## Load Cloud Sim
|
||||
task_id = node.compute_output("Cloud Task")
|
||||
with tempfile.NamedTemporaryFile(delete=False) as f:
|
||||
_path_tmp = Path(f.name)
|
||||
_path_tmp.rename(f.name + ".json")
|
||||
path_tmp = Path(f.name + ".json")
|
||||
cloud_sim = _td_web.api.webapi.load_simulation(task_id, path=str(path_tmp))
|
||||
|
||||
Path(path_tmp).unlink()
|
||||
|
||||
## Compare
|
||||
if local_sim != cloud_sim:
|
||||
node.release_uploaded_task()
|
||||
msg = "Loaded simulation doesn't match input simulation"
|
||||
raise ValueError(msg)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
class RunUploadedTidy3DSim(bpy.types.Operator):
|
||||
bl_idname = "blender_maxwell.run_uploaded_tidy_3d_sim"
|
||||
bl_label = "Run Uploaded Tidy3D Sim"
|
||||
bl_description = "Run the currently uploaded (and loaded) simulation"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
space = context.space_data
|
||||
return (
|
||||
space.type == 'NODE_EDITOR'
|
||||
and space.node_tree is not None
|
||||
and space.node_tree.bl_idname == "MaxwellSimTreeType"
|
||||
and is_td_web_authed()
|
||||
and hasattr(context, "node")
|
||||
and context.node.lock_tree
|
||||
and context.node.uploaded_task_id
|
||||
and task_status(context.node.uploaded_task_id) == "draft"
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
node = context.node
|
||||
node.run_uploaded_task()
|
||||
bpy.ops.blender_maxwell.tidy_3d_task_status_modal_operator()
|
||||
return {'FINISHED'}
|
||||
|
||||
class ReleaseTidy3DExportOperator(bpy.types.Operator):
|
||||
bl_idname = "blender_maxwell.release_tidy_3d_export_operator"
|
||||
bl_label = "Release Tidy3D Export Operator"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
space = context.space_data
|
||||
return (
|
||||
space.type == 'NODE_EDITOR'
|
||||
and space.node_tree is not None
|
||||
and space.node_tree.bl_idname == "MaxwellSimTreeType"
|
||||
and is_td_web_authed()
|
||||
and hasattr(context, "node")
|
||||
and context.node.lock_tree
|
||||
and context.node.uploaded_task_id
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
node = context.node
|
||||
node.release_uploaded_task()
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
|
||||
####################
|
||||
# - Web Exporter Node
|
||||
####################
|
||||
class Tidy3DWebExporterNode(base.MaxwellSimNode):
|
||||
node_type = ct.NodeType.Tidy3DWebExporter
|
||||
bl_label = "Tidy3DWebExporter"
|
||||
|
||||
input_sockets = {
|
||||
"FDTD Sim": sockets.MaxwellFDTDSimSocketDef(),
|
||||
"Cloud Task": sockets.Tidy3DCloudTaskSocketDef(
|
||||
task_exists=False,
|
||||
),
|
||||
}
|
||||
output_sockets = {
|
||||
"Cloud Task": sockets.Tidy3DCloudTaskSocketDef(
|
||||
task_exists=True,
|
||||
),
|
||||
}
|
||||
|
||||
lock_tree: bpy.props.BoolProperty(
|
||||
name="Whether to lock the attached tree",
|
||||
description="Whether or not to lock the attached tree",
|
||||
default=False,
|
||||
update=(lambda self, context: self.sync_lock_tree(context)),
|
||||
)
|
||||
uploaded_task_id: bpy.props.StringProperty(
|
||||
name="Uploaded Task ID",
|
||||
description="The uploaded task ID",
|
||||
default="",
|
||||
)
|
||||
|
||||
####################
|
||||
# - Sync Methods
|
||||
####################
|
||||
def sync_lock_tree(self, context):
|
||||
node_tree = self.id_data
|
||||
|
||||
if self.lock_tree:
|
||||
self.trigger_action("enable_lock")
|
||||
self.locked = False
|
||||
for bl_socket in self.inputs:
|
||||
if bl_socket.name == "FDTD Sim": continue
|
||||
bl_socket.locked = False
|
||||
|
||||
else:
|
||||
self.trigger_action("disable_lock")
|
||||
|
||||
####################
|
||||
# - Output Socket Callbacks
|
||||
####################
|
||||
def web_upload(self):
|
||||
if not (sim := self._compute_input("FDTD Sim")):
|
||||
raise ValueError("Must attach simulation")
|
||||
|
||||
if not (new_task_dict := self._compute_input("Cloud Task")):
|
||||
raise ValueError("No valid cloud task defined")
|
||||
|
||||
td_web = g_td_web(None) ## Presume already auth'ed
|
||||
|
||||
self.uploaded_task_id = td_web.api.webapi.upload(
|
||||
sim,
|
||||
**new_task_dict,
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
self.inputs["Cloud Task"].sync_task_loaded(self.uploaded_task_id)
|
||||
|
||||
def load_uploaded_task(self):
|
||||
self.inputs["Cloud Task"].sync_task_loaded(None)
|
||||
self.uploaded_task_id = self._compute_input("Cloud Task")
|
||||
|
||||
self.trigger_action("value_changed")
|
||||
|
||||
def run_uploaded_task(self):
|
||||
td_web = g_td_web(None) ## Presume already auth'ed
|
||||
td_web.api.webapi.start(self.uploaded_task_id)
|
||||
|
||||
self.trigger_action("value_changed")
|
||||
|
||||
def release_uploaded_task(self):
|
||||
self.uploaded_task_id = ""
|
||||
self.inputs["Cloud Task"].sync_task_released(specify_new_task=True)
|
||||
|
||||
self.trigger_action("value_changed")
|
||||
|
||||
####################
|
||||
# - UI
|
||||
####################
|
||||
def draw_operators(self, context, layout):
|
||||
is_authed = is_td_web_authed()
|
||||
has_uploaded_task_id = bool(self.uploaded_task_id)
|
||||
|
||||
# Row: Run Simulation
|
||||
row = layout.row(align=True)
|
||||
if has_uploaded_task_id: row.enabled = False
|
||||
row.operator(
|
||||
Tidy3DWebUploadOperator.bl_idname,
|
||||
text="Upload Sim",
|
||||
)
|
||||
tree_lock_icon = "LOCKED" if self.lock_tree else "UNLOCKED"
|
||||
row.prop(self, "lock_tree", toggle=True, icon=tree_lock_icon, text="")
|
||||
|
||||
# Row: Run Simulation
|
||||
row = layout.row(align=True)
|
||||
if is_authed and has_uploaded_task_id:
|
||||
run_sim_text = f"Run Sim (~{estimated_task_cost(self.uploaded_task_id):.3f} credits)"
|
||||
else:
|
||||
run_sim_text = f"Run Sim"
|
||||
|
||||
row.operator(
|
||||
RunUploadedTidy3DSim.bl_idname,
|
||||
text=run_sim_text,
|
||||
)
|
||||
if has_uploaded_task_id:
|
||||
tree_lock_icon = "LOOP_BACK"
|
||||
row.operator(
|
||||
ReleaseTidy3DExportOperator.bl_idname,
|
||||
icon="LOOP_BACK",
|
||||
text="",
|
||||
)
|
||||
else:
|
||||
row.operator(
|
||||
Tidy3DLoadUploadedOperator.bl_idname,
|
||||
icon="TRIA_UP_BAR",
|
||||
text="",
|
||||
)
|
||||
|
||||
# Row: Simulation Progress
|
||||
if is_authed and has_uploaded_task_id:
|
||||
progress = {
|
||||
"draft": (0.0, "Waiting to Run..."),
|
||||
"initialized": (0.0, "Initializing..."),
|
||||
"queued": (0.0, "Queued..."),
|
||||
"preprocessing": (0.05, "Pre-processing..."),
|
||||
"running": (0.2, "Running..."),
|
||||
"postprocess": (0.85, "Post-processing..."),
|
||||
"success": (1.0, f"Success (={billed_task_cost(self.uploaded_task_id)} credits)"),
|
||||
"error": (1.0, f"Error (={billed_task_cost(self.uploaded_task_id)} credits)"),
|
||||
}[task_status(self.uploaded_task_id)]
|
||||
|
||||
layout.separator()
|
||||
row = layout.row(align=True)
|
||||
row.progress(
|
||||
factor=progress[0],
|
||||
type="BAR",
|
||||
text=progress[1],
|
||||
)
|
||||
|
||||
####################
|
||||
# - Output Methods
|
||||
####################
|
||||
@base.computes_output_socket(
|
||||
"Cloud Task",
|
||||
input_sockets={"Cloud Task"},
|
||||
)
|
||||
def compute_cloud_task(self, input_sockets: dict) -> str | None:
|
||||
if self.uploaded_task_id: return self.uploaded_task_id
|
||||
return None
|
||||
|
||||
####################
|
||||
# - Update
|
||||
####################
|
||||
@base.on_value_changed(socket_name="FDTD Sim")
|
||||
def on_value_changed__fdtd_sim(self):
|
||||
estimated_task_cost.cache_clear()
|
||||
task_status.cache_clear()
|
||||
billed_task_cost.cache_clear()
|
||||
|
||||
@base.on_value_changed(socket_name="Cloud Task")
|
||||
def on_value_changed__cloud_task(self):
|
||||
estimated_task_cost.cache_clear()
|
||||
task_status.cache_clear()
|
||||
billed_task_cost.cache_clear()
|
||||
|
||||
|
||||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = [
|
||||
Tidy3DWebUploadOperator,
|
||||
Tidy3DTaskStatusModalOperator,
|
||||
RunUploadedTidy3DSim,
|
||||
Tidy3DLoadUploadedOperator,
|
||||
ReleaseTidy3DExportOperator,
|
||||
Tidy3DWebExporterNode,
|
||||
]
|
||||
BL_NODES = {
|
||||
ct.NodeType.Tidy3DWebExporter: (
|
||||
ct.NodeCategory.MAXWELLSIM_OUTPUTS_EXPORTERS
|
||||
)
|
||||
}
|
|
@ -0,0 +1,169 @@
|
|||
import functools
|
||||
import typing as typ
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
import sympy as sp
|
||||
import pydantic as pyd
|
||||
import tidy3d as td
|
||||
|
||||
from ... import contracts as ct
|
||||
from ... import sockets
|
||||
from .. import base
|
||||
from ...managed_objs import managed_bl_object
|
||||
|
||||
|
||||
class ConsoleViewOperator(bpy.types.Operator):
|
||||
bl_idname = "blender_maxwell.console_view_operator"
|
||||
bl_label = "View Plots"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
node = context.node
|
||||
node.print_data_to_console()
|
||||
return {'FINISHED'}
|
||||
|
||||
class RefreshPlotViewOperator(bpy.types.Operator):
|
||||
bl_idname = "blender_maxwell.refresh_plot_view_operator"
|
||||
bl_label = "Refresh Plots"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
node = context.node
|
||||
node.trigger_action("value_changed", "Data")
|
||||
return {'FINISHED'}
|
||||
|
||||
####################
|
||||
# - Node
|
||||
####################
|
||||
class ViewerNode(base.MaxwellSimNode):
|
||||
node_type = ct.NodeType.Viewer
|
||||
bl_label = "Viewer"
|
||||
|
||||
input_sockets = {
|
||||
"Data": sockets.AnySocketDef(),
|
||||
}
|
||||
|
||||
####################
|
||||
# - Properties
|
||||
####################
|
||||
auto_plot: bpy.props.BoolProperty(
|
||||
name="Auto-Plot",
|
||||
description="Whether to auto-plot anything plugged into the viewer node",
|
||||
default=False,
|
||||
update=lambda self, context: self.sync_prop("auto_plot", context),
|
||||
)
|
||||
|
||||
auto_3d_preview: bpy.props.BoolProperty(
|
||||
name="Auto 3D Preview",
|
||||
description="Whether to auto-preview anything 3D, that's plugged into the viewer node",
|
||||
default=False,
|
||||
update=lambda self, context: self.sync_prop("auto_3d_preview", context),
|
||||
)
|
||||
|
||||
####################
|
||||
# - UI
|
||||
####################
|
||||
def draw_operators(self, context, layout):
|
||||
split = layout.split(factor=0.4)
|
||||
|
||||
# Split LHS
|
||||
col = split.column(align=False)
|
||||
col.label(text="Console")
|
||||
col.label(text="Plot")
|
||||
col.label(text="3D")
|
||||
|
||||
# Split RHS
|
||||
col = split.column(align=False)
|
||||
|
||||
## Console Options
|
||||
col.operator(ConsoleViewOperator.bl_idname, text="Print")
|
||||
|
||||
## Plot Options
|
||||
row = col.row(align=True)
|
||||
row.prop(self, "auto_plot", text="Plot", toggle=True)
|
||||
row.operator(
|
||||
RefreshPlotViewOperator.bl_idname,
|
||||
text="",
|
||||
icon="FILE_REFRESH",
|
||||
)
|
||||
|
||||
## 3D Preview Options
|
||||
row = col.row(align=True)
|
||||
row.prop(self, "auto_3d_preview", text="3D Preview", toggle=True)
|
||||
|
||||
####################
|
||||
# - Methods
|
||||
####################
|
||||
def print_data_to_console(self):
|
||||
if not (data := self._compute_input("Data")):
|
||||
return
|
||||
|
||||
if isinstance(data, sp.Basic):
|
||||
sp.pprint(data, use_unicode=True)
|
||||
|
||||
print(str(data))
|
||||
|
||||
####################
|
||||
# - Updates
|
||||
####################
|
||||
@base.on_value_changed(
|
||||
socket_name="Data",
|
||||
props={"auto_3d_preview"},
|
||||
)
|
||||
def on_value_changed__data(self, props):
|
||||
# Show Plot
|
||||
## Don't have to un-show other plots.
|
||||
if self.auto_plot:
|
||||
self.trigger_action("show_plot")
|
||||
|
||||
# Remove Anything Previewed
|
||||
preview_collection = managed_bl_object.bl_collection(
|
||||
managed_bl_object.PREVIEW_COLLECTION_NAME,
|
||||
view_layer_exclude=False,
|
||||
)
|
||||
for bl_object in preview_collection.objects.values():
|
||||
preview_collection.objects.unlink(bl_object)
|
||||
|
||||
# Preview Anything that Should be Previewed (maybe)
|
||||
if props["auto_3d_preview"]:
|
||||
self.trigger_action("show_preview")
|
||||
|
||||
@base.on_value_changed(
|
||||
prop_name="auto_3d_preview",
|
||||
props={"auto_3d_preview"},
|
||||
)
|
||||
def on_value_changed__auto_3d_preview(self, props):
|
||||
# Remove Anything Previewed
|
||||
preview_collection = managed_bl_object.bl_collection(
|
||||
managed_bl_object.PREVIEW_COLLECTION_NAME,
|
||||
view_layer_exclude=False,
|
||||
)
|
||||
for bl_object in preview_collection.objects.values():
|
||||
preview_collection.objects.unlink(bl_object)
|
||||
|
||||
# Preview Anything that Should be Previewed (maybe)
|
||||
if props["auto_3d_preview"]:
|
||||
self.trigger_action("show_preview")
|
||||
|
||||
|
||||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = [
|
||||
ConsoleViewOperator,
|
||||
RefreshPlotViewOperator,
|
||||
ViewerNode,
|
||||
]
|
||||
BL_NODES = {
|
||||
ct.NodeType.Viewer: (
|
||||
ct.NodeCategory.MAXWELLSIM_OUTPUTS
|
||||
)
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
from . import sim_domain
|
||||
|
||||
#from . import sim_grid
|
||||
#from . import sim_grid_axes
|
||||
|
||||
from . import fdtd_sim
|
||||
|
||||
BL_REGISTER = [
|
||||
*sim_domain.BL_REGISTER,
|
||||
# *sim_grid.BL_REGISTER,
|
||||
# *sim_grid_axes.BL_REGISTER,
|
||||
*fdtd_sim.BL_REGISTER,
|
||||
]
|
||||
BL_NODES = {
|
||||
**sim_domain.BL_NODES,
|
||||
# **sim_grid.BL_NODES,
|
||||
# **sim_grid_axes.BL_NODES,
|
||||
**fdtd_sim.BL_NODES,
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
import tidy3d as td
|
||||
import sympy as sp
|
||||
import sympy.physics.units as spu
|
||||
|
||||
from ... import contracts as ct
|
||||
from ... import sockets
|
||||
from .. import base
|
||||
|
||||
class FDTDSimNode(base.MaxwellSimNode):
|
||||
node_type = ct.NodeType.FDTDSim
|
||||
bl_label = "FDTD Simulation"
|
||||
|
||||
####################
|
||||
# - Sockets
|
||||
####################
|
||||
input_sockets = {
|
||||
"Domain": sockets.MaxwellSimDomainSocketDef(),
|
||||
"BCs": sockets.MaxwellBoundCondsSocketDef(),
|
||||
"Sources": sockets.MaxwellSourceSocketDef(),
|
||||
"Structures": sockets.MaxwellStructureSocketDef(),
|
||||
"Monitors": sockets.MaxwellMonitorSocketDef(),
|
||||
}
|
||||
output_sockets = {
|
||||
"FDTD Sim": sockets.MaxwellFDTDSimSocketDef(),
|
||||
}
|
||||
|
||||
####################
|
||||
# - Output Socket Computation
|
||||
####################
|
||||
@base.computes_output_socket(
|
||||
"FDTD Sim",
|
||||
kind=ct.DataFlowKind.Value,
|
||||
input_sockets={
|
||||
"Sources", "Structures", "Domain", "BCs", "Monitors"
|
||||
},
|
||||
)
|
||||
def compute_fdtd_sim(self, input_sockets: dict) -> sp.Expr:
|
||||
sim_domain = input_sockets["Domain"]
|
||||
sources = input_sockets["Sources"]
|
||||
structures = input_sockets["Structures"]
|
||||
bounds = input_sockets["BCs"]
|
||||
monitors = input_sockets["Monitors"]
|
||||
|
||||
if not isinstance(sources, list):
|
||||
sources = [sources]
|
||||
if not isinstance(structures, list):
|
||||
structures = [structures]
|
||||
if not isinstance(monitors, list):
|
||||
monitors = [monitors]
|
||||
|
||||
return td.Simulation(
|
||||
**sim_domain, ## run_time=, size=, grid=, medium=
|
||||
structures=structures,
|
||||
sources=sources,
|
||||
monitors=monitors,
|
||||
boundary_spec=bounds,
|
||||
)
|
||||
|
||||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = [
|
||||
FDTDSimNode,
|
||||
]
|
||||
BL_NODES = {
|
||||
ct.NodeType.FDTDSim: (
|
||||
ct.NodeCategory.MAXWELLSIM_SIMS
|
||||
)
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
import bpy
|
||||
import sympy as sp
|
||||
import sympy.physics.units as spu
|
||||
import scipy as sc
|
||||
|
||||
from .....utils import analyze_geonodes
|
||||
from ... import contracts as ct
|
||||
from ... import sockets
|
||||
from .. import base
|
||||
from ... import managed_objs
|
||||
|
||||
GEONODES_DOMAIN_BOX = "simdomain_box"
|
||||
|
||||
class SimDomainNode(base.MaxwellSimNode):
|
||||
node_type = ct.NodeType.SimDomain
|
||||
bl_label = "Sim Domain"
|
||||
|
||||
input_sockets = {
|
||||
"Duration": sockets.PhysicalTimeSocketDef(
|
||||
default_value = 5 * spu.ps,
|
||||
default_unit = spu.ps,
|
||||
),
|
||||
"Center": sockets.PhysicalSize3DSocketDef(),
|
||||
"Size": sockets.PhysicalSize3DSocketDef(),
|
||||
"Grid": sockets.MaxwellSimGridSocketDef(),
|
||||
"Ambient Medium": sockets.MaxwellMediumSocketDef(),
|
||||
}
|
||||
output_sockets = {
|
||||
"Domain": sockets.MaxwellSimDomainSocketDef(),
|
||||
}
|
||||
|
||||
managed_obj_defs = {
|
||||
"domain_box": ct.schemas.ManagedObjDef(
|
||||
mk=lambda name: managed_objs.ManagedBLObject(name),
|
||||
name_prefix="domain_box_",
|
||||
)
|
||||
}
|
||||
|
||||
####################
|
||||
# - Callbacks
|
||||
####################
|
||||
@base.computes_output_socket(
|
||||
"Domain",
|
||||
input_sockets={"Duration", "Center", "Size", "Grid", "Ambient Medium"},
|
||||
)
|
||||
def compute_sim_domain(self, input_sockets: dict) -> sp.Expr:
|
||||
if all([
|
||||
(_duration := input_sockets["Duration"]),
|
||||
(_center := input_sockets["Center"]),
|
||||
(_size := input_sockets["Size"]),
|
||||
(grid := input_sockets["Grid"]),
|
||||
(medium := input_sockets["Ambient Medium"]),
|
||||
]):
|
||||
duration = spu.convert_to(_duration, spu.second) / spu.second
|
||||
center = tuple(spu.convert_to(_center, spu.um) / spu.um)
|
||||
size = tuple(spu.convert_to(_size, spu.um) / spu.um)
|
||||
return dict(
|
||||
run_time=duration,
|
||||
center=center,
|
||||
size=size,
|
||||
grid_spec=grid,
|
||||
medium=medium,
|
||||
)
|
||||
|
||||
####################
|
||||
# - Preview
|
||||
####################
|
||||
@base.on_value_changed(
|
||||
socket_name={"Center", "Size"},
|
||||
input_sockets={"Center", "Size"},
|
||||
managed_objs={"domain_box"},
|
||||
)
|
||||
def on_value_changed__center_size(
|
||||
self,
|
||||
input_sockets: dict,
|
||||
managed_objs: dict[str, ct.schemas.ManagedObj],
|
||||
):
|
||||
_center = input_sockets["Center"]
|
||||
center = tuple([
|
||||
float(el)
|
||||
for el in spu.convert_to(_center, spu.um) / spu.um
|
||||
])
|
||||
|
||||
_size = input_sockets["Size"]
|
||||
size = tuple([
|
||||
float(el)
|
||||
for el in spu.convert_to(_size, spu.um) / spu.um
|
||||
])
|
||||
## TODO: Preview unit system?? Presume um for now
|
||||
|
||||
# Retrieve Hard-Coded GeoNodes and Analyze Input
|
||||
geo_nodes = bpy.data.node_groups[GEONODES_DOMAIN_BOX]
|
||||
geonodes_interface = analyze_geonodes.interface(
|
||||
geo_nodes, direc="INPUT"
|
||||
)
|
||||
|
||||
# Sync Modifier Inputs
|
||||
managed_objs["domain_box"].sync_geonodes_modifier(
|
||||
geonodes_node_group=geo_nodes,
|
||||
geonodes_identifier_to_value={
|
||||
geonodes_interface["Size"].identifier: size,
|
||||
## TODO: Use 'bl_socket_map.value_to_bl`!
|
||||
## - This accounts for auto-conversion, unit systems, etc. .
|
||||
## - We could keep it in the node base class...
|
||||
## - ...But it needs aligning with Blender, too. Hmm.
|
||||
}
|
||||
)
|
||||
|
||||
# Sync Object Position
|
||||
managed_objs["domain_box"].bl_object("MESH").location = center
|
||||
|
||||
@base.on_show_preview(
|
||||
managed_objs={"domain_box"},
|
||||
)
|
||||
def on_show_preview(
|
||||
self,
|
||||
managed_objs: dict[str, ct.schemas.ManagedObj],
|
||||
):
|
||||
managed_objs["domain_box"].show_preview("MESH")
|
||||
self.on_value_changed__center_size()
|
||||
|
||||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = [
|
||||
SimDomainNode,
|
||||
]
|
||||
BL_NODES = {
|
||||
ct.NodeType.SimDomain: (
|
||||
ct.NodeCategory.MAXWELLSIM_SIMS
|
||||
)
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
|
@ -0,0 +1,17 @@
|
|||
from . import automatic_sim_grid_axis
|
||||
from . import manual_sim_grid_axis
|
||||
from . import uniform_sim_grid_axis
|
||||
from . import array_sim_grid_axis
|
||||
|
||||
BL_REGISTER = [
|
||||
*automatic_sim_grid_axis.BL_REGISTER,
|
||||
*manual_sim_grid_axis.BL_REGISTER,
|
||||
*uniform_sim_grid_axis.BL_REGISTER,
|
||||
*array_sim_grid_axis.BL_REGISTER,
|
||||
]
|
||||
BL_NODES = {
|
||||
**automatic_sim_grid_axis.BL_NODES,
|
||||
**manual_sim_grid_axis.BL_NODES,
|
||||
**uniform_sim_grid_axis.BL_NODES,
|
||||
**array_sim_grid_axis.BL_NODES,
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
from . import temporal_shapes
|
||||
|
||||
from . import point_dipole_source
|
||||
#from . import uniform_current_source
|
||||
from . import plane_wave_source
|
||||
#from . import gaussian_beam_source
|
||||
#from . import astigmatic_gaussian_beam_source
|
||||
#from . import tfsf_source
|
||||
|
||||
BL_REGISTER = [
|
||||
*temporal_shapes.BL_REGISTER,
|
||||
*point_dipole_source.BL_REGISTER,
|
||||
# *uniform_current_source.BL_REGISTER,
|
||||
*plane_wave_source.BL_REGISTER,
|
||||
# *gaussian_beam_source.BL_REGISTER,
|
||||
# *astigmatic_gaussian_beam_source.BL_REGISTER,
|
||||
# *tfsf_source.BL_REGISTER,
|
||||
]
|
||||
BL_NODES = {
|
||||
**temporal_shapes.BL_NODES,
|
||||
**point_dipole_source.BL_NODES,
|
||||
# **uniform_current_source.BL_NODES,
|
||||
**plane_wave_source.BL_NODES,
|
||||
# **gaussian_beam_source.BL_NODES,
|
||||
# **astigmatic_gaussian_beam_source.BL_NODES,
|
||||
# **tfsf_source.BL_NODES,
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
import typing_extensions as typx
|
||||
import math
|
||||
|
||||
import tidy3d as td
|
||||
import sympy as sp
|
||||
import sympy.physics.units as spu
|
||||
|
||||
import bpy
|
||||
|
||||
from .....utils import analyze_geonodes
|
||||
from ... import managed_objs
|
||||
from ... import contracts as ct
|
||||
from ... import sockets
|
||||
from .. import base
|
||||
|
||||
GEONODES_PLANE_WAVE = "source_plane_wave"
|
||||
|
||||
def convert_vector_to_spherical(
|
||||
v: sp.MatrixBase,
|
||||
) -> tuple[str, str, sp.Expr, sp.Expr]:
|
||||
"""Converts a vector (maybe normalized) to spherical coordinates from an arbitrary choice of injection axis.
|
||||
|
||||
Injection axis is chosen to minimize `theta`
|
||||
"""
|
||||
x, y, z = v
|
||||
|
||||
injection_axis = max(
|
||||
('x', abs(x)),
|
||||
('y', abs(y)),
|
||||
('z', abs(z)),
|
||||
key=lambda item: item[1]
|
||||
)[0]
|
||||
## Select injection axis that minimizes 'theta'
|
||||
|
||||
if injection_axis == "x":
|
||||
direction = "+" if x >= 0 else "-"
|
||||
theta = sp.acos(x / sp.sqrt(x**2 + y**2 + z**2))
|
||||
phi = sp.atan2(z, y)
|
||||
elif injection_axis == "y":
|
||||
direction = "+" if y >= 0 else "-"
|
||||
theta = sp.acos(y / sp.sqrt(x**2 + y**2 + z**2))
|
||||
phi = sp.atan2(x, z)
|
||||
else:
|
||||
direction = "+" if z >= 0 else "-"
|
||||
theta = sp.acos(z / sp.sqrt(x**2 + y**2 + z**2))
|
||||
phi = sp.atan2(y, x)
|
||||
|
||||
return injection_axis, direction, theta, phi
|
||||
|
||||
class PlaneWaveSourceNode(base.MaxwellSimNode):
|
||||
node_type = ct.NodeType.PlaneWaveSource
|
||||
bl_label = "Plane Wave Source"
|
||||
|
||||
####################
|
||||
# - Sockets
|
||||
####################
|
||||
input_sockets = {
|
||||
"Temporal Shape": sockets.MaxwellTemporalShapeSocketDef(),
|
||||
"Center": sockets.PhysicalPoint3DSocketDef(),
|
||||
"Direction": sockets.Real3DVectorSocketDef(
|
||||
default_value=sp.Matrix([0, 0, -1])
|
||||
),
|
||||
"Pol Angle": sockets.PhysicalAngleSocketDef(),
|
||||
}
|
||||
output_sockets = {
|
||||
"Source": sockets.MaxwellSourceSocketDef(),
|
||||
}
|
||||
|
||||
managed_obj_defs = {
|
||||
"plane_wave_source": ct.schemas.ManagedObjDef(
|
||||
mk=lambda name: managed_objs.ManagedBLObject(name),
|
||||
name_prefix="",
|
||||
)
|
||||
}
|
||||
|
||||
####################
|
||||
# - Output Socket Computation
|
||||
####################
|
||||
@base.computes_output_socket(
|
||||
"Source",
|
||||
input_sockets={"Temporal Shape", "Center", "Direction", "Pol Angle"},
|
||||
)
|
||||
def compute_source(self, input_sockets: dict):
|
||||
temporal_shape = input_sockets["Temporal Shape"]
|
||||
_center = input_sockets["Center"]
|
||||
direction = input_sockets["Direction"]
|
||||
pol_angle = input_sockets["Pol Angle"]
|
||||
|
||||
injection_axis, dir_sgn, theta, phi = convert_vector_to_spherical(direction)
|
||||
|
||||
size = {
|
||||
"x": (0, math.inf, math.inf),
|
||||
"y": (math.inf, 0, math.inf),
|
||||
"z": (math.inf, math.inf, 0),
|
||||
}[injection_axis]
|
||||
center = tuple(spu.convert_to(_center, spu.um) / spu.um)
|
||||
|
||||
# Display the results
|
||||
return td.PlaneWave(
|
||||
center=center,
|
||||
source_time=temporal_shape,
|
||||
size=size,
|
||||
direction=dir_sgn,
|
||||
angle_theta=theta,
|
||||
angle_phi=phi,
|
||||
pol_angle=pol_angle,
|
||||
)
|
||||
|
||||
####################
|
||||
# - Preview
|
||||
####################
|
||||
@base.on_value_changed(
|
||||
socket_name={"Center", "Direction"},
|
||||
input_sockets={"Center", "Direction"},
|
||||
managed_objs={"plane_wave_source"},
|
||||
)
|
||||
def on_value_changed__center_direction(
|
||||
self,
|
||||
input_sockets: dict,
|
||||
managed_objs: dict[str, ct.schemas.ManagedObj],
|
||||
):
|
||||
_center = input_sockets["Center"]
|
||||
center = tuple([
|
||||
float(el)
|
||||
for el in spu.convert_to(_center, spu.um) / spu.um
|
||||
])
|
||||
|
||||
_direction = input_sockets["Direction"]
|
||||
direction = tuple([
|
||||
float(el)
|
||||
for el in _direction
|
||||
])
|
||||
## TODO: Preview unit system?? Presume um for now
|
||||
|
||||
# Retrieve Hard-Coded GeoNodes and Analyze Input
|
||||
geo_nodes = bpy.data.node_groups[GEONODES_PLANE_WAVE]
|
||||
geonodes_interface = analyze_geonodes.interface(
|
||||
geo_nodes, direc="INPUT"
|
||||
)
|
||||
|
||||
# Sync Modifier Inputs
|
||||
managed_objs["plane_wave_source"].sync_geonodes_modifier(
|
||||
geonodes_node_group=geo_nodes,
|
||||
geonodes_identifier_to_value={
|
||||
geonodes_interface["Direction"].identifier: direction,
|
||||
## TODO: Use 'bl_socket_map.value_to_bl`!
|
||||
## - This accounts for auto-conversion, unit systems, etc. .
|
||||
## - We could keep it in the node base class...
|
||||
## - ...But it needs aligning with Blender, too. Hmm.
|
||||
}
|
||||
)
|
||||
|
||||
# Sync Object Position
|
||||
managed_objs["plane_wave_source"].bl_object("MESH").location = center
|
||||
|
||||
@base.on_show_preview(
|
||||
managed_objs={"plane_wave_source"},
|
||||
)
|
||||
def on_show_preview(
|
||||
self,
|
||||
managed_objs: dict[str, ct.schemas.ManagedObj],
|
||||
):
|
||||
managed_objs["plane_wave_source"].show_preview("MESH")
|
||||
self.on_value_changed__center_direction()
|
||||
|
||||
|
||||
|
||||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = [
|
||||
PlaneWaveSourceNode,
|
||||
]
|
||||
BL_NODES = {
|
||||
ct.NodeType.PlaneWaveSource: (
|
||||
ct.NodeCategory.MAXWELLSIM_SOURCES
|
||||
)
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
import typing as typ
|
||||
import tidy3d as td
|
||||
import sympy as sp
|
||||
import sympy.physics.units as spu
|
||||
|
||||
import bpy
|
||||
|
||||
from ... import contracts as ct
|
||||
from ... import sockets
|
||||
from .. import base
|
||||
from ... import managed_objs
|
||||
|
||||
class PointDipoleSourceNode(base.MaxwellSimNode):
|
||||
node_type = ct.NodeType.PointDipoleSource
|
||||
bl_label = "Point Dipole Source"
|
||||
|
||||
####################
|
||||
# - Sockets
|
||||
####################
|
||||
input_sockets = {
|
||||
"Temporal Shape": sockets.MaxwellTemporalShapeSocketDef(),
|
||||
"Center": sockets.PhysicalPoint3DSocketDef(),
|
||||
"Interpolate": sockets.BoolSocketDef(
|
||||
default_value=True,
|
||||
),
|
||||
}
|
||||
output_sockets = {
|
||||
"Source": sockets.MaxwellSourceSocketDef(),
|
||||
}
|
||||
|
||||
managed_obj_defs = {
|
||||
"sphere_empty": ct.schemas.ManagedObjDef(
|
||||
mk=lambda name: managed_objs.ManagedBLObject(name),
|
||||
name_prefix="point_dipole_",
|
||||
)
|
||||
}
|
||||
|
||||
####################
|
||||
# - Properties
|
||||
####################
|
||||
pol_axis: bpy.props.EnumProperty(
|
||||
name="Polarization Axis",
|
||||
description="Polarization Axis",
|
||||
items=[
|
||||
("EX", "Ex", "Electric field in x-dir"),
|
||||
("EY", "Ey", "Electric field in y-dir"),
|
||||
("EZ", "Ez", "Electric field in z-dir"),
|
||||
],
|
||||
default="EX",
|
||||
update=(lambda self, context: self.sync_prop("pol_axis")),
|
||||
)
|
||||
|
||||
####################
|
||||
# - UI
|
||||
####################
|
||||
def draw_props(self, context, layout):
|
||||
layout.prop(self, "pol_axis", text="Pol Axis")
|
||||
|
||||
####################
|
||||
# - Output Socket Computation
|
||||
####################
|
||||
@base.computes_output_socket(
|
||||
"Source",
|
||||
input_sockets={"Temporal Shape", "Center", "Interpolate"},
|
||||
props={"pol_axis"},
|
||||
)
|
||||
def compute_source(self, input_sockets: dict[str, typ.Any], props: dict[str, typ.Any]) -> td.PointDipole:
|
||||
pol_axis = {
|
||||
"EX": "Ex",
|
||||
"EY": "Ey",
|
||||
"EZ": "Ez",
|
||||
}[props["pol_axis"]]
|
||||
|
||||
temporal_shape = input_sockets["Temporal Shape"]
|
||||
_center = input_sockets["Center"]
|
||||
interpolate = input_sockets["Interpolate"]
|
||||
|
||||
center = tuple(spu.convert_to(_center, spu.um) / spu.um)
|
||||
|
||||
_res = td.PointDipole(
|
||||
center=center,
|
||||
source_time=temporal_shape,
|
||||
interpolate=interpolate,
|
||||
polarization=pol_axis,
|
||||
)
|
||||
return _res
|
||||
|
||||
####################
|
||||
# - Preview
|
||||
####################
|
||||
@base.on_value_changed(
|
||||
socket_name="Center",
|
||||
input_sockets={"Center"},
|
||||
managed_objs={"sphere_empty"},
|
||||
)
|
||||
def on_value_changed__center(
|
||||
self,
|
||||
input_sockets: dict,
|
||||
managed_objs: dict[str, ct.schemas.ManagedObj],
|
||||
):
|
||||
_center = input_sockets["Center"]
|
||||
center = tuple(spu.convert_to(_center, spu.um) / spu.um)
|
||||
## TODO: Preview unit system?? Presume um for now
|
||||
|
||||
mobj = managed_objs["sphere_empty"]
|
||||
bl_object = mobj.bl_object("EMPTY")
|
||||
bl_object.location = center #tuple([float(el) for el in center])
|
||||
|
||||
@base.on_show_preview(
|
||||
managed_objs={"sphere_empty"},
|
||||
)
|
||||
def on_show_preview(
|
||||
self,
|
||||
managed_objs: dict[str, ct.schemas.ManagedObj],
|
||||
):
|
||||
managed_objs["sphere_empty"].show_preview(
|
||||
"EMPTY",
|
||||
empty_display_type="SPHERE",
|
||||
)
|
||||
|
||||
|
||||
|
||||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = [
|
||||
PointDipoleSourceNode,
|
||||
]
|
||||
BL_NODES = {
|
||||
ct.NodeType.PointDipoleSource: (
|
||||
ct.NodeCategory.MAXWELLSIM_SOURCES
|
||||
)
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
from . import gaussian_pulse_temporal_shape
|
||||
#from . import continuous_wave_temporal_shape
|
||||
#from . import array_temporal_shape
|
||||
|
||||
BL_REGISTER = [
|
||||
*gaussian_pulse_temporal_shape.BL_REGISTER,
|
||||
# *continuous_wave_temporal_shape.BL_REGISTER,
|
||||
# *array_temporal_shape.BL_REGISTER,
|
||||
]
|
||||
BL_NODES = {
|
||||
**gaussian_pulse_temporal_shape.BL_NODES,
|
||||
# **continuous_wave_temporal_shape.BL_NODES,
|
||||
# **array_temporal_shape.BL_NODES,
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = []
|
||||
BL_NODES = {}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import tidy3d as td
|
||||
import sympy as sp
|
||||
import sympy.physics.units as spu
|
||||
|
||||
from .... import contracts
|
||||
from .... import sockets
|
||||
from ... import base
|
||||
|
||||
class ContinuousWaveTemporalShapeNode(base.MaxwellSimTreeNode):
|
||||
node_type = contracts.NodeType.ContinuousWaveTemporalShape
|
||||
|
||||
bl_label = "Continuous Wave Temporal Shape"
|
||||
#bl_icon = ...
|
||||
|
||||
####################
|
||||
# - Sockets
|
||||
####################
|
||||
input_sockets = {
|
||||
#"amplitude": sockets.RealNumberSocketDef(
|
||||
# label="Temporal Shape",
|
||||
#), ## Should have a unit of some kind...
|
||||
"phase": sockets.PhysicalAngleSocketDef(
|
||||
label="Phase",
|
||||
),
|
||||
"freq_center": sockets.PhysicalFreqSocketDef(
|
||||
label="Freq Center",
|
||||
),
|
||||
"freq_std": sockets.PhysicalFreqSocketDef(
|
||||
label="Freq STD",
|
||||
),
|
||||
"time_delay_rel_ang_freq": sockets.RealNumberSocketDef(
|
||||
label="Time Delay rel. Ang. Freq",
|
||||
default_value=5.0,
|
||||
),
|
||||
}
|
||||
output_sockets = {
|
||||
"temporal_shape": sockets.MaxwellTemporalShapeSocketDef(
|
||||
label="Temporal Shape",
|
||||
),
|
||||
}
|
||||
|
||||
####################
|
||||
# - Output Socket Computation
|
||||
####################
|
||||
@base.computes_output_socket("temporal_shape")
|
||||
def compute_source(self: contracts.NodeTypeProtocol) -> td.PointDipole:
|
||||
_phase = self.compute_input("phase")
|
||||
_freq_center = self.compute_input("freq_center")
|
||||
_freq_std = self.compute_input("freq_std")
|
||||
time_delay_rel_ang_freq = self.compute_input("time_delay_rel_ang_freq")
|
||||
|
||||
cheating_amplitude = 1.0
|
||||
phase = spu.convert_to(_phase, spu.radian) / spu.radian
|
||||
freq_center = spu.convert_to(_freq_center, spu.hertz) / spu.hertz
|
||||
freq_std = spu.convert_to(_freq_std, spu.hertz) / spu.hertz
|
||||
|
||||
return td.ContinuousWave(
|
||||
amplitude=cheating_amplitude,
|
||||
phase=phase,
|
||||
freq0=freq_center,
|
||||
fwidth=freq_std,
|
||||
offset=time_delay_rel_ang_freq,
|
||||
)
|
||||
|
||||
|
||||
|
||||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = [
|
||||
ContinuousWaveTemporalShapeNode,
|
||||
]
|
||||
BL_NODES = {
|
||||
contracts.NodeType.ContinuousWaveTemporalShape: (
|
||||
contracts.NodeCategory.MAXWELLSIM_SOURCES_TEMPORALSHAPES
|
||||
)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue