refactor: Streamlined graph-update semantics.
parent
dc76ab7688
commit
480679a3c0
12
TODO.md
12
TODO.md
|
@ -1,12 +1,17 @@
|
||||||
# Acute Tasks
|
# Acute Tasks
|
||||||
- [x] Implement Material Import for Maxim Data
|
- [x] Implement Material Import for Maxim Data
|
||||||
- [x] Implement Robust DataFlowKind for list-like / spectral-like composite types
|
- [x] Implement Robust DataFlowKind for list-like / spectral-like composite types
|
||||||
|
- [ ] Unify random node/socket caches.
|
||||||
- [ ] Finish the "Low-Hanging Fruit" Nodes
|
- [ ] Finish the "Low-Hanging Fruit" Nodes
|
||||||
- [ ] Move preview GN trees to the asset library.
|
- [ ] Move preview GN trees to the asset library.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Nodes
|
# Nodes
|
||||||
|
## Analysis
|
||||||
|
- [ ] Extract
|
||||||
|
- [ ] Viz
|
||||||
|
|
||||||
## Inputs
|
## Inputs
|
||||||
- [x] Wave Constant
|
- [x] Wave Constant
|
||||||
- [x] Implement export of frequency / wavelength array/range.
|
- [x] Implement export of frequency / wavelength array/range.
|
||||||
|
@ -161,12 +166,7 @@
|
||||||
- [ ] Sim Grid Axes / Uniform Sim Grid Axis
|
- [ ] Sim Grid Axes / Uniform Sim Grid Axis
|
||||||
- [ ] Sim Grid Axes / Array Sim Grid Axis
|
- [ ] Sim Grid Axes / Array Sim Grid Axis
|
||||||
|
|
||||||
## Converters
|
## Utilities
|
||||||
- [ ] Math
|
|
||||||
- [ ] Implement common operations w/secondary choice of socket type based on a custom internal data structure
|
|
||||||
- [ ] Implement angfreq/frequency/vacwl conversion.
|
|
||||||
- [ ] Implement spectral math on SDs
|
|
||||||
- [ ] Implement easy derivation of ex. transmission and reflection.
|
|
||||||
- [ ] Separate
|
- [ ] Separate
|
||||||
- [x] Combine
|
- [x] Combine
|
||||||
- [x] Implement concatenation of sim-critical socket types into their multi-type
|
- [x] Implement concatenation of sim-critical socket types into their multi-type
|
||||||
|
|
|
@ -23,6 +23,7 @@ dependencies = [
|
||||||
"charset-normalizer==2.0.10",
|
"charset-normalizer==2.0.10",
|
||||||
"certifi==2021.10.8",
|
"certifi==2021.10.8",
|
||||||
"jax[cpu]>=0.4.26",
|
"jax[cpu]>=0.4.26",
|
||||||
|
"msgspec[toml]>=0.18.6",
|
||||||
]
|
]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = "~= 3.11"
|
requires-python = "~= 3.11"
|
||||||
|
@ -136,4 +137,4 @@ max-args = 6
|
||||||
[tool.ruff.format]
|
[tool.ruff.format]
|
||||||
quote-style = "single"
|
quote-style = "single"
|
||||||
indent-style = "tab"
|
indent-style = "tab"
|
||||||
docstring-code-format = true
|
docstring-code-format = false
|
||||||
|
|
|
@ -63,6 +63,7 @@ ml-dtypes==0.4.0
|
||||||
# via jaxlib
|
# via jaxlib
|
||||||
mpmath==1.3.0
|
mpmath==1.3.0
|
||||||
# via sympy
|
# via sympy
|
||||||
|
msgspec==0.18.6
|
||||||
networkx==3.2
|
networkx==3.2
|
||||||
numpy==1.24.3
|
numpy==1.24.3
|
||||||
# via contourpy
|
# via contourpy
|
||||||
|
@ -135,6 +136,8 @@ sympy==1.12
|
||||||
tidy3d==2.6.3
|
tidy3d==2.6.3
|
||||||
toml==0.10.2
|
toml==0.10.2
|
||||||
# via tidy3d
|
# via tidy3d
|
||||||
|
tomli-w==1.0.0
|
||||||
|
# via msgspec
|
||||||
toolz==0.12.1
|
toolz==0.12.1
|
||||||
# via dask
|
# via dask
|
||||||
# via partd
|
# via partd
|
||||||
|
|
|
@ -62,6 +62,7 @@ ml-dtypes==0.4.0
|
||||||
# via jaxlib
|
# via jaxlib
|
||||||
mpmath==1.3.0
|
mpmath==1.3.0
|
||||||
# via sympy
|
# via sympy
|
||||||
|
msgspec==0.18.6
|
||||||
networkx==3.2
|
networkx==3.2
|
||||||
numpy==1.24.3
|
numpy==1.24.3
|
||||||
# via contourpy
|
# via contourpy
|
||||||
|
@ -133,6 +134,8 @@ sympy==1.12
|
||||||
tidy3d==2.6.3
|
tidy3d==2.6.3
|
||||||
toml==0.10.2
|
toml==0.10.2
|
||||||
# via tidy3d
|
# via tidy3d
|
||||||
|
tomli-w==1.0.0
|
||||||
|
# via msgspec
|
||||||
toolz==0.12.1
|
toolz==0.12.1
|
||||||
# via dask
|
# via dask
|
||||||
# via partd
|
# via partd
|
||||||
|
|
|
@ -0,0 +1,529 @@
|
||||||
|
"""Implements various key caches on instances of Blender objects, especially nodes and sockets."""
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import inspect
|
||||||
|
import typing as typ
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import msgspec
|
||||||
|
import sympy as sp
|
||||||
|
import sympy.physics.units as spu
|
||||||
|
|
||||||
|
from ...utils import extra_sympy_units as spux
|
||||||
|
from ...utils import logger
|
||||||
|
from . import contracts as ct
|
||||||
|
from . import managed_objs, sockets
|
||||||
|
|
||||||
|
log = logger.get(__name__)
|
||||||
|
|
||||||
|
InstanceID: typ.TypeAlias = str ## Stringified UUID4
|
||||||
|
|
||||||
|
|
||||||
|
class BLInstance(typ.Protocol):
|
||||||
|
"""An instance of a blender object, ex. nodes/sockets.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
instance_id: Stringified UUID4 that uniquely identifies an instance, among all active instances on all active classes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
instance_id: InstanceID
|
||||||
|
|
||||||
|
|
||||||
|
EncodableValue: typ.TypeAlias = typ.Any ## msgspec-compatible
|
||||||
|
PropGetMethod: typ.TypeAlias = typ.Callable[[BLInstance], EncodableValue]
|
||||||
|
PropSetMethod: typ.TypeAlias = typ.Callable[[BLInstance, EncodableValue], None]
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - (De)Serialization
|
||||||
|
####################
|
||||||
|
EncodedComplex: typ.TypeAlias = tuple[float, float] | list[float, float]
|
||||||
|
EncodedSympy: typ.TypeAlias = str
|
||||||
|
EncodedManagedObj: typ.TypeAlias = tuple[str, str] | list[str, str]
|
||||||
|
EncodedPydanticModel: typ.TypeAlias = tuple[str, str] | list[str, str]
|
||||||
|
|
||||||
|
|
||||||
|
def _enc_hook(obj: typ.Any) -> EncodableValue:
|
||||||
|
"""Translates types not natively supported by `msgspec`, to an encodable form supported by `msgspec`.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
obj: The object of arbitrary type to transform into an encodable value.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A value encodable by `msgspec`.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError: When the type transformation hasn't been implemented.
|
||||||
|
"""
|
||||||
|
if isinstance(obj, complex):
|
||||||
|
return (obj.real, obj.imag)
|
||||||
|
if isinstance(obj, sp.Basic | sp.MatrixBase | sp.Expr | spu.Quantity):
|
||||||
|
return sp.srepr(obj)
|
||||||
|
if isinstance(obj, managed_objs.ManagedObj):
|
||||||
|
return (obj.name, obj.__class__.__name__)
|
||||||
|
if isinstance(obj, ct.schemas.SocketDef):
|
||||||
|
return (obj.model_dump(), obj.__class__.__name__)
|
||||||
|
|
||||||
|
msg = f'Can\'t encode "{obj}" of type {type(obj)}'
|
||||||
|
raise NotImplementedError(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def _dec_hook(_type: type, obj: EncodableValue) -> typ.Any:
|
||||||
|
"""Translates the `msgspec`-encoded form of an object back to its true form.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
_type: The type to transform the `msgspec`-encoded object back into.
|
||||||
|
obj: The encoded object of to transform back into an encodable value.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A value encodable by `msgspec`.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError: When the type transformation hasn't been implemented.
|
||||||
|
"""
|
||||||
|
if _type is complex and isinstance(obj, EncodedComplex):
|
||||||
|
return complex(obj[0], obj[1])
|
||||||
|
if (
|
||||||
|
_type is sp.Basic
|
||||||
|
and isinstance(obj, EncodedSympy)
|
||||||
|
or _type is sp.Expr
|
||||||
|
and isinstance(obj, EncodedSympy)
|
||||||
|
or _type is sp.MatrixBase
|
||||||
|
and isinstance(obj, EncodedSympy)
|
||||||
|
or _type is spu.Quantity
|
||||||
|
and isinstance(obj, EncodedSympy)
|
||||||
|
):
|
||||||
|
return sp.sympify(obj).subs(spux.ALL_UNIT_SYMBOLS)
|
||||||
|
if (
|
||||||
|
_type is managed_objs.ManagedBLMesh
|
||||||
|
and isinstance(obj, EncodedManagedObj)
|
||||||
|
or _type is managed_objs.ManagedBLImage
|
||||||
|
and isinstance(obj, EncodedManagedObj)
|
||||||
|
or _type is managed_objs.ManagedBLModifier
|
||||||
|
and isinstance(obj, EncodedManagedObj)
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
'ManagedBLMesh': managed_objs.ManagedBLMesh,
|
||||||
|
'ManagedBLImage': managed_objs.ManagedBLImage,
|
||||||
|
'ManagedBLModifier': managed_objs.ManagedBLModifier,
|
||||||
|
}[obj[1]](obj[0])
|
||||||
|
if _type is ct.schemas.SocketDef:
|
||||||
|
return getattr(sockets, obj[1])(**obj[0])
|
||||||
|
|
||||||
|
msg = f'Can\'t decode "{obj}" to type {type(obj)}'
|
||||||
|
raise NotImplementedError(msg)
|
||||||
|
|
||||||
|
|
||||||
|
ENCODER = msgspec.json.Encoder(enc_hook=_enc_hook, order='deterministic')
|
||||||
|
|
||||||
|
_DECODERS: dict[type, msgspec.json.Decoder] = {
|
||||||
|
complex: msgspec.json.Decoder(type=complex, dec_hook=_dec_hook),
|
||||||
|
sp.Basic: msgspec.json.Decoder(type=sp.Basic, dec_hook=_dec_hook),
|
||||||
|
sp.Expr: msgspec.json.Decoder(type=sp.Expr, dec_hook=_dec_hook),
|
||||||
|
sp.MatrixBase: msgspec.json.Decoder(type=sp.MatrixBase, dec_hook=_dec_hook),
|
||||||
|
spu.Quantity: msgspec.json.Decoder(type=spu.Quantity, dec_hook=_dec_hook),
|
||||||
|
managed_objs.ManagedBLMesh: msgspec.json.Decoder(
|
||||||
|
type=managed_objs.ManagedBLMesh,
|
||||||
|
dec_hook=_dec_hook,
|
||||||
|
),
|
||||||
|
managed_objs.ManagedBLImage: msgspec.json.Decoder(
|
||||||
|
type=managed_objs.ManagedBLImage,
|
||||||
|
dec_hook=_dec_hook,
|
||||||
|
),
|
||||||
|
managed_objs.ManagedBLModifier: msgspec.json.Decoder(
|
||||||
|
type=managed_objs.ManagedBLModifier,
|
||||||
|
dec_hook=_dec_hook,
|
||||||
|
),
|
||||||
|
# managed_objs.ManagedObj: msgspec.json.Decoder(
|
||||||
|
# type=managed_objs.ManagedObj, dec_hook=_dec_hook
|
||||||
|
# ), ## Doesn't work b/c unions are not explicit
|
||||||
|
ct.schemas.SocketDef: msgspec.json.Decoder(
|
||||||
|
type=ct.schemas.SocketDef,
|
||||||
|
dec_hook=_dec_hook,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
_DECODER_FALLBACK: msgspec.json.Decoder = msgspec.json.Decoder(dec_hook=_dec_hook)
|
||||||
|
|
||||||
|
|
||||||
|
@functools.cache
|
||||||
|
def DECODER(_type: type) -> msgspec.json.Decoder: # noqa: N802
|
||||||
|
"""Retrieve a suitable `msgspec.json.Decoder` by-type.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
_type: The type to retrieve a decoder for.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A suitable decoder.
|
||||||
|
"""
|
||||||
|
if (decoder := _DECODERS.get(_type)) is not None:
|
||||||
|
return decoder
|
||||||
|
|
||||||
|
return _DECODER_FALLBACK
|
||||||
|
|
||||||
|
|
||||||
|
def decode_any(_type: type, obj: str) -> typ.Any:
|
||||||
|
naive_decode = DECODER(_type).decode(obj)
|
||||||
|
if _type == dict[str, ct.schemas.SocketDef]:
|
||||||
|
return {
|
||||||
|
socket_name: getattr(sockets, socket_def_list[1])(**socket_def_list[0])
|
||||||
|
for socket_name, socket_def_list in naive_decode.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.critical(
|
||||||
|
'Naive Decode of "%s" to "%s" (%s)', str(obj), str(naive_decode), str(_type)
|
||||||
|
)
|
||||||
|
return naive_decode
|
||||||
|
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Cache: Non-Persistent
|
||||||
|
####################
|
||||||
|
CACHE_NOPERSIST: dict[InstanceID, dict[typ.Any, typ.Any]] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def invalidate_nonpersist_instance_id(instance_id: InstanceID) -> None:
|
||||||
|
"""Invalidate any `instance_id` that might be utilizing cache space in `CACHE_NOPERSIST`.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This should be run by the `instance_id` owner in its `free()` method.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
instance_id: The ID of the Blender object instance that's being freed.
|
||||||
|
"""
|
||||||
|
CACHE_NOPERSIST.pop(instance_id, None)
|
||||||
|
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Property Descriptor
|
||||||
|
####################
|
||||||
|
class CachedBLProperty:
|
||||||
|
"""A descriptor that caches a computed attribute of a Blender node/socket/... instance (`bl_instance`), with optional cache persistence.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
**Accessing the internal `_*` attributes is likely an anti-pattern**.
|
||||||
|
|
||||||
|
`CachedBLProperty` does not own the data; it only provides a convenient interface of running user-provided getter/setters.
|
||||||
|
This also applies to the `bpy.types.Property` entry created by `CachedBLProperty`, which should not be accessed directly.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
_getter_method: Method of `bl_instance` that computes the value.
|
||||||
|
_setter_method: Method of `bl_instance` that sets the value.
|
||||||
|
_persist: Whether to persist the value on a `bpy.types.Property` defined on `bl_instance`.
|
||||||
|
The name of this `bpy.types.Property` will be `cache__<prop_name>`.
|
||||||
|
_type: The type of the value, used by the persistent decoder.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, getter_method: PropGetMethod, persist: bool):
|
||||||
|
"""Initialize the getter (and persistance) of the cached property.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- When `persist` is true, the return annotation of the getter mathod will be used to guide deserialization.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
getter_method: Method of `bl_instance` that computes the value.
|
||||||
|
persist: Whether to persist the value on a `bpy.types.Property` defined on `bl_instance`.
|
||||||
|
The name of this `bpy.types.Property` will be `cache__<prop_name>`.
|
||||||
|
"""
|
||||||
|
self._getter_method: PropGetMethod = getter_method
|
||||||
|
self._setter_method: PropSetMethod | None = None
|
||||||
|
|
||||||
|
# Persistance
|
||||||
|
self._persist: bool = persist
|
||||||
|
self._type: type | None = (
|
||||||
|
inspect.signature(getter_method).return_annotation if persist else None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check Non-Empty Type Annotation
|
||||||
|
## For now, just presume that all types can be encoded/decoded.
|
||||||
|
|
||||||
|
# Check Non-Empty Type Annotation
|
||||||
|
## For now, just presume that all types can be encoded/decoded.
|
||||||
|
if self._type is not None and self._type is inspect.Signature.empty:
|
||||||
|
msg = f'A CachedBLProperty was instantiated with "persist={persist}", but its getter method "{self._getter_method}" has no return type annotation'
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
def __set_name__(self, owner: type[BLInstance], name: str) -> None:
|
||||||
|
"""Generates the property name from the name of the attribute that this descriptor is assigned to.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Run by Python when setting an instance of this class to an attribute.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
owner: The class that contains an attribute assigned to an instance of this descriptor.
|
||||||
|
name: The name of the attribute that an instance of descriptor was assigned to.
|
||||||
|
"""
|
||||||
|
self.prop_name: str = name
|
||||||
|
self._bl_prop_name: str = f'blcache__{name}'
|
||||||
|
|
||||||
|
# Define Blender Property (w/Update Sync)
|
||||||
|
owner.set_prop(
|
||||||
|
self._bl_prop_name,
|
||||||
|
bpy.props.StringProperty,
|
||||||
|
name=f'DO NOT USE: Cache for {self.prop_name}',
|
||||||
|
default='',
|
||||||
|
no_update=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __get__(
|
||||||
|
self, bl_instance: BLInstance | None, owner: type[BLInstance]
|
||||||
|
) -> typ.Any:
|
||||||
|
"""Retrieves the property from a cache, or computes it and fills the cache(s).
|
||||||
|
|
||||||
|
If `self._persist` is `True`, the persistent cache will be checked and filled after the non-persistent cache.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- The persistent cache keeps the
|
||||||
|
- The persistent cache is fast and has good compatibility (courtesy `msgspec` encoding), but isn't nearly as fast as
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
bl_instance: The Blender object this prop
|
||||||
|
"""
|
||||||
|
if bl_instance is None:
|
||||||
|
return None
|
||||||
|
# Create Non-Persistent Cache Entry
|
||||||
|
## Prefer explicit cache management to 'defaultdict'
|
||||||
|
if CACHE_NOPERSIST.get(bl_instance.instance_id) is None:
|
||||||
|
CACHE_NOPERSIST[bl_instance.instance_id] = {}
|
||||||
|
cache_nopersist = CACHE_NOPERSIST[bl_instance.instance_id]
|
||||||
|
|
||||||
|
# Try Hit on Non-Persistent Cache
|
||||||
|
if (value := cache_nopersist.get(self._bl_prop_name)) is not None:
|
||||||
|
return value
|
||||||
|
|
||||||
|
# Try Hit on Persistent Cache
|
||||||
|
## Hit: Fill Non-Persistent Cache
|
||||||
|
if (
|
||||||
|
self._persist
|
||||||
|
and (encoded_value := getattr(bl_instance, self._bl_prop_name)) != ''
|
||||||
|
):
|
||||||
|
value = decode_any(self._type, encoded_value)
|
||||||
|
cache_nopersist[self._bl_prop_name] = value
|
||||||
|
return value
|
||||||
|
|
||||||
|
# Compute Value
|
||||||
|
## Fill Non-Persistent Cache
|
||||||
|
## Fill Persistent Cache (maybe)
|
||||||
|
value = self._getter_method(bl_instance)
|
||||||
|
cache_nopersist[self._bl_prop_name] = value
|
||||||
|
if self._persist:
|
||||||
|
setattr(
|
||||||
|
bl_instance, self._bl_prop_name, ENCODER.encode(value).decode('utf-8')
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def __set__(self, bl_instance: BLInstance, value: typ.Any) -> None:
|
||||||
|
"""Runs the user-provided setter, after invalidating the caches.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- This invalidates all caches without re-filling them.
|
||||||
|
- The caches will be re-filled on the first `__get__` invocation, which may be slow due to having to run the getter method.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
bl_instance: The Blender object this prop
|
||||||
|
"""
|
||||||
|
if self._setter_method is None:
|
||||||
|
msg = f'Tried to set "{value}" to "{self.prop_name}" on "{bl_instance.bl_label}", but a setter was not defined'
|
||||||
|
raise NotImplementedError(msg)
|
||||||
|
|
||||||
|
# Invalidate Caches
|
||||||
|
self._invalidate_cache(bl_instance)
|
||||||
|
|
||||||
|
# Set the Value
|
||||||
|
self._setter_method(bl_instance, value)
|
||||||
|
|
||||||
|
def setter(self, setter_method: PropSetMethod) -> typ.Self:
|
||||||
|
"""Decorator to add a setter to the cached property.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The same descriptor, so that use of the same method name for defining a setter won't change the semantics of the attribute.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Without the decor
|
||||||
|
```python
|
||||||
|
class Test(bpy.types.Node):
|
||||||
|
bl_label = 'Default'
|
||||||
|
...
|
||||||
|
def method(self) -> str: return self.bl_label
|
||||||
|
attr = CachedBLProperty(getter_method=method, persist=False)
|
||||||
|
|
||||||
|
@attr.setter
|
||||||
|
def attr(self, value: str) -> None:
|
||||||
|
self.bl_label = 'Altered'
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
# Validate Setter Signature
|
||||||
|
setter_sig = inspect.signature(setter_method)
|
||||||
|
|
||||||
|
## Parameter Length
|
||||||
|
if (sig_len := len(setter_sig.parameters)) != 2: # noqa: PLR2004
|
||||||
|
msg = f'Setter method for "{self.prop_name}" should have 2 parameters, not "{sig_len}"'
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
## Parameter Value Type
|
||||||
|
if (sig_ret_type := setter_sig.return_annotation) is not None:
|
||||||
|
msg = f'Setter method for "{self.prop_name}" return value type "{sig_ret_type}", but it should be "None" (omitting an annotation does not imply "None")'
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
self._setter_method = setter_method
|
||||||
|
return self
|
||||||
|
|
||||||
|
def _invalidate_cache(self, bl_instance: BLInstance) -> None:
|
||||||
|
"""Invalidates all caches that might be storing the computed property value.
|
||||||
|
|
||||||
|
This is invoked by `__set__`.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
Will not delete the `bpy.props.StringProperty`; instead, it will be set to ''.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
bl_instance: The instance of the Blender object that contains this property.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
It is discouraged to run this directly, as any use-pattern that requires manually invalidating a property cache is **likely an anti-pattern**.
|
||||||
|
|
||||||
|
With that disclaimer, manual invocation looks like this:
|
||||||
|
```python
|
||||||
|
bl_instance.attr._invalidate_cache()
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
# Invalidate Non-Persistent Cache
|
||||||
|
if CACHE_NOPERSIST.get(bl_instance.instance_id) is not None:
|
||||||
|
CACHE_NOPERSIST[bl_instance.instance_id].pop(self._bl_prop_name, None)
|
||||||
|
|
||||||
|
# Invalidate Persistent Cache
|
||||||
|
if self._persist and getattr(bl_instance, self._bl_prop_name) != '':
|
||||||
|
setattr(bl_instance, self._bl_prop_name, '')
|
||||||
|
|
||||||
|
|
||||||
|
## TODO: How do we invalidate the data that the computed cached property depends on?
|
||||||
|
####################
|
||||||
|
# - Property Decorators
|
||||||
|
####################
|
||||||
|
def cached_bl_property(persist: bool = ...):
|
||||||
|
"""Decorator creating a descriptor that caches a computed attribute of a Blender node/socket.
|
||||||
|
|
||||||
|
Many such `bl_instance`s rely on fast access to computed, cached properties, for example to ensure that `draw()` remains effectively non-blocking.
|
||||||
|
It is also sometimes desired that this cache persist on `bl_instance`, ex. in the case of loose sockets or cached web data.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Unfortunately, `functools.cached_property` doesn't work, and can't handle persistance.
|
||||||
|
- Use `cached_attribute` instead if merely persisting the value is desired.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
persist: Whether or not to persist the cache value in the Blender object.
|
||||||
|
This should be used when the **source(s) of the computed value also persists with the Blender object**.
|
||||||
|
For example, this is especially helpful when caching information for use in `draw()` methods, so that reloading the file won't alter the cache.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
```python
|
||||||
|
class CustomNode(bpy.types.Node):
|
||||||
|
@bl_cache.cached(persist=True|False)
|
||||||
|
def computed_prop(self) -> ...: return ...
|
||||||
|
|
||||||
|
print(bl_instance.prop) ## Computes first time
|
||||||
|
print(bl_instance.prop) ## Cached (maybe persistently in a property, maybe not)
|
||||||
|
```
|
||||||
|
|
||||||
|
When
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(getter_method: typ.Callable[[BLInstance], None]) -> type:
|
||||||
|
return CachedBLProperty(getter_method=getter_method, persist=persist)
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Attribute Descriptor
|
||||||
|
####################
|
||||||
|
class BLField:
|
||||||
|
"""A descriptor that allows persisting arbitrary types in Blender objects, with cached reads."""
|
||||||
|
|
||||||
|
def __init__(self, default_value: typ.Any, triggers_prop_update: bool = True):
|
||||||
|
"""Initializes and sets the attribute to a given default value.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
default_value: The default value to use if the value is read before it's set.
|
||||||
|
trigger_prop_update: Whether to run `bl_instance.sync_prop(attr_name)` whenever value is set.
|
||||||
|
|
||||||
|
"""
|
||||||
|
log.debug(
|
||||||
|
'Initializing BLField (default_value=%s, triggers_prop_update=%s)',
|
||||||
|
str(default_value),
|
||||||
|
str(triggers_prop_update),
|
||||||
|
)
|
||||||
|
self._default_value: typ.Any = default_value
|
||||||
|
self._triggers_prop_update: bool = triggers_prop_update
|
||||||
|
|
||||||
|
def __set_name__(self, owner: type[BLInstance], name: str) -> None:
|
||||||
|
"""Sets up getters/setters for attribute access, and sets up a `CachedBLProperty` to internally utilize them.
|
||||||
|
|
||||||
|
Our getter/setter essentially reads/writes to a `bpy.props.StringProperty`, with
|
||||||
|
|
||||||
|
and use them as user-provided getter/setter to internally define a normal non-persistent `CachedBLProperty`.
|
||||||
|
As a result, we can reuse almost all of the logic in `CachedBLProperty`
|
||||||
|
|
||||||
|
Note:
|
||||||
|
Run by Python when setting an instance of this class to an attribute.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
owner: The class that contains an attribute assigned to an instance of this descriptor.
|
||||||
|
name: The name of the attribute that an instance of descriptor was assigned to.
|
||||||
|
"""
|
||||||
|
# Compute Name and Type of Property
|
||||||
|
## Also compute the internal
|
||||||
|
attr_name = name
|
||||||
|
bl_attr_name = f'blattr__{name}'
|
||||||
|
if (AttrType := inspect.get_annotations(owner).get(name)) is None: # noqa: N806
|
||||||
|
msg = f'BLField "{self.prop_name}" must define a type annotation, but doesn\'t.'
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
# Define Blender Property (w/Update Sync)
|
||||||
|
encoded_default_value = ENCODER.encode(self._default_value).decode('utf-8')
|
||||||
|
log.debug(
|
||||||
|
'%s set to StringProperty w/default "%s" and no_update="%s"',
|
||||||
|
bl_attr_name,
|
||||||
|
encoded_default_value,
|
||||||
|
str(not self._triggers_prop_update),
|
||||||
|
)
|
||||||
|
owner.set_prop(
|
||||||
|
bl_attr_name,
|
||||||
|
bpy.props.StringProperty,
|
||||||
|
name=f'Encoded Attribute for {attr_name}',
|
||||||
|
default=encoded_default_value,
|
||||||
|
no_update=not self._triggers_prop_update,
|
||||||
|
update_with_name=attr_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
## Getter:
|
||||||
|
## 1. Initialize bpy.props.StringProperty to Default (if undefined).
|
||||||
|
## 2. Retrieve bpy.props.StringProperty string.
|
||||||
|
## 3. Decode using annotated type.
|
||||||
|
def getter(_self: BLInstance) -> AttrType:
|
||||||
|
return decode_any(AttrType, getattr(_self, bl_attr_name))
|
||||||
|
|
||||||
|
## Setter:
|
||||||
|
## 1. Initialize bpy.props.StringProperty to Default (if undefined).
|
||||||
|
## 3. Encode value (implicitly using the annotated type).
|
||||||
|
## 2. Set bpy.props.StringProperty string.
|
||||||
|
def setter(_self: BLInstance, value: AttrType) -> None:
|
||||||
|
encoded_value = ENCODER.encode(value).decode('utf-8')
|
||||||
|
log.debug(
|
||||||
|
'Writing BLField attr "%s" w/encoded value: %s',
|
||||||
|
bl_attr_name,
|
||||||
|
encoded_value,
|
||||||
|
)
|
||||||
|
setattr(_self, bl_attr_name, encoded_value)
|
||||||
|
|
||||||
|
# Initialize CachedBLProperty w/Getter and Setter
|
||||||
|
## This is the usual descriptor assignment procedure.
|
||||||
|
self._cached_bl_property = CachedBLProperty(getter_method=getter, persist=False)
|
||||||
|
self._cached_bl_property.__set_name__(owner, name)
|
||||||
|
self._cached_bl_property.setter(setter)
|
||||||
|
|
||||||
|
def __get__(
|
||||||
|
self, bl_instance: BLInstance | None, owner: type[BLInstance]
|
||||||
|
) -> typ.Any:
|
||||||
|
return self._cached_bl_property.__get__(bl_instance, owner)
|
||||||
|
|
||||||
|
def __set__(self, bl_instance: BLInstance, value: typ.Any) -> None:
|
||||||
|
self._cached_bl_property.__set__(bl_instance, value)
|
|
@ -220,7 +220,7 @@ def _writable_bl_socket_value(
|
||||||
_bl_socket_value = value
|
_bl_socket_value = value
|
||||||
|
|
||||||
# Compute Blender Socket Value
|
# Compute Blender Socket Value
|
||||||
if isinstance(_bl_socket_value, sp.Basic):
|
if isinstance(_bl_socket_value, sp.Basic | sp.MatrixBase):
|
||||||
bl_socket_value = spux.sympy_to_python(_bl_socket_value)
|
bl_socket_value = spux.sympy_to_python(_bl_socket_value)
|
||||||
else:
|
else:
|
||||||
bl_socket_value = _bl_socket_value
|
bl_socket_value = _bl_socket_value
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import enum
|
||||||
import pydantic as pyd
|
import pydantic as pyd
|
||||||
import typing_extensions as pytypes_ext
|
import typing_extensions as pytypes_ext
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
import enum
|
import enum
|
||||||
|
import typing as typ
|
||||||
|
|
||||||
|
import typing_extensions as typx
|
||||||
|
|
||||||
|
|
||||||
class DataFlowAction(enum.StrEnum):
|
class DataFlowAction(enum.StrEnum):
|
||||||
|
@ -7,8 +10,37 @@ class DataFlowAction(enum.StrEnum):
|
||||||
DisableLock = 'disable_lock'
|
DisableLock = 'disable_lock'
|
||||||
|
|
||||||
# Value
|
# Value
|
||||||
|
OutputRequested = 'output_requested'
|
||||||
DataChanged = 'value_changed'
|
DataChanged = 'value_changed'
|
||||||
|
|
||||||
# Previewing
|
# Previewing
|
||||||
ShowPreview = 'show_preview'
|
ShowPreview = 'show_preview'
|
||||||
ShowPlot = 'show_plot'
|
ShowPlot = 'show_plot'
|
||||||
|
|
||||||
|
def trigger_direction(action: typ.Self) -> typx.Literal['input', 'output']:
|
||||||
|
"""When a given action is triggered, all sockets/nodes/... in this direction should be recursively triggered.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
action: The action for which to retrieve the trigger direction.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The trigger direction, which can be used ex. in nodes to select `node.inputs` or `node.outputs`.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
DataFlowAction.EnableLock: 'input',
|
||||||
|
DataFlowAction.DisableLock: 'input',
|
||||||
|
DataFlowAction.DataChanged: 'output',
|
||||||
|
DataFlowAction.OutputRequested: 'input',
|
||||||
|
DataFlowAction.ShowPreview: 'input',
|
||||||
|
DataFlowAction.ShowPlot: 'input',
|
||||||
|
}[action]
|
||||||
|
|
||||||
|
def stop_if_no_event_methods(action: typ.Self) -> bool:
|
||||||
|
return {
|
||||||
|
DataFlowAction.EnableLock: False,
|
||||||
|
DataFlowAction.DisableLock: False,
|
||||||
|
DataFlowAction.DataChanged: True,
|
||||||
|
DataFlowAction.OutputRequested: True,
|
||||||
|
DataFlowAction.ShowPreview: False,
|
||||||
|
DataFlowAction.ShowPlot: False,
|
||||||
|
}[action]
|
||||||
|
|
|
@ -5,7 +5,6 @@ import typing as typ
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
|
|
||||||
# import colour ## TODO
|
# import colour ## TODO
|
||||||
import jax
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import sympy as sp
|
import sympy as sp
|
||||||
import sympy.physics.units as spu
|
import sympy.physics.units as spu
|
||||||
|
@ -77,6 +76,21 @@ class DataFlowKind(enum.StrEnum):
|
||||||
LazyValueRange = enum.auto()
|
LazyValueRange = enum.auto()
|
||||||
LazyValueSpectrum = enum.auto()
|
LazyValueSpectrum = enum.auto()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def scale_to_unit_system(cls, kind: typ.Self, value, socket_type, unit_system):
|
||||||
|
if kind == cls.Value:
|
||||||
|
return spux.sympy_to_python(
|
||||||
|
spux.scale_to_unit(
|
||||||
|
value,
|
||||||
|
unit_system[socket_type],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if kind == cls.LazyValueRange:
|
||||||
|
return value.rescale_to_unit(unit_system[socket_type])
|
||||||
|
|
||||||
|
msg = 'Tried to scale unknown kind'
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Data Structures: Capabilities
|
# - Data Structures: Capabilities
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
#from .managed_bl_empty import ManagedBLEmpty
|
import typing as typ
|
||||||
|
|
||||||
|
# from .managed_bl_empty import ManagedBLEmpty
|
||||||
from .managed_bl_image import ManagedBLImage
|
from .managed_bl_image import ManagedBLImage
|
||||||
|
|
||||||
# from .managed_bl_collection import ManagedBLCollection
|
# from .managed_bl_collection import ManagedBLCollection
|
||||||
|
@ -8,6 +10,8 @@ from .managed_bl_mesh import ManagedBLMesh
|
||||||
# from .managed_bl_volume import ManagedBLVolume
|
# from .managed_bl_volume import ManagedBLVolume
|
||||||
from .managed_bl_modifier import ManagedBLModifier
|
from .managed_bl_modifier import ManagedBLModifier
|
||||||
|
|
||||||
|
ManagedObj: typ.TypeAlias = ManagedBLImage | ManagedBLMesh | ManagedBLModifier
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
#'ManagedBLEmpty',
|
#'ManagedBLEmpty',
|
||||||
'ManagedBLImage',
|
'ManagedBLImage',
|
||||||
|
@ -17,3 +21,5 @@ __all__ = [
|
||||||
#'ManagedBLVolume',
|
#'ManagedBLVolume',
|
||||||
'ManagedBLModifier',
|
'ManagedBLModifier',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
## REMEMBER: Add the appropriate entry to the bl_cache.DECODER
|
||||||
|
|
|
@ -31,6 +31,13 @@ class ManagedBLMesh(ct.schemas.ManagedObj):
|
||||||
'Changing BLMesh w/Name "%s" to Name "%s"', self._bl_object_name, value
|
'Changing BLMesh w/Name "%s" to Name "%s"', self._bl_object_name, value
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self._bl_object_name == value:
|
||||||
|
## TODO: This is a workaround.
|
||||||
|
## Really, we can't tell if a name is valid by searching objects.
|
||||||
|
## Since, after all, other managedobjs may have taken a name..
|
||||||
|
## ...but not yet made an object that has it.
|
||||||
|
return
|
||||||
|
|
||||||
if (bl_object := bpy.data.objects.get(value)) is None:
|
if (bl_object := bpy.data.objects.get(value)) is None:
|
||||||
log.info(
|
log.info(
|
||||||
'Desired BLMesh Name "%s" Not Taken',
|
'Desired BLMesh Name "%s" Not Taken',
|
||||||
|
|
|
@ -130,6 +130,7 @@ def write_modifier_geonodes(
|
||||||
bl_modifier[iface_id] = float(bl_socket_value)
|
bl_modifier[iface_id] = float(bl_socket_value)
|
||||||
modifier_altered = True
|
modifier_altered = True
|
||||||
else:
|
else:
|
||||||
|
## TODO: Whitelist what can be here. I'm done with the TypeErrors.
|
||||||
bl_modifier[iface_id] = bl_socket_value
|
bl_modifier[iface_id] = bl_socket_value
|
||||||
modifier_altered = True
|
modifier_altered = True
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import contextlib
|
||||||
import typing as typ
|
import typing as typ
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
@ -15,47 +16,176 @@ MemAddr = int
|
||||||
|
|
||||||
|
|
||||||
class DeltaNodeLinkCache(typ.TypedDict):
|
class DeltaNodeLinkCache(typ.TypedDict):
|
||||||
|
"""Describes change in the `NodeLink`s of a node tree.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
added: Set of pointers to added node tree links.
|
||||||
|
removed: Set of pointers to removed node tree links.
|
||||||
|
"""
|
||||||
|
|
||||||
added: set[MemAddr]
|
added: set[MemAddr]
|
||||||
removed: set[MemAddr]
|
removed: set[MemAddr]
|
||||||
|
|
||||||
|
|
||||||
class NodeLinkCache:
|
class NodeLinkCache:
|
||||||
|
"""A pointer-based cache of node links in a node tree.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
_node_tree: Reference to the owning node tree.
|
||||||
|
link_ptrs_as_links:
|
||||||
|
link_ptrs: Pointers (as in integer memory adresses) to `NodeLink`s.
|
||||||
|
link_ptrs_as_links: Map from pointers to actual `NodeLink`s.
|
||||||
|
link_ptrs_from_sockets: Map from pointers to `NodeSocket`s, representing the source of each `NodeLink`.
|
||||||
|
link_ptrs_from_sockets: Map from pointers to `NodeSocket`s, representing the destination of each `NodeLink`.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, node_tree: bpy.types.NodeTree):
|
def __init__(self, node_tree: bpy.types.NodeTree):
|
||||||
# Initialize Parameters
|
"""Initialize the cache from a node tree.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
node_tree: The Blender node tree whose `NodeLink`s will be cached.
|
||||||
|
"""
|
||||||
self._node_tree = node_tree
|
self._node_tree = node_tree
|
||||||
self.link_ptrs_to_links = {}
|
|
||||||
self.link_ptrs = set()
|
# Link PTR and PTR->REF
|
||||||
self.link_ptrs_from_sockets = {}
|
self.link_ptrs: set[MemAddr] = set()
|
||||||
self.link_ptrs_to_sockets = {}
|
self.link_ptrs_as_links: dict[MemAddr, bpy.types.NodeLink] = {}
|
||||||
|
|
||||||
|
# Socket PTR and PTR->REF
|
||||||
|
self.socket_ptrs: set[MemAddr] = set()
|
||||||
|
self.socket_ptrs_as_sockets: dict[MemAddr, bpy.types.NodeSocket] = {}
|
||||||
|
self.socket_ptr_refcount: dict[MemAddr, int] = {}
|
||||||
|
|
||||||
|
# Link PTR -> Socket PTR
|
||||||
|
self.link_ptrs_as_from_socket_ptrs: dict[MemAddr, MemAddr] = {}
|
||||||
|
self.link_ptrs_as_to_socket_ptrs: dict[MemAddr, MemAddr] = {}
|
||||||
|
|
||||||
# Fill Cache
|
# Fill Cache
|
||||||
self.regenerate()
|
self.regenerate()
|
||||||
|
|
||||||
def remove(self, link_ptrs: set[MemAddr]) -> None:
|
def remove_link(self, link_ptr: MemAddr) -> None:
|
||||||
for link_ptr in link_ptrs:
|
"""Removes a link pointer from the cache, indicating that the link doesn't exist anymore.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- **DOES NOT** remove PTR->REF dictionary entries
|
||||||
|
- Invoking this method directly causes the removed node links to not be reported as "removed" by `NodeLinkCache.regenerate()`.
|
||||||
|
- This **must** be done whenever a node link is deleted.
|
||||||
|
- Failure to do so may result in a segmentation fault at arbitrary future time.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
link_ptrs: Pointers to remove from the cache.
|
||||||
|
"""
|
||||||
self.link_ptrs.remove(link_ptr)
|
self.link_ptrs.remove(link_ptr)
|
||||||
self.link_ptrs_to_links.pop(link_ptr, None)
|
self.link_ptrs_as_links.pop(link_ptr)
|
||||||
|
|
||||||
|
def remove_sockets_by_link_ptr(self, link_ptr: MemAddr) -> None:
|
||||||
|
"""Removes a single pointer's reference to its from/to sockets."""
|
||||||
|
from_socket_ptr = self.link_ptrs_as_from_socket_ptrs.pop(link_ptr, None)
|
||||||
|
to_socket_ptr = self.link_ptrs_as_to_socket_ptrs.pop(link_ptr, None)
|
||||||
|
|
||||||
|
for socket_ptr in [from_socket_ptr, to_socket_ptr]:
|
||||||
|
if socket_ptr is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Delete w/RefCount Respect
|
||||||
|
if self.socket_ptr_refcount[socket_ptr] == 1:
|
||||||
|
self.socket_ptrs.remove(socket_ptr)
|
||||||
|
self.socket_ptrs_as_sockets.pop(socket_ptr)
|
||||||
|
self.socket_ptr_refcount.pop(socket_ptr)
|
||||||
|
else:
|
||||||
|
self.socket_ptr_refcount[socket_ptr] -= 1
|
||||||
|
|
||||||
def regenerate(self) -> DeltaNodeLinkCache:
|
def regenerate(self) -> DeltaNodeLinkCache:
|
||||||
current_link_ptrs_to_links = {
|
"""Regenerates the cache from the internally-linked node tree.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- This is designed to run within the `update()` invocation of the node tree.
|
||||||
|
- This should be a very fast function, since it is called so much.
|
||||||
|
"""
|
||||||
|
# Compute All NodeLink Pointers
|
||||||
|
all_link_ptrs_as_links = {
|
||||||
link.as_pointer(): link for link in self._node_tree.links
|
link.as_pointer(): link for link in self._node_tree.links
|
||||||
}
|
}
|
||||||
current_link_ptrs = set(current_link_ptrs_to_links.keys())
|
all_link_ptrs = set(all_link_ptrs_as_links.keys())
|
||||||
|
|
||||||
# Compute Delta
|
# Compute Added/Removed Links
|
||||||
added_link_ptrs = current_link_ptrs - self.link_ptrs
|
added_link_ptrs = all_link_ptrs - self.link_ptrs
|
||||||
removed_link_ptrs = self.link_ptrs - current_link_ptrs
|
removed_link_ptrs = self.link_ptrs - all_link_ptrs
|
||||||
|
|
||||||
# Update Caches Incrementally
|
# Edge Case: 'from_socket' Reassignment
|
||||||
self.remove(removed_link_ptrs)
|
## (Reverse engineered) When all:
|
||||||
|
## - Created a new link between the same two nodes.
|
||||||
|
## - Matching 'to_socket'.
|
||||||
|
## - Non-matching 'from_socket' on the same node.
|
||||||
|
## -> THEN the link_ptr will not change, but the from_socket ptr should.
|
||||||
|
if len(added_link_ptrs) == 0 and len(removed_link_ptrs) == 0:
|
||||||
|
# Find the Link w/Reassigned 'from_socket' PTR
|
||||||
|
## A bit of a performance hit from the search, but it's an edge case.
|
||||||
|
_link_ptr_as_from_socket_ptrs = {
|
||||||
|
link_ptr: (
|
||||||
|
from_socket_ptr,
|
||||||
|
all_link_ptrs_as_links[link_ptr].from_socket.as_pointer(),
|
||||||
|
)
|
||||||
|
for link_ptr, from_socket_ptr in self.link_ptrs_as_from_socket_ptrs.items()
|
||||||
|
if all_link_ptrs_as_links[link_ptr].from_socket.as_pointer()
|
||||||
|
!= from_socket_ptr
|
||||||
|
}
|
||||||
|
|
||||||
|
# Completely Remove the Old Link (w/Reassigned 'from_socket')
|
||||||
|
## This effectively reclassifies the edge case as a normal 're-add'.
|
||||||
|
for link_ptr in _link_ptr_as_from_socket_ptrs:
|
||||||
|
log.info(
|
||||||
|
'Edge-Case - "from_socket" Reassigned in NodeLink w/o New NodeLink Pointer: %s',
|
||||||
|
link_ptr,
|
||||||
|
)
|
||||||
|
self.remove_link(link_ptr)
|
||||||
|
self.remove_sockets_by_link_ptr(link_ptr)
|
||||||
|
|
||||||
|
# Recompute Added/Removed Links
|
||||||
|
## The algorithm will now detect an "added link".
|
||||||
|
added_link_ptrs = all_link_ptrs - self.link_ptrs
|
||||||
|
removed_link_ptrs = self.link_ptrs - all_link_ptrs
|
||||||
|
|
||||||
|
# Shuffle Cache based on Change in Links
|
||||||
|
## Remove Entries for Removed Pointers
|
||||||
|
for removed_link_ptr in removed_link_ptrs:
|
||||||
|
self.remove_link(removed_link_ptr)
|
||||||
|
## User must manually call 'remove_socket_by_link_ptr' later.
|
||||||
|
## For now, leave dangling socket information by-link.
|
||||||
|
|
||||||
|
# Add New Link Pointers
|
||||||
self.link_ptrs |= added_link_ptrs
|
self.link_ptrs |= added_link_ptrs
|
||||||
for link_ptr in added_link_ptrs:
|
for link_ptr in added_link_ptrs:
|
||||||
link = current_link_ptrs_to_links[link_ptr]
|
# Add Link PTR->REF
|
||||||
|
new_link = all_link_ptrs_as_links[link_ptr]
|
||||||
|
self.link_ptrs_as_links[link_ptr] = new_link
|
||||||
|
|
||||||
self.link_ptrs_to_links[link_ptr] = link
|
# Retrieve Link Socket Information
|
||||||
self.link_ptrs_from_sockets[link_ptr] = link.from_socket
|
from_socket = new_link.from_socket
|
||||||
self.link_ptrs_to_sockets[link_ptr] = link.to_socket
|
from_socket_ptr = from_socket.as_pointer()
|
||||||
|
to_socket = new_link.to_socket
|
||||||
|
to_socket_ptr = to_socket.as_pointer()
|
||||||
|
|
||||||
|
# Add Socket PTR, PTR -> REF
|
||||||
|
for socket_ptr, bl_socket in zip( # noqa: B905
|
||||||
|
[from_socket_ptr, to_socket_ptr],
|
||||||
|
[from_socket, to_socket],
|
||||||
|
):
|
||||||
|
# Increment RefCount of Socket PTR
|
||||||
|
## This happens if another link also uses the same socket.
|
||||||
|
## 1. An output socket links to several inputs.
|
||||||
|
## 2. A multi-input socket links from several inputs.
|
||||||
|
if socket_ptr in self.socket_ptr_refcount:
|
||||||
|
self.socket_ptr_refcount[socket_ptr] += 1
|
||||||
|
else:
|
||||||
|
## RefCount == 0: Add PTR, PTR -> REF
|
||||||
|
self.socket_ptrs.add(socket_ptr)
|
||||||
|
self.socket_ptrs_as_sockets[socket_ptr] = bl_socket
|
||||||
|
self.socket_ptr_refcount[socket_ptr] = 1
|
||||||
|
|
||||||
|
# Add Link PTR -> Socket PTR
|
||||||
|
self.link_ptrs_as_from_socket_ptrs[link_ptr] = from_socket_ptr
|
||||||
|
self.link_ptrs_as_to_socket_ptrs[link_ptr] = to_socket_ptr
|
||||||
|
|
||||||
return {'added': added_link_ptrs, 'removed': removed_link_ptrs}
|
return {'added': added_link_ptrs, 'removed': removed_link_ptrs}
|
||||||
|
|
||||||
|
@ -71,20 +201,42 @@ class MaxwellSimTree(bpy.types.NodeTree):
|
||||||
####################
|
####################
|
||||||
# - Lock Methods
|
# - Lock Methods
|
||||||
####################
|
####################
|
||||||
def unlock_all(self):
|
def unlock_all(self) -> None:
|
||||||
|
"""Unlock all nodes in the node tree, making them editable."""
|
||||||
|
log.info('Unlocking All Nodes in NodeTree "%s"', self.bl_label)
|
||||||
for node in self.nodes:
|
for node in self.nodes:
|
||||||
node.locked = False
|
node.locked = False
|
||||||
for bl_socket in [*node.inputs, *node.outputs]:
|
for bl_socket in [*node.inputs, *node.outputs]:
|
||||||
bl_socket.locked = False
|
bl_socket.locked = False
|
||||||
|
|
||||||
def unpreview_all(self):
|
@contextlib.contextmanager
|
||||||
log.info('Disabling All 3D Previews')
|
def repreview_all(self) -> None:
|
||||||
for node in self.nodes:
|
all_nodes_with_preview_active = {
|
||||||
if node.preview_active:
|
node.instance_id: node for node in self.nodes if node.preview_active
|
||||||
node.preview_active = False
|
}
|
||||||
|
self.is_currently_repreviewing = True
|
||||||
|
self.newly_previewed_nodes = {}
|
||||||
|
|
||||||
for bl_object in preview_collection().objects.values():
|
try:
|
||||||
preview_collection().objects.unlink(bl_object)
|
yield
|
||||||
|
finally:
|
||||||
|
for dangling_previewed_node in [
|
||||||
|
node
|
||||||
|
for node_instance_id, node in all_nodes_with_preview_active.items()
|
||||||
|
if node_instance_id not in self.newly_previewed_nodes
|
||||||
|
]:
|
||||||
|
# log.debug(
|
||||||
|
# 'Removing Dangling Preview of Node "{%s}"',
|
||||||
|
# str(dangling_previewed_node),
|
||||||
|
# )
|
||||||
|
dangling_previewed_node.preview_active = False
|
||||||
|
|
||||||
|
def report_show_preview(self, node: bpy.types.Node) -> None:
|
||||||
|
if (
|
||||||
|
hasattr(self, 'is_currently_repreviewing')
|
||||||
|
and self.is_currently_repreviewing
|
||||||
|
):
|
||||||
|
self.newly_previewed_nodes[node.instance_id] = node
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Init Methods
|
# - Init Methods
|
||||||
|
@ -94,11 +246,10 @@ class MaxwellSimTree(bpy.types.NodeTree):
|
||||||
|
|
||||||
It's a bit of a "fake" function - in practicality, it's triggered on the first update() function.
|
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
|
if hasattr(self, 'node_link_cache'):
|
||||||
if hasattr(self, '_node_link_cache'):
|
self.node_link_cache.regenerate()
|
||||||
self._node_link_cache.regenerate()
|
|
||||||
else:
|
else:
|
||||||
self._node_link_cache = NodeLinkCache(self)
|
self.node_link_cache = NodeLinkCache(self)
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Update Methods
|
# - Update Methods
|
||||||
|
@ -106,23 +257,35 @@ class MaxwellSimTree(bpy.types.NodeTree):
|
||||||
def sync_node_removed(self, node: bpy.types.Node):
|
def sync_node_removed(self, node: bpy.types.Node):
|
||||||
"""Run by `Node.free()` when a node is being removed.
|
"""Run by `Node.free()` when a node is being removed.
|
||||||
|
|
||||||
|
ONLY input socket links are removed from the NodeLink cache.
|
||||||
|
- `self.update()` handles link-removal from existing nodes.
|
||||||
|
- `self.update()` can't handle link-removal
|
||||||
|
|
||||||
Removes node input links from the internal cache (so we don't attempt to update non-existant sockets).
|
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
|
## ONLY Input Socket Links are Removed from the NodeLink Cache
|
||||||
## - update() handles link-removal from still-existing node just fine.
|
## - update() handles link-removal from still-existing node just fine.
|
||||||
## - update() does NOT handle link-removal of non-existant nodes.
|
## - update() does NOT handle link-removal of non-existant nodes.
|
||||||
|
for bl_socket in list(node.inputs.values()) + list(node.outputs.values()):
|
||||||
|
# Compute About-To-Be-Freed Link Ptrs
|
||||||
|
link_ptrs = {link.as_pointer() for link in bl_socket.links}
|
||||||
|
|
||||||
def update(self):
|
if link_ptrs:
|
||||||
"""Run by Blender when 'something changes' in the node tree.
|
for link_ptr in link_ptrs:
|
||||||
|
self.node_link_cache.remove_link(link_ptr)
|
||||||
|
self.node_link_cache.remove_sockets_by_link_ptr(link_ptr)
|
||||||
|
|
||||||
Updates an internal node link cache, then updates sockets that just lost/gained an input link.
|
def update(self) -> None:
|
||||||
|
"""Monitors all changes to the node tree, potentially responding with appropriate callbacks.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Run by Blender when "anything" changes in the node tree.
|
||||||
|
- Responds to node link changes with callbacks, with the help of a performant node link cache.
|
||||||
"""
|
"""
|
||||||
if not hasattr(self, '_node_link_cache'):
|
if not hasattr(self, 'ignore_update'):
|
||||||
|
self.ignore_update = False
|
||||||
|
|
||||||
|
if not hasattr(self, 'node_link_cache'):
|
||||||
self.on_load()
|
self.on_load()
|
||||||
## We presume update() is run before the first link is altered.
|
## We presume update() is run before the first link is altered.
|
||||||
## - Else, the first link of the session will not update caches.
|
## - Else, the first link of the session will not update caches.
|
||||||
|
@ -130,51 +293,82 @@ class MaxwellSimTree(bpy.types.NodeTree):
|
||||||
## - Therefore, self.on_load() is also called as a load_post handler.
|
## - Therefore, self.on_load() is also called as a load_post handler.
|
||||||
return
|
return
|
||||||
|
|
||||||
# Compute Changes to NodeLink Cache
|
# Ignore Update
|
||||||
delta_links = self._node_link_cache.regenerate()
|
## Manually set to implement link corrections w/o recursion.
|
||||||
|
if self.ignore_update:
|
||||||
|
return
|
||||||
|
|
||||||
link_alterations = {
|
# Compute Changes to Node Links
|
||||||
|
delta_links = self.node_link_cache.regenerate()
|
||||||
|
|
||||||
|
link_corrections = {
|
||||||
'to_remove': [],
|
'to_remove': [],
|
||||||
'to_add': [],
|
'to_add': [],
|
||||||
}
|
}
|
||||||
for link_ptr in delta_links['removed']:
|
for link_ptr in delta_links['removed']:
|
||||||
from_socket = self._node_link_cache.link_ptrs_from_sockets[link_ptr]
|
# Retrieve Link PTR -> From/To Socket PTR
|
||||||
to_socket = self._node_link_cache.link_ptrs_to_sockets[link_ptr]
|
## We don't know if they exist yet.
|
||||||
|
from_socket_ptr = self.node_link_cache.link_ptrs_as_from_socket_ptrs[
|
||||||
|
link_ptr
|
||||||
|
]
|
||||||
|
to_socket_ptr = self.node_link_cache.link_ptrs_as_to_socket_ptrs[link_ptr]
|
||||||
|
|
||||||
# Update Socket Caches
|
# Check Existance of From/To Socket
|
||||||
self._node_link_cache.link_ptrs_from_sockets.pop(link_ptr, None)
|
## `Node.free()` must report removed sockets, so this here works.
|
||||||
self._node_link_cache.link_ptrs_to_sockets.pop(link_ptr, None)
|
## If Both Exist: 'to_socket' may "non-consent" to the link removal.
|
||||||
|
if (
|
||||||
|
from_socket_ptr in self.node_link_cache.socket_ptrs
|
||||||
|
and to_socket_ptr in self.node_link_cache.socket_ptrs
|
||||||
|
):
|
||||||
|
# Retrieve 'from_socket'/'to_socket' REF
|
||||||
|
from_socket = self.node_link_cache.socket_ptrs_as_sockets[
|
||||||
|
from_socket_ptr
|
||||||
|
]
|
||||||
|
to_socket = self.node_link_cache.socket_ptrs_as_sockets[to_socket_ptr]
|
||||||
|
|
||||||
# Trigger Report Chain on Socket that Just Lost a Link
|
# Ask 'to_socket' for Consent to Remove Link
|
||||||
## Aka. Forward-Refresh Caches Relying on Linkage
|
## The link has already been removed, but we can fix that.
|
||||||
if not (consent_removal := to_socket.sync_link_removed(from_socket)):
|
## If NO: Queue re-adding the link (safe since the sockets exist)
|
||||||
# Did Not Consent to Removal: Queue Add Link
|
## TODO: Crash if deleting removing linked loose sockets.
|
||||||
link_alterations['to_add'].append((from_socket, to_socket))
|
consent_removal = to_socket.sync_link_removed(from_socket)
|
||||||
|
if not consent_removal:
|
||||||
|
link_corrections['to_add'].append((from_socket, to_socket))
|
||||||
|
|
||||||
|
# Ensure Removal of Socket PTRs, PTRs->REFs
|
||||||
|
self.node_link_cache.remove_sockets_by_link_ptr(link_ptr)
|
||||||
|
|
||||||
for link_ptr in delta_links['added']:
|
for link_ptr in delta_links['added']:
|
||||||
link = self._node_link_cache.link_ptrs_to_links.get(link_ptr)
|
# Retrieve Link Reference
|
||||||
if link is None:
|
link = self.node_link_cache.link_ptrs_as_links[link_ptr]
|
||||||
continue
|
|
||||||
|
|
||||||
# Trigger Report Chain on Socket that Just Gained a Link
|
# Ask 'to_socket' for Consent to Remove Link
|
||||||
## Aka. Forward-Refresh Caches Relying on Linkage
|
## The link has already been added, but we can fix that.
|
||||||
|
## If NO: Queue re-adding the link (safe since the sockets exist)
|
||||||
|
consent_added = link.to_socket.sync_link_added(link)
|
||||||
|
if not consent_added:
|
||||||
|
link_corrections['to_remove'].append(link)
|
||||||
|
|
||||||
if not (consent_added := link.to_socket.sync_link_added(link)):
|
# Link Corrections
|
||||||
# Did Not Consent to Addition: Queue Remove Link
|
## ADD: Links that 'to_socket' don't want removed.
|
||||||
link_alterations['to_remove'].append(link)
|
## REMOVE: Links that 'to_socket' don't want added.
|
||||||
|
## NOTE: Both remove() and new() recursively triggers update().
|
||||||
# Execute Queued Operations
|
for link in link_corrections['to_remove']:
|
||||||
## - Especially undoing undesirable link changes.
|
self.ignore_update = True
|
||||||
## - This is important for locked graphs, whose links must not change.
|
self.links.remove(link) ## Recursively triggers update()
|
||||||
for link in link_alterations['to_remove']:
|
self.ignore_update = False
|
||||||
self.links.remove(link)
|
for from_socket, to_socket in link_corrections['to_add']:
|
||||||
for from_socket, to_socket in link_alterations['to_add']:
|
## 'to_socket' and 'from_socket' are guaranteed to exist.
|
||||||
|
self.ignore_update = True
|
||||||
self.links.new(from_socket, to_socket)
|
self.links.new(from_socket, to_socket)
|
||||||
|
self.ignore_update = False
|
||||||
|
|
||||||
# If Queued Operations: Regenerate Cache
|
# Regenerate on Corrections
|
||||||
## - This prevents the next update() from picking up on alterations.
|
## Prevents next update() from trying to correct the corrections.
|
||||||
if link_alterations['to_remove'] or link_alterations['to_add']:
|
## We must remember to trigger '.remove_sockets_by_link_ptr'
|
||||||
self._node_link_cache.regenerate()
|
if link_corrections['to_remove'] or link_corrections['to_add']:
|
||||||
|
delta_links = self.node_link_cache.regenerate()
|
||||||
|
for link_ptr in delta_links['removed']:
|
||||||
|
self.node_link_cache.remove_sockets_by_link_ptr(link_ptr)
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
|
|
@ -13,14 +13,15 @@ CACHE_SIM_DATA = {}
|
||||||
|
|
||||||
|
|
||||||
class ExtractDataNode(base.MaxwellSimNode):
|
class ExtractDataNode(base.MaxwellSimNode):
|
||||||
"""Node for visualizing simulation data, by querying its monitors."""
|
"""Node for extracting data from other objects."""
|
||||||
|
|
||||||
node_type = ct.NodeType.ExtractData
|
node_type = ct.NodeType.ExtractData
|
||||||
bl_label = 'Extract Data'
|
bl_label = 'Extract'
|
||||||
|
|
||||||
input_socket_sets: typ.ClassVar = {
|
input_socket_sets: typ.ClassVar = {
|
||||||
'Sim Data': {'Sim Data': sockets.MaxwellFDTDSimDataSocketDef()},
|
'Sim Data': {'Sim Data': sockets.MaxwellFDTDSimDataSocketDef()},
|
||||||
'Field Data': {'Field Data': sockets.AnySocketDef()},
|
'Field Data': {'Field Data': sockets.AnySocketDef()},
|
||||||
|
'Flux Data': {'Flux Data': sockets.AnySocketDef()},
|
||||||
}
|
}
|
||||||
output_sockets: typ.ClassVar = {
|
output_sockets: typ.ClassVar = {
|
||||||
'Data': sockets.AnySocketDef(),
|
'Data': sockets.AnySocketDef(),
|
||||||
|
@ -192,6 +193,20 @@ class ExtractDataNode(base.MaxwellSimNode):
|
||||||
elif not self.inputs['Field Data'].is_linked and self.cache__components:
|
elif not self.inputs['Field Data'].is_linked and self.cache__components:
|
||||||
self.cache__components = ''
|
self.cache__components = ''
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Flux Data
|
||||||
|
####################
|
||||||
|
|
||||||
|
def draw_props__flux_data(
|
||||||
|
self, _: bpy.types.Context, col: bpy.types.UILayout
|
||||||
|
) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def draw_info__flux_data(
|
||||||
|
self, _: bpy.types.Context, col: bpy.types.UILayout
|
||||||
|
) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Global
|
# - Global
|
||||||
####################
|
####################
|
||||||
|
@ -200,12 +215,16 @@ class ExtractDataNode(base.MaxwellSimNode):
|
||||||
self.draw_props__sim_data(context, col)
|
self.draw_props__sim_data(context, col)
|
||||||
if self.active_socket_set == 'Field Data':
|
if self.active_socket_set == 'Field Data':
|
||||||
self.draw_props__field_data(context, col)
|
self.draw_props__field_data(context, col)
|
||||||
|
if self.active_socket_set == 'Flux Data':
|
||||||
|
self.draw_props__flux_data(context, col)
|
||||||
|
|
||||||
def draw_info(self, context: bpy.types.Context, col: bpy.types.UILayout) -> None:
|
def draw_info(self, context: bpy.types.Context, col: bpy.types.UILayout) -> None:
|
||||||
if self.active_socket_set == 'Sim Data':
|
if self.active_socket_set == 'Sim Data':
|
||||||
self.draw_info__sim_data(context, col)
|
self.draw_info__sim_data(context, col)
|
||||||
if self.active_socket_set == 'Field Data':
|
if self.active_socket_set == 'Field Data':
|
||||||
self.draw_info__field_data(context, col)
|
self.draw_info__field_data(context, col)
|
||||||
|
if self.active_socket_set == 'Flux Data':
|
||||||
|
self.draw_info__flux_data(context, col)
|
||||||
|
|
||||||
@events.computes_output_socket(
|
@events.computes_output_socket(
|
||||||
'Data',
|
'Data',
|
||||||
|
@ -226,6 +245,10 @@ class ExtractDataNode(base.MaxwellSimNode):
|
||||||
field_data = self._compute_input('Field Data')
|
field_data = self._compute_input('Field Data')
|
||||||
return getattr(field_data, props['field_data__component'])
|
return getattr(field_data, props['field_data__component'])
|
||||||
|
|
||||||
|
elif self.active_socket_set == 'Flux Data': # noqa: RET505
|
||||||
|
flux_data = self._compute_input('Flux Data')
|
||||||
|
return getattr(flux_data, 'flux')
|
||||||
|
|
||||||
msg = f'Tried to get data from unknown output socket in "{self.bl_label}"'
|
msg = f'Tried to get data from unknown output socket in "{self.bl_label}"'
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,12 +1,10 @@
|
||||||
import enum
|
import dataclasses
|
||||||
import inspect
|
import inspect
|
||||||
import typing as typ
|
import typing as typ
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
|
|
||||||
from ....utils import extra_sympy_units as spux
|
|
||||||
from ....utils import logger
|
from ....utils import logger
|
||||||
from .. import contracts as ct
|
from .. import contracts as ct
|
||||||
from .base import MaxwellSimNode
|
|
||||||
|
|
||||||
log = logger.get(__name__)
|
log = logger.get(__name__)
|
||||||
|
|
||||||
|
@ -14,50 +12,35 @@ UnitSystemID = str
|
||||||
UnitSystem = dict[ct.SocketType, typ.Any]
|
UnitSystem = dict[ct.SocketType, typ.Any]
|
||||||
|
|
||||||
|
|
||||||
class EventCallbackType(enum.StrEnum):
|
|
||||||
"""Names of actions that support callbacks."""
|
|
||||||
|
|
||||||
computes_output_socket = enum.auto()
|
|
||||||
on_value_changed = enum.auto()
|
|
||||||
on_show_plot = enum.auto()
|
|
||||||
on_init = enum.auto()
|
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Event Callback Information
|
# - Event Callback Information
|
||||||
####################
|
####################
|
||||||
class EventCallbackData_ComputesOutputSocket(typ.TypedDict): # noqa: N801
|
@dataclasses.dataclass(kw_only=True, frozen=True)
|
||||||
"""Extra data used to select a method to compute output sockets."""
|
class InfoDataChanged:
|
||||||
|
run_on_init: bool
|
||||||
|
on_changed_sockets: set[ct.SocketName]
|
||||||
|
on_changed_props: set[str]
|
||||||
|
on_any_changed_loose_input: set[str]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(kw_only=True, frozen=True)
|
||||||
|
class InfoOutputRequested:
|
||||||
output_socket_name: ct.SocketName
|
output_socket_name: ct.SocketName
|
||||||
any_loose_output_socket: bool
|
any_loose_output_socket: bool
|
||||||
kind: ct.DataFlowKind
|
kind: ct.DataFlowKind
|
||||||
|
|
||||||
|
depon_props: set[str]
|
||||||
|
|
||||||
class EventCallbackData_OnValueChanged(typ.TypedDict): # noqa: N801
|
depon_input_sockets: set[ct.SocketName]
|
||||||
"""Extra data used to select a method to compute output sockets."""
|
depon_input_socket_kinds: dict[ct.SocketName, ct.DataFlowKind]
|
||||||
|
depon_all_loose_input_sockets: bool
|
||||||
|
|
||||||
changed_sockets: set[ct.SocketName]
|
depon_output_sockets: set[ct.SocketName]
|
||||||
changed_props: set[str]
|
depon_output_socket_kinds: dict[ct.SocketName, ct.DataFlowKind]
|
||||||
changed_loose_input: set[str]
|
depon_all_loose_output_sockets: bool
|
||||||
|
|
||||||
|
|
||||||
class EventCallbackData_OnShowPlot(typ.TypedDict): # noqa: N801
|
EventCallbackInfo: typ.TypeAlias = InfoDataChanged | InfoOutputRequested
|
||||||
"""Extra data in the callback, used when showing a plot."""
|
|
||||||
|
|
||||||
stop_propagation: bool
|
|
||||||
|
|
||||||
|
|
||||||
class EventCallbackData_OnInit(typ.TypedDict): # noqa: D101, N801
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
EventCallbackData: typ.TypeAlias = (
|
|
||||||
EventCallbackData_ComputesOutputSocket
|
|
||||||
| EventCallbackData_OnValueChanged
|
|
||||||
| EventCallbackData_OnShowPlot
|
|
||||||
| EventCallbackData_OnInit
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
@ -68,16 +51,21 @@ PropName: typ.TypeAlias = str
|
||||||
|
|
||||||
|
|
||||||
def event_decorator(
|
def event_decorator(
|
||||||
action_type: EventCallbackType,
|
action_type: ct.DataFlowAction,
|
||||||
extra_data: EventCallbackData,
|
callback_info: EventCallbackInfo | None,
|
||||||
props: set[PropName] = frozenset(),
|
stop_propagation: bool = False,
|
||||||
|
# Request Data for Callback
|
||||||
managed_objs: set[ManagedObjName] = frozenset(),
|
managed_objs: set[ManagedObjName] = frozenset(),
|
||||||
|
props: set[PropName] = frozenset(),
|
||||||
input_sockets: set[ct.SocketName] = frozenset(),
|
input_sockets: set[ct.SocketName] = frozenset(),
|
||||||
|
input_sockets_optional: dict[ct.SocketName, bool] = MappingProxyType({}),
|
||||||
input_socket_kinds: dict[ct.SocketName, ct.DataFlowKind] = MappingProxyType({}),
|
input_socket_kinds: dict[ct.SocketName, ct.DataFlowKind] = MappingProxyType({}),
|
||||||
output_sockets: set[ct.SocketName] = frozenset(),
|
output_sockets: set[ct.SocketName] = frozenset(),
|
||||||
|
output_sockets_optional: dict[ct.SocketName, bool] = MappingProxyType({}),
|
||||||
output_socket_kinds: dict[ct.SocketName, ct.DataFlowKind] = MappingProxyType({}),
|
output_socket_kinds: dict[ct.SocketName, ct.DataFlowKind] = MappingProxyType({}),
|
||||||
all_loose_input_sockets: bool = False,
|
all_loose_input_sockets: bool = False,
|
||||||
all_loose_output_sockets: bool = False,
|
all_loose_output_sockets: bool = False,
|
||||||
|
# Request Unit System Scaling
|
||||||
unit_systems: dict[UnitSystemID, UnitSystem] = MappingProxyType({}),
|
unit_systems: dict[UnitSystemID, UnitSystem] = MappingProxyType({}),
|
||||||
scale_input_sockets: dict[ct.SocketName, UnitSystemID] = MappingProxyType({}),
|
scale_input_sockets: dict[ct.SocketName, UnitSystemID] = MappingProxyType({}),
|
||||||
scale_output_sockets: dict[ct.SocketName, UnitSystemID] = MappingProxyType({}),
|
scale_output_sockets: dict[ct.SocketName, UnitSystemID] = MappingProxyType({}),
|
||||||
|
@ -87,9 +75,11 @@ def event_decorator(
|
||||||
Parameters:
|
Parameters:
|
||||||
action_type: A name describing which event the decorator should respond to.
|
action_type: A name describing which event the decorator should respond to.
|
||||||
Set to `return_method.action_type`
|
Set to `return_method.action_type`
|
||||||
extra_data: A dictionary that provides the caller with additional per-`action_type` information.
|
callback_info: A dictionary that provides the caller with additional per-`action_type` information.
|
||||||
This might include parameters to help select the most appropriate method(s) to respond to an event with, or actions to take after running the callback.
|
This might include parameters to help select the most appropriate method(s) to respond to an event with, or actions to take after running the callback.
|
||||||
props: Set of `props` to compute, then pass to the decorated method.
|
props: Set of `props` to compute, then pass to the decorated method.
|
||||||
|
stop_propagation: Whether or stop propagating the event through the graph after encountering this method.
|
||||||
|
Other methods defined on the same node will still run.
|
||||||
managed_objs: Set of `managed_objs` to retrieve, then pass to the decorated method.
|
managed_objs: Set of `managed_objs` to retrieve, then pass to the decorated method.
|
||||||
input_sockets: Set of `input_sockets` to compute, then pass to the decorated method.
|
input_sockets: Set of `input_sockets` to compute, then pass to the decorated method.
|
||||||
input_socket_kinds: The `ct.DataFlowKind` to compute per-input-socket.
|
input_socket_kinds: The `ct.DataFlowKind` to compute per-input-socket.
|
||||||
|
@ -104,7 +94,7 @@ def event_decorator(
|
||||||
A decorator, which can be applied to a method of `MaxwellSimNode`.
|
A decorator, which can be applied to a method of `MaxwellSimNode`.
|
||||||
When a `MaxwellSimNode` subclass initializes, such a decorated method will be picked up on.
|
When a `MaxwellSimNode` subclass initializes, such a decorated method will be picked up on.
|
||||||
|
|
||||||
When the `action_type` action passes through the node, then `extra_data` is used to determine
|
When the `action_type` action passes through the node, then `callback_info` is used to determine
|
||||||
"""
|
"""
|
||||||
req_params = (
|
req_params = (
|
||||||
{'self'}
|
{'self'}
|
||||||
|
@ -119,6 +109,8 @@ def event_decorator(
|
||||||
|
|
||||||
# TODO: Check that all Unit System IDs referenced are also defined in 'unit_systems'.
|
# TODO: Check that all Unit System IDs referenced are also defined in 'unit_systems'.
|
||||||
## TODO: More ex. introspective checks and such, to make it really hard to write invalid methods.
|
## TODO: More ex. introspective checks and such, to make it really hard to write invalid methods.
|
||||||
|
# TODO: Check Function Annotation Validity
|
||||||
|
## - socket capabilities
|
||||||
|
|
||||||
def decorator(method: typ.Callable) -> typ.Callable:
|
def decorator(method: typ.Callable) -> typ.Callable:
|
||||||
# Check Function Signature Validity
|
# Check Function Signature Validity
|
||||||
|
@ -133,127 +125,126 @@ def event_decorator(
|
||||||
msg = f'Decorated method {method.__name__} has superfluous arguments {func_sig - req_params}'
|
msg = f'Decorated method {method.__name__} has superfluous arguments {func_sig - req_params}'
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
# TODO: Check Function Annotation Validity
|
def decorated(node):
|
||||||
## - socket capabilities
|
|
||||||
|
|
||||||
def decorated(node: MaxwellSimNode):
|
|
||||||
method_kw_args = {} ## Keyword Arguments for Decorated Method
|
method_kw_args = {} ## Keyword Arguments for Decorated Method
|
||||||
|
|
||||||
# Compute Requested Props
|
# Unit Systems
|
||||||
if props:
|
method_kw_args |= {'unit_systems': unit_systems} if unit_systems else {}
|
||||||
_props = {prop_name: getattr(node, prop_name) for prop_name in props}
|
|
||||||
method_kw_args |= {'props': _props}
|
|
||||||
|
|
||||||
# Retrieve Requested Managed Objects
|
# Properties
|
||||||
if managed_objs:
|
method_kw_args |= (
|
||||||
_managed_objs = {
|
{'props': {prop_name: getattr(node, prop_name) for prop_name in props}}
|
||||||
|
if props
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Managed Objects
|
||||||
|
method_kw_args |= (
|
||||||
|
{
|
||||||
|
'managed_objs': {
|
||||||
managed_obj_name: node.managed_objs[managed_obj_name]
|
managed_obj_name: node.managed_objs[managed_obj_name]
|
||||||
for managed_obj_name in managed_objs
|
for managed_obj_name in managed_objs
|
||||||
}
|
}
|
||||||
method_kw_args |= {'managed_objs': _managed_objs}
|
}
|
||||||
|
if managed_objs
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
|
||||||
# Requested Sockets
|
# Sockets
|
||||||
## Compute Requested Input Sockets
|
## Input Sockets
|
||||||
if input_sockets:
|
method_kw_args |= (
|
||||||
_input_sockets = {
|
{
|
||||||
|
'input_sockets': {
|
||||||
input_socket_name: node._compute_input(
|
input_socket_name: node._compute_input(
|
||||||
input_socket_name,
|
input_socket_name,
|
||||||
kind=input_socket_kinds.get(
|
kind=input_socket_kinds.get(
|
||||||
input_socket_name, ct.DataFlowKind.Value
|
input_socket_name, ct.DataFlowKind.Value
|
||||||
),
|
),
|
||||||
|
unit_system=(
|
||||||
|
unit_system := unit_systems.get(
|
||||||
|
scale_input_sockets.get(input_socket_name)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
optional=input_sockets_optional.get(
|
||||||
|
input_socket_name, False
|
||||||
|
),
|
||||||
)
|
)
|
||||||
for input_socket_name in input_sockets
|
for input_socket_name in input_sockets
|
||||||
}
|
}
|
||||||
|
}
|
||||||
# Scale Specified Input Sockets to Unit System
|
if input_sockets
|
||||||
## First, scale the input socket value to the given unit system
|
else {}
|
||||||
## Then, convert the symbol-less sympy scalar to a python type.
|
|
||||||
for input_socket_name, unit_system_id in scale_input_sockets.items():
|
|
||||||
unit_system = unit_systems[unit_system_id]
|
|
||||||
kind = input_socket_kinds.get(
|
|
||||||
input_socket_name, ct.DataFlowKind.Value
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if kind == ct.DataFlowKind.Value:
|
## Output Sockets
|
||||||
_input_sockets[input_socket_name] = spux.sympy_to_python(
|
method_kw_args |= (
|
||||||
spux.scale_to_unit(
|
{
|
||||||
_input_sockets[input_socket_name],
|
'output_sockets': {
|
||||||
unit_system[node.inputs[input_socket_name].socket_type],
|
output_socket_name: ct.DataFlowKind.scale_to_unit_system(
|
||||||
|
(
|
||||||
|
output_socket_kind := output_socket_kinds.get(
|
||||||
|
output_socket_name, ct.DataFlowKind.Value
|
||||||
)
|
)
|
||||||
|
),
|
||||||
|
node.compute_output(
|
||||||
|
output_socket_name,
|
||||||
|
kind=output_socket_kind,
|
||||||
|
optional=output_sockets_optional.get(
|
||||||
|
output_socket_name, False
|
||||||
|
),
|
||||||
|
),
|
||||||
|
node.outputs[output_socket_name].socket_type,
|
||||||
|
unit_systems.get(
|
||||||
|
scale_output_sockets.get(output_socket_name)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
elif kind == ct.DataFlowKind.LazyValueRange:
|
if scale_output_sockets.get(output_socket_name) is not None
|
||||||
_input_sockets[input_socket_name] = _input_sockets[
|
else node.compute_output(
|
||||||
input_socket_name
|
|
||||||
].rescale_to_unit(
|
|
||||||
unit_system[node.inputs[input_socket_name].socket_type]
|
|
||||||
)
|
|
||||||
|
|
||||||
method_kw_args |= {'input_sockets': _input_sockets}
|
|
||||||
|
|
||||||
## Compute Requested Output Sockets
|
|
||||||
if output_sockets:
|
|
||||||
_output_sockets = {
|
|
||||||
output_socket_name: node.compute_output(
|
|
||||||
output_socket_name,
|
output_socket_name,
|
||||||
kind=output_socket_kinds.get(
|
kind=output_socket_kinds.get(
|
||||||
output_socket_name, ct.DataFlowKind.Value
|
output_socket_name, ct.DataFlowKind.Value
|
||||||
),
|
),
|
||||||
|
optional=output_sockets_optional.get(
|
||||||
|
output_socket_name, False
|
||||||
|
),
|
||||||
)
|
)
|
||||||
for output_socket_name in output_sockets
|
for output_socket_name in output_sockets
|
||||||
}
|
}
|
||||||
|
}
|
||||||
# Scale Specified Output Sockets to Unit System
|
if output_sockets
|
||||||
## First, scale the output socket value to the given unit system
|
else {}
|
||||||
## Then, convert the symbol-less sympy scalar to a python type.
|
|
||||||
for output_socket_name, unit_system_id in scale_output_sockets.items():
|
|
||||||
unit_system = unit_systems[unit_system_id]
|
|
||||||
kind = input_socket_kinds.get(
|
|
||||||
input_socket_name, ct.DataFlowKind.Value
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if kind == ct.DataFlowKind.Value:
|
|
||||||
_output_sockets[output_socket_name] = spux.sympy_to_python(
|
|
||||||
spux.scale_to_unit(
|
|
||||||
_output_sockets[output_socket_name],
|
|
||||||
unit_system[
|
|
||||||
node.outputs[output_socket_name].socket_type
|
|
||||||
],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elif kind == ct.DataFlowKind.LazyValueRange:
|
|
||||||
_output_sockets[output_socket_name] = _output_sockets[
|
|
||||||
output_socket_name
|
|
||||||
].rescale_to_unit(
|
|
||||||
unit_system[node.outputs[output_socket_name].socket_type]
|
|
||||||
)
|
|
||||||
method_kw_args |= {'output_sockets': _output_sockets}
|
|
||||||
|
|
||||||
# Loose Sockets
|
# Loose Sockets
|
||||||
## Compute All Loose Input Sockets
|
## Compute All Loose Input Sockets
|
||||||
if all_loose_input_sockets:
|
method_kw_args |= (
|
||||||
_loose_input_sockets = {
|
{
|
||||||
|
'loose_input_sockets': {
|
||||||
input_socket_name: node._compute_input(
|
input_socket_name: node._compute_input(
|
||||||
input_socket_name,
|
input_socket_name,
|
||||||
kind=node.inputs[input_socket_name].active_kind,
|
kind=node.inputs[input_socket_name].active_kind,
|
||||||
)
|
)
|
||||||
for input_socket_name in node.loose_input_sockets
|
for input_socket_name in node.loose_input_sockets
|
||||||
}
|
}
|
||||||
method_kw_args |= {'loose_input_sockets': _loose_input_sockets}
|
}
|
||||||
|
if all_loose_input_sockets
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
|
||||||
## Compute All Loose Output Sockets
|
## Compute All Loose Output Sockets
|
||||||
if all_loose_output_sockets:
|
method_kw_args |= (
|
||||||
_loose_output_sockets = {
|
{
|
||||||
|
'loose_output_sockets': {
|
||||||
output_socket_name: node.compute_output(
|
output_socket_name: node.compute_output(
|
||||||
output_socket_name,
|
output_socket_name,
|
||||||
kind=node.outputs[output_socket_name].active_kind,
|
kind=node.outputs[output_socket_name].active_kind,
|
||||||
)
|
)
|
||||||
for output_socket_name in node.loose_output_sockets
|
for output_socket_name in node.loose_output_sockets
|
||||||
}
|
}
|
||||||
method_kw_args |= {'loose_output_sockets': _loose_output_sockets}
|
}
|
||||||
|
if all_loose_output_sockets
|
||||||
# Unit Systems
|
else {}
|
||||||
if unit_systems:
|
)
|
||||||
method_kw_args |= {'unit_systems': unit_systems}
|
|
||||||
|
|
||||||
# Call Method
|
# Call Method
|
||||||
return method(
|
return method(
|
||||||
|
@ -270,7 +261,8 @@ def event_decorator(
|
||||||
|
|
||||||
## Add Spice
|
## Add Spice
|
||||||
decorated.action_type = action_type
|
decorated.action_type = action_type
|
||||||
decorated.extra_data = extra_data
|
decorated.callback_info = callback_info
|
||||||
|
decorated.stop_propagation = stop_propagation
|
||||||
|
|
||||||
return decorated
|
return decorated
|
||||||
|
|
||||||
|
@ -280,19 +272,22 @@ def event_decorator(
|
||||||
####################
|
####################
|
||||||
# - Simplified Event Callbacks
|
# - Simplified Event Callbacks
|
||||||
####################
|
####################
|
||||||
def computes_output_socket(
|
def on_enable_lock(
|
||||||
output_socket_name: ct.SocketName | None,
|
|
||||||
any_loose_output_socket: bool = False,
|
|
||||||
kind: ct.DataFlowKind = ct.DataFlowKind.Value,
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
return event_decorator(
|
return event_decorator(
|
||||||
action_type='computes_output_socket',
|
action_type=ct.DataFlowAction.EnableLock,
|
||||||
extra_data={
|
callback_info=None,
|
||||||
'output_socket_name': output_socket_name,
|
**kwargs,
|
||||||
'any_loose_output_socket': any_loose_output_socket,
|
)
|
||||||
'kind': kind,
|
|
||||||
},
|
|
||||||
|
def on_disable_lock(
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
return event_decorator(
|
||||||
|
action_type=ct.DataFlowAction.DisableLock,
|
||||||
|
callback_info=None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -302,37 +297,67 @@ def on_value_changed(
|
||||||
socket_name: set[ct.SocketName] | ct.SocketName | None = None,
|
socket_name: set[ct.SocketName] | ct.SocketName | None = None,
|
||||||
prop_name: set[str] | str | None = None,
|
prop_name: set[str] | str | None = None,
|
||||||
any_loose_input_socket: bool = False,
|
any_loose_input_socket: bool = False,
|
||||||
|
run_on_init: bool = False,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
return event_decorator(
|
return event_decorator(
|
||||||
action_type=EventCallbackType.on_value_changed,
|
action_type=ct.DataFlowAction.DataChanged,
|
||||||
extra_data={
|
callback_info=InfoDataChanged(
|
||||||
'changed_sockets': (
|
run_on_init=run_on_init,
|
||||||
|
on_changed_sockets=(
|
||||||
socket_name if isinstance(socket_name, set) else {socket_name}
|
socket_name if isinstance(socket_name, set) else {socket_name}
|
||||||
),
|
),
|
||||||
'changed_props': (prop_name if isinstance(prop_name, set) else {prop_name}),
|
on_changed_props=(prop_name if isinstance(prop_name, set) else {prop_name}),
|
||||||
'changed_loose_input': any_loose_input_socket,
|
on_any_changed_loose_input=any_loose_input_socket,
|
||||||
},
|
),
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
## TODO: Change name to 'on_output_requested'
|
||||||
|
def computes_output_socket(
|
||||||
|
output_socket_name: ct.SocketName | None,
|
||||||
|
any_loose_output_socket: bool = False,
|
||||||
|
kind: ct.DataFlowKind = ct.DataFlowKind.Value,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
return event_decorator(
|
||||||
|
action_type=ct.DataFlowAction.OutputRequested,
|
||||||
|
callback_info=InfoOutputRequested(
|
||||||
|
output_socket_name=output_socket_name,
|
||||||
|
any_loose_output_socket=any_loose_output_socket,
|
||||||
|
kind=kind,
|
||||||
|
depon_props=kwargs.get('props', set()),
|
||||||
|
depon_input_sockets=kwargs.get('input_sockets', set()),
|
||||||
|
depon_input_socket_kinds=kwargs.get('input_socket_kinds', set()),
|
||||||
|
depon_output_sockets=kwargs.get('output_sockets', set()),
|
||||||
|
depon_output_socket_kinds=kwargs.get('output_socket_kinds', set()),
|
||||||
|
depon_all_loose_input_sockets=kwargs.get('all_loose_input_sockets', set()),
|
||||||
|
depon_all_loose_output_sockets=kwargs.get(
|
||||||
|
'all_loose_output_sockets', set()
|
||||||
|
),
|
||||||
|
),
|
||||||
|
**kwargs, ## stop_propagation has no effect.
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def on_show_preview(
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
return event_decorator(
|
||||||
|
action_type=ct.DataFlowAction.ShowPreview,
|
||||||
|
callback_info={},
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def on_show_plot(
|
def on_show_plot(
|
||||||
stop_propagation: bool = False,
|
stop_propagation: bool = True,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
return event_decorator(
|
return event_decorator(
|
||||||
action_type=EventCallbackType.on_show_plot,
|
action_type=ct.DataFlowAction.ShowPlot,
|
||||||
extra_data={
|
callback_info={},
|
||||||
'stop_propagation': stop_propagation,
|
stop_propagation=stop_propagation,
|
||||||
},
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def on_init(**kwargs):
|
|
||||||
return event_decorator(
|
|
||||||
action_type=EventCallbackType.on_init,
|
|
||||||
extra_data={},
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
|
@ -57,6 +57,7 @@ class Tidy3DFileImporterNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
# - Properties
|
# - Properties
|
||||||
####################
|
####################
|
||||||
|
## TODO: More automatic determination of which file type is in use :)
|
||||||
tidy3d_type: bpy.props.EnumProperty(
|
tidy3d_type: bpy.props.EnumProperty(
|
||||||
name='Tidy3D Type',
|
name='Tidy3D Type',
|
||||||
description='Type of Tidy3D object to load',
|
description='Type of Tidy3D object to load',
|
||||||
|
@ -228,7 +229,6 @@ class Tidy3DFileImporterNode(base.MaxwellSimNode):
|
||||||
disp_fitter = CACHE[self.bl_label]['fitter']
|
disp_fitter = CACHE[self.bl_label]['fitter']
|
||||||
|
|
||||||
# Plot
|
# Plot
|
||||||
log.debug(disp_fitter)
|
|
||||||
managed_objs['plot'].mpl_plot_to_image(
|
managed_objs['plot'].mpl_plot_to_image(
|
||||||
lambda ax: disp_fitter.plot(
|
lambda ax: disp_fitter.plot(
|
||||||
medium=model_medium,
|
medium=model_medium,
|
||||||
|
|
|
@ -5,11 +5,14 @@ import sympy as sp
|
||||||
import sympy.physics.units as spu
|
import sympy.physics.units as spu
|
||||||
|
|
||||||
from .....utils import extra_sympy_units as spux
|
from .....utils import extra_sympy_units as spux
|
||||||
|
from .....utils import logger
|
||||||
from .....utils import sci_constants as constants
|
from .....utils import sci_constants as constants
|
||||||
from ... import contracts as ct
|
from ... import contracts as ct
|
||||||
from ... import sockets
|
from ... import sockets
|
||||||
from .. import base, events
|
from .. import base, events
|
||||||
|
|
||||||
|
log = logger.get(__name__)
|
||||||
|
|
||||||
|
|
||||||
class WaveConstantNode(base.MaxwellSimNode):
|
class WaveConstantNode(base.MaxwellSimNode):
|
||||||
node_type = ct.NodeType.WaveConstant
|
node_type = ct.NodeType.WaveConstant
|
||||||
|
@ -22,7 +25,7 @@ class WaveConstantNode(base.MaxwellSimNode):
|
||||||
|
|
||||||
use_range: bpy.props.BoolProperty(
|
use_range: bpy.props.BoolProperty(
|
||||||
name='Range',
|
name='Range',
|
||||||
description='Whether to use the wavelength range',
|
description='Whether to use a wavelength/frequency range',
|
||||||
default=False,
|
default=False,
|
||||||
update=lambda self, context: self.sync_prop('use_range', context),
|
update=lambda self, context: self.sync_prop('use_range', context),
|
||||||
)
|
)
|
||||||
|
@ -36,69 +39,87 @@ class WaveConstantNode(base.MaxwellSimNode):
|
||||||
@events.computes_output_socket(
|
@events.computes_output_socket(
|
||||||
'WL',
|
'WL',
|
||||||
kind=ct.DataFlowKind.Value,
|
kind=ct.DataFlowKind.Value,
|
||||||
all_loose_input_sockets=True,
|
# Data
|
||||||
|
input_sockets={'WL', 'Freq'},
|
||||||
|
input_sockets_optional={'WL': True, 'Freq': True},
|
||||||
)
|
)
|
||||||
def compute_wl_value(self, loose_input_sockets: dict) -> sp.Expr:
|
def compute_wl_value(self, input_sockets: dict) -> sp.Expr:
|
||||||
if (wl := loose_input_sockets.get('WL')) is not None:
|
if input_sockets['WL'] is not None:
|
||||||
return wl
|
return input_sockets['WL']
|
||||||
|
|
||||||
freq = loose_input_sockets.get('Freq')
|
if input_sockets['WL'] is None and input_sockets['Freq'] is None:
|
||||||
return constants.vac_speed_of_light / freq
|
msg = 'Both WL and Freq are None.'
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
|
return constants.vac_speed_of_light / input_sockets['Freq']
|
||||||
|
|
||||||
@events.computes_output_socket(
|
@events.computes_output_socket(
|
||||||
'Freq',
|
'Freq',
|
||||||
kind=ct.DataFlowKind.Value,
|
kind=ct.DataFlowKind.Value,
|
||||||
all_loose_input_sockets=True,
|
# Data
|
||||||
|
input_sockets={'WL', 'Freq'},
|
||||||
|
input_sockets_optional={'WL': True, 'Freq': True},
|
||||||
)
|
)
|
||||||
def compute_freq_value(self, loose_input_sockets: dict) -> sp.Expr:
|
def compute_freq_value(self, input_sockets: dict) -> sp.Expr:
|
||||||
if (freq := loose_input_sockets.get('Freq')) is not None:
|
log.critical(input_sockets)
|
||||||
return freq
|
if input_sockets['Freq'] is not None:
|
||||||
|
return input_sockets['Freq']
|
||||||
|
|
||||||
wl = loose_input_sockets.get('WL')
|
if input_sockets['WL'] is None and input_sockets['Freq'] is None:
|
||||||
return constants.vac_speed_of_light / wl
|
msg = 'Both WL and Freq are None.'
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
|
return constants.vac_speed_of_light / input_sockets['WL']
|
||||||
|
|
||||||
@events.computes_output_socket(
|
@events.computes_output_socket(
|
||||||
'WL',
|
'WL',
|
||||||
kind=ct.DataFlowKind.LazyValueRange,
|
kind=ct.DataFlowKind.LazyValueRange,
|
||||||
all_loose_input_sockets=True,
|
# Data
|
||||||
|
input_sockets={'WL', 'Freq'},
|
||||||
|
input_sockets_optional={'WL': True, 'Freq': True},
|
||||||
)
|
)
|
||||||
def compute_wl_lazyvaluerange(self, loose_input_sockets: dict) -> sp.Expr:
|
def compute_wl_range(self, input_sockets: dict) -> sp.Expr:
|
||||||
if (wl := loose_input_sockets.get('WL')) is not None:
|
if input_sockets['WL'] is not None:
|
||||||
return wl
|
return input_sockets['WL']
|
||||||
|
|
||||||
freq = loose_input_sockets.get('Freq')
|
if input_sockets['WL'] is None and input_sockets['Freq'] is None:
|
||||||
|
msg = 'Both WL and Freq are None.'
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
if isinstance(freq, ct.LazyDataValueRange):
|
return input_sockets['Freq'].rescale_bounds(
|
||||||
return freq.rescale_bounds(
|
|
||||||
lambda bound: constants.vac_speed_of_light / bound, reverse=True
|
lambda bound: constants.vac_speed_of_light / bound, reverse=True
|
||||||
)
|
)
|
||||||
|
|
||||||
return constants.vac_speed_of_light / freq
|
|
||||||
|
|
||||||
@events.computes_output_socket(
|
@events.computes_output_socket(
|
||||||
'Freq',
|
'Freq',
|
||||||
kind=ct.DataFlowKind.LazyValueRange,
|
kind=ct.DataFlowKind.LazyValueRange,
|
||||||
all_loose_input_sockets=True,
|
# Data
|
||||||
|
input_sockets={'WL', 'Freq'},
|
||||||
|
input_socket_kinds={
|
||||||
|
'WL': ct.DataFlowKind.LazyValueRange,
|
||||||
|
'Freq': ct.DataFlowKind.LazyValueRange,
|
||||||
|
},
|
||||||
|
input_sockets_optional={'WL': True, 'Freq': True},
|
||||||
)
|
)
|
||||||
def compute_freq_lazyvaluerange(self, loose_input_sockets: dict) -> sp.Expr:
|
def compute_freq_range(self, input_sockets: dict) -> sp.Expr:
|
||||||
if (freq := loose_input_sockets.get('Freq')) is not None:
|
if input_sockets['Freq'] is not None:
|
||||||
return freq
|
return input_sockets['Freq']
|
||||||
|
|
||||||
wl = loose_input_sockets.get('WL')
|
if input_sockets['WL'] is None and input_sockets['Freq'] is None:
|
||||||
|
msg = 'Both WL and Freq are None.'
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
if isinstance(wl, ct.LazyDataValueRange):
|
return input_sockets['WL'].rescale_bounds(
|
||||||
return wl.rescale_bounds(
|
|
||||||
lambda bound: constants.vac_speed_of_light / bound, reverse=True
|
lambda bound: constants.vac_speed_of_light / bound, reverse=True
|
||||||
)
|
)
|
||||||
|
|
||||||
return constants.vac_speed_of_light / wl
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Event Methods
|
# - Event Methods
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
prop_name={'active_socket_set', 'use_range'},
|
prop_name={'active_socket_set', 'use_range'},
|
||||||
props={'active_socket_set', 'use_range'},
|
props={'active_socket_set', 'use_range'},
|
||||||
|
run_on_init=True,
|
||||||
)
|
)
|
||||||
def on_input_spec_change(self, props: dict):
|
def on_input_spec_change(self, props: dict):
|
||||||
if props['active_socket_set'] == 'Wavelength':
|
if props['active_socket_set'] == 'Wavelength':
|
||||||
|
@ -123,12 +144,6 @@ class WaveConstantNode(base.MaxwellSimNode):
|
||||||
'Freq': sockets.PhysicalFreqSocketDef(is_array=props['use_range']),
|
'Freq': sockets.PhysicalFreqSocketDef(is_array=props['use_range']),
|
||||||
}
|
}
|
||||||
|
|
||||||
@events.on_init(
|
|
||||||
props={'active_socket_set', 'use_range'},
|
|
||||||
)
|
|
||||||
def on_init(self, props: dict):
|
|
||||||
self.on_input_spec_change()
|
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Blender Registration
|
# - Blender Registration
|
||||||
|
|
|
@ -47,15 +47,16 @@ class Tidy3DWebImporterNode(base.MaxwellSimNode):
|
||||||
## TODO: REMOVE TEST
|
## TODO: REMOVE TEST
|
||||||
log.info('Loading SimulationData File')
|
log.info('Loading SimulationData File')
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
for module_name, module in sys.modules.copy().items():
|
for module_name, module in sys.modules.copy().items():
|
||||||
if module_name == '__mp_main__':
|
if module_name == '__mp_main__':
|
||||||
print('Problematic Module Entry', module_name)
|
print('Problematic Module Entry', module_name)
|
||||||
print(module)
|
print(module)
|
||||||
#print('MODULE REPR', module)
|
# print('MODULE REPR', module)
|
||||||
continue
|
continue
|
||||||
#return td.SimulationData.from_file(
|
# return td.SimulationData.from_file(
|
||||||
# fname='/home/sofus/src/blender_maxwell/dev/sim_demo.hdf5'
|
# fname='/home/sofus/src/blender_maxwell/dev/sim_demo.hdf5'
|
||||||
#)
|
# )
|
||||||
|
|
||||||
# Validate Task Availability
|
# Validate Task Availability
|
||||||
if (cloud_task := input_sockets['Cloud Task']) is None:
|
if (cloud_task := input_sockets['Cloud Task']) is None:
|
||||||
|
@ -77,7 +78,9 @@ class Tidy3DWebImporterNode(base.MaxwellSimNode):
|
||||||
cloud_task, _sim_data_cache_path(cloud_task.task_id)
|
cloud_task, _sim_data_cache_path(cloud_task.task_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
@events.on_value_changed(socket_name='Cloud Task', input_sockets={'Cloud Task'})
|
@events.on_value_changed(
|
||||||
|
socket_name='Cloud Task', run_on_init=True, input_sockets={'Cloud Task'}
|
||||||
|
)
|
||||||
def on_cloud_task_changed(self, input_sockets: dict):
|
def on_cloud_task_changed(self, input_sockets: dict):
|
||||||
if (
|
if (
|
||||||
(cloud_task := input_sockets['Cloud Task']) is not None
|
(cloud_task := input_sockets['Cloud Task']) is not None
|
||||||
|
@ -90,10 +93,6 @@ class Tidy3DWebImporterNode(base.MaxwellSimNode):
|
||||||
else:
|
else:
|
||||||
self.loose_output_sockets = {}
|
self.loose_output_sockets = {}
|
||||||
|
|
||||||
@events.on_init()
|
|
||||||
def on_init(self):
|
|
||||||
self.on_cloud_task_changed()
|
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Blender Registration
|
# - Blender Registration
|
||||||
|
|
|
@ -99,9 +99,6 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
|
||||||
name=props['sim_node_name'],
|
name=props['sim_node_name'],
|
||||||
interval_space=tuple(input_sockets['Samples/Space']),
|
interval_space=tuple(input_sockets['Samples/Space']),
|
||||||
freqs=input_sockets['Freqs'].realize().values,
|
freqs=input_sockets['Freqs'].realize().values,
|
||||||
#freqs=[
|
|
||||||
# float(spu.convert_to(freq, spu.hertz) / spu.hertz) for freq in freqs
|
|
||||||
#],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
|
|
@ -45,8 +45,9 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
output_sockets: typ.ClassVar = {
|
output_socket_sets: typ.ClassVar = {
|
||||||
'Monitor': sockets.MaxwellMonitorSocketDef(),
|
'Freq Domain': {'Freq Monitor': sockets.MaxwellMonitorSocketDef()},
|
||||||
|
'Time Domain': {'Time Monitor': sockets.MaxwellMonitorSocketDef()},
|
||||||
}
|
}
|
||||||
|
|
||||||
managed_obj_defs: typ.ClassVar = {
|
managed_obj_defs: typ.ClassVar = {
|
||||||
|
@ -62,36 +63,31 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
|
||||||
# - Event Methods: Computation
|
# - Event Methods: Computation
|
||||||
####################
|
####################
|
||||||
@events.computes_output_socket(
|
@events.computes_output_socket(
|
||||||
'Monitor',
|
'Freq Monitor',
|
||||||
props={'active_socket_set', 'sim_node_name'},
|
props={'sim_node_name'},
|
||||||
input_sockets={
|
input_sockets={
|
||||||
'Rec Start',
|
|
||||||
'Rec Stop',
|
|
||||||
'Center',
|
'Center',
|
||||||
'Size',
|
'Size',
|
||||||
'Samples/Space',
|
'Samples/Space',
|
||||||
'Samples/Time',
|
|
||||||
'Freqs',
|
'Freqs',
|
||||||
'Direction',
|
'Direction',
|
||||||
},
|
},
|
||||||
input_socket_kinds={
|
input_socket_kinds={
|
||||||
'Freqs': ct.LazyDataValueRange,
|
'Freqs': ct.DataFlowKind.LazyValueRange,
|
||||||
},
|
},
|
||||||
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
|
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
|
||||||
scale_input_sockets={
|
scale_input_sockets={
|
||||||
'Center': 'Tidy3DUnits',
|
'Center': 'Tidy3DUnits',
|
||||||
'Size': 'Tidy3DUnits',
|
'Size': 'Tidy3DUnits',
|
||||||
'Freqs': 'Tidy3DUnits',
|
'Freqs': 'Tidy3DUnits',
|
||||||
'Samples/Space': 'Tidy3DUnits',
|
|
||||||
'Rec Start': 'Tidy3DUnits',
|
|
||||||
'Rec Stop': 'Tidy3DUnits',
|
|
||||||
'Samples/Time': 'Tidy3DUnits',
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
def compute_monitor(self, input_sockets: dict, props: dict) -> td.FieldTimeMonitor:
|
def compute_freq_monitor(
|
||||||
direction = '+' if input_sockets['Direction'] else '-'
|
self,
|
||||||
|
input_sockets: dict,
|
||||||
if props['active_socket_set'] == 'Freq Domain':
|
props: dict,
|
||||||
|
unit_systems: dict,
|
||||||
|
) -> td.FieldMonitor:
|
||||||
log.info(
|
log.info(
|
||||||
'Computing FluxMonitor (name="%s") with center="%s", size="%s"',
|
'Computing FluxMonitor (name="%s") with center="%s", size="%s"',
|
||||||
props['sim_node_name'],
|
props['sim_node_name'],
|
||||||
|
@ -102,20 +98,9 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
|
||||||
center=input_sockets['Center'],
|
center=input_sockets['Center'],
|
||||||
size=input_sockets['Size'],
|
size=input_sockets['Size'],
|
||||||
name=props['sim_node_name'],
|
name=props['sim_node_name'],
|
||||||
interval_space=input_sockets['Samples/Space'],
|
interval_space=(1,1,1),
|
||||||
freqs=input_sockets['Freqs'].realize().values,
|
freqs=input_sockets['Freqs'].realize().values,
|
||||||
normal_dir=direction,
|
normal_dir='+' if input_sockets['Direction'] else '-',
|
||||||
)
|
|
||||||
|
|
||||||
return td.FluxTimeMonitor(
|
|
||||||
center=input_sockets['Center'],
|
|
||||||
size=input_sockets['Size'],
|
|
||||||
name=props['sim_node_name'],
|
|
||||||
start=input_sockets['Rec Start'],
|
|
||||||
stop=input_sockets['Rec Stop'],
|
|
||||||
interval=input_sockets['Samples/Time'],
|
|
||||||
interval_space=input_sockets['Samples/Space'],
|
|
||||||
normal_dir=direction,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
|
|
@ -70,12 +70,6 @@ class ViewerNode(base.MaxwellSimNode):
|
||||||
update=lambda self, context: self.sync_prop('auto_3d_preview', context),
|
update=lambda self, context: self.sync_prop('auto_3d_preview', context),
|
||||||
)
|
)
|
||||||
|
|
||||||
cache__data_socket_linked: bpy.props.BoolProperty(
|
|
||||||
name='Data Is Linked',
|
|
||||||
description='Whether the Data input was linked last time it was checked.',
|
|
||||||
default=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - UI
|
# - UI
|
||||||
####################
|
####################
|
||||||
|
@ -125,42 +119,27 @@ class ViewerNode(base.MaxwellSimNode):
|
||||||
# - Event Methods
|
# - Event Methods
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
|
socket_name='Data',
|
||||||
prop_name='auto_plot',
|
prop_name='auto_plot',
|
||||||
props={'auto_plot'},
|
props={'auto_plot'},
|
||||||
)
|
)
|
||||||
def on_changed_plot_preview(self, props):
|
def on_changed_plot_preview(self, props):
|
||||||
if self.inputs['Data'].is_linked and props['auto_plot']:
|
if self.inputs['Data'].is_linked and props['auto_plot']:
|
||||||
# log.debug('Enabling 2D Plot from "%s"', self.name)
|
|
||||||
self.trigger_action(ct.DataFlowAction.ShowPlot)
|
self.trigger_action(ct.DataFlowAction.ShowPlot)
|
||||||
|
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
|
socket_name='Data',
|
||||||
prop_name='auto_3d_preview',
|
prop_name='auto_3d_preview',
|
||||||
props={'auto_3d_preview'},
|
props={'auto_3d_preview'},
|
||||||
)
|
)
|
||||||
def on_changed_3d_preview(self, props):
|
def on_changed_3d_preview(self, props):
|
||||||
# Unpreview Everything
|
|
||||||
if props['auto_3d_preview']:
|
|
||||||
node_tree = self.id_data
|
node_tree = self.id_data
|
||||||
node_tree.unpreview_all()
|
|
||||||
|
|
||||||
# Trigger Preview Action
|
# Remove Non-Repreviewed Previews on Close
|
||||||
|
with node_tree.repreview_all():
|
||||||
if self.inputs['Data'].is_linked and props['auto_3d_preview']:
|
if self.inputs['Data'].is_linked and props['auto_3d_preview']:
|
||||||
# log.debug('Enabling 3D Previews from "%s"', self.name)
|
|
||||||
self.trigger_action(ct.DataFlowAction.ShowPreview)
|
self.trigger_action(ct.DataFlowAction.ShowPreview)
|
||||||
|
|
||||||
@events.on_value_changed(
|
|
||||||
socket_name='Data',
|
|
||||||
)
|
|
||||||
def on_changed_3d_data(self):
|
|
||||||
# Is Linked: Re-Preview
|
|
||||||
if self.inputs['Data'].is_linked:
|
|
||||||
self.on_changed_3d_preview()
|
|
||||||
self.on_changed_plot_preview()
|
|
||||||
|
|
||||||
# Just Linked / Just Unlinked: Preview/Unpreview All
|
|
||||||
if self.inputs['Data'].is_linked ^ self.cache__data_socket_linked:
|
|
||||||
self.cache__data_socket_linked = self.inputs['Data'].is_linked
|
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Blender Registration
|
# - Blender Registration
|
||||||
|
|
|
@ -63,6 +63,7 @@ class SimDomainNode(base.MaxwellSimNode):
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
socket_name={'Center', 'Size'},
|
socket_name={'Center', 'Size'},
|
||||||
prop_name='preview_active',
|
prop_name='preview_active',
|
||||||
|
run_on_init=True,
|
||||||
props={'preview_active'},
|
props={'preview_active'},
|
||||||
input_sockets={'Center', 'Size'},
|
input_sockets={'Center', 'Size'},
|
||||||
managed_objs={'mesh', 'modifier'},
|
managed_objs={'mesh', 'modifier'},
|
||||||
|
@ -94,10 +95,6 @@ class SimDomainNode(base.MaxwellSimNode):
|
||||||
if props['preview_active']:
|
if props['preview_active']:
|
||||||
managed_objs['mesh'].show_preview()
|
managed_objs['mesh'].show_preview()
|
||||||
|
|
||||||
@events.on_init()
|
|
||||||
def on_init(self):
|
|
||||||
self.on_input_changed()
|
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Blender Registration
|
# - Blender Registration
|
||||||
|
|
|
@ -71,14 +71,14 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
|
||||||
socket_name='GeoNodes',
|
socket_name='GeoNodes',
|
||||||
prop_name='preview_active',
|
prop_name='preview_active',
|
||||||
any_loose_input_socket=True,
|
any_loose_input_socket=True,
|
||||||
|
run_on_init=True,
|
||||||
|
# Pass Data
|
||||||
props={'preview_active'},
|
props={'preview_active'},
|
||||||
managed_objs={'mesh', 'modifier'},
|
managed_objs={'mesh', 'modifier'},
|
||||||
input_sockets={'Center', 'GeoNodes'},
|
input_sockets={'Center', 'GeoNodes'},
|
||||||
all_loose_input_sockets=True,
|
all_loose_input_sockets=True,
|
||||||
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
|
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
|
||||||
scale_input_sockets={
|
scale_input_sockets={'Center': 'BlenderUnits'},
|
||||||
'Center': 'BlenderUnits'
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
def on_input_changed(
|
def on_input_changed(
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -62,6 +62,7 @@ class BoxStructureNode(base.MaxwellSimNode):
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
socket_name={'Center', 'Size'},
|
socket_name={'Center', 'Size'},
|
||||||
prop_name='preview_active',
|
prop_name='preview_active',
|
||||||
|
run_on_init=True,
|
||||||
props={'preview_active'},
|
props={'preview_active'},
|
||||||
input_sockets={'Center', 'Size'},
|
input_sockets={'Center', 'Size'},
|
||||||
managed_objs={'mesh', 'modifier'},
|
managed_objs={'mesh', 'modifier'},
|
||||||
|
@ -93,10 +94,6 @@ class BoxStructureNode(base.MaxwellSimNode):
|
||||||
if props['preview_active']:
|
if props['preview_active']:
|
||||||
managed_objs['mesh'].show_preview()
|
managed_objs['mesh'].show_preview()
|
||||||
|
|
||||||
@events.on_init()
|
|
||||||
def on_init(self):
|
|
||||||
self.on_inputs_changed()
|
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Blender Registration
|
# - Blender Registration
|
||||||
|
|
|
@ -64,6 +64,7 @@ class SphereStructureNode(base.MaxwellSimNode):
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
socket_name={'Center', 'Radius'},
|
socket_name={'Center', 'Radius'},
|
||||||
prop_name='preview_active',
|
prop_name='preview_active',
|
||||||
|
run_on_init=True,
|
||||||
props={'preview_active'},
|
props={'preview_active'},
|
||||||
input_sockets={'Center', 'Radius'},
|
input_sockets={'Center', 'Radius'},
|
||||||
managed_objs={'mesh', 'modifier'},
|
managed_objs={'mesh', 'modifier'},
|
||||||
|
@ -96,10 +97,6 @@ class SphereStructureNode(base.MaxwellSimNode):
|
||||||
if props['preview_active']:
|
if props['preview_active']:
|
||||||
managed_objs['mesh'].show_preview()
|
managed_objs['mesh'].show_preview()
|
||||||
|
|
||||||
@events.on_init()
|
|
||||||
def on_init(self):
|
|
||||||
self.on_inputs_changed()
|
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Blender Registration
|
# - Blender Registration
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import typing as typ
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import sympy as sp
|
import sympy as sp
|
||||||
|
|
||||||
|
@ -5,18 +7,15 @@ from ... import contracts as ct
|
||||||
from ... import sockets
|
from ... import sockets
|
||||||
from .. import base, events
|
from .. import base, events
|
||||||
|
|
||||||
MAX_AMOUNT = 20
|
|
||||||
|
|
||||||
|
|
||||||
class CombineNode(base.MaxwellSimNode):
|
class CombineNode(base.MaxwellSimNode):
|
||||||
node_type = ct.NodeType.Combine
|
node_type = ct.NodeType.Combine
|
||||||
bl_label = 'Combine'
|
bl_label = 'Combine'
|
||||||
# bl_icon = ...
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Sockets
|
# - Sockets
|
||||||
####################
|
####################
|
||||||
input_socket_sets = {
|
input_socket_sets: typ.ClassVar = {
|
||||||
'Maxwell Sources': {},
|
'Maxwell Sources': {},
|
||||||
'Maxwell Structures': {},
|
'Maxwell Structures': {},
|
||||||
'Maxwell Monitors': {},
|
'Maxwell Monitors': {},
|
||||||
|
@ -69,7 +68,7 @@ class CombineNode(base.MaxwellSimNode):
|
||||||
description='Amount of Objects to Combine',
|
description='Amount of Objects to Combine',
|
||||||
default=1,
|
default=1,
|
||||||
min=1,
|
min=1,
|
||||||
max=MAX_AMOUNT,
|
# max=MAX_AMOUNT,
|
||||||
update=lambda self, context: self.sync_prop('amount', context),
|
update=lambda self, context: self.sync_prop('amount', context),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -118,6 +117,7 @@ class CombineNode(base.MaxwellSimNode):
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
prop_name='active_socket_set',
|
prop_name='active_socket_set',
|
||||||
props={'active_socket_set', 'amount'},
|
props={'active_socket_set', 'amount'},
|
||||||
|
run_on_init=True,
|
||||||
)
|
)
|
||||||
def on_value_changed__active_socket_set(self, props):
|
def on_value_changed__active_socket_set(self, props):
|
||||||
if props['active_socket_set'] == 'Maxwell Sources':
|
if props['active_socket_set'] == 'Maxwell Sources':
|
||||||
|
@ -144,10 +144,6 @@ class CombineNode(base.MaxwellSimNode):
|
||||||
def on_value_changed__amount(self):
|
def on_value_changed__amount(self):
|
||||||
self.on_value_changed__active_socket_set()
|
self.on_value_changed__active_socket_set()
|
||||||
|
|
||||||
@events.on_init()
|
|
||||||
def on_init(self):
|
|
||||||
self.on_value_changed__active_socket_set()
|
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Blender Registration
|
# - Blender Registration
|
||||||
|
|
|
@ -116,7 +116,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
|
||||||
`trigger_action` method will be called.
|
`trigger_action` method will be called.
|
||||||
"""
|
"""
|
||||||
# Forwards Chains
|
# Forwards Chains
|
||||||
if action in {'value_changed'}:
|
if action in {ct.DataFlowAction.DataChanged}:
|
||||||
## Input Socket
|
## Input Socket
|
||||||
if not self.is_output:
|
if not self.is_output:
|
||||||
self.node.trigger_action(action, socket_name=self.name)
|
self.node.trigger_action(action, socket_name=self.name)
|
||||||
|
@ -128,15 +128,17 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
|
||||||
|
|
||||||
# Backwards Chains
|
# Backwards Chains
|
||||||
elif action in {
|
elif action in {
|
||||||
'enable_lock',
|
ct.DataFlowAction.EnableLock,
|
||||||
'disable_lock',
|
ct.DataFlowAction.DisableLock,
|
||||||
'show_preview',
|
ct.DataFlowAction.OutputRequested,
|
||||||
'show_plot',
|
ct.DataFlowAction.DataChanged,
|
||||||
|
ct.DataFlowAction.ShowPreview,
|
||||||
|
ct.DataFlowAction.ShowPlot,
|
||||||
}:
|
}:
|
||||||
if action == 'enable_lock':
|
if action == ct.DataFlowAction.EnableLock:
|
||||||
self.locked = True
|
self.locked = True
|
||||||
|
|
||||||
if action == 'disable_lock':
|
if action == ct.DataFlowAction.DisableLock:
|
||||||
self.locked = False
|
self.locked = False
|
||||||
|
|
||||||
## Output Socket
|
## Output Socket
|
||||||
|
@ -208,6 +210,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
|
||||||
|
|
||||||
Returns a bool, whether or not the socket consents to the link change.
|
Returns a bool, whether or not the socket consents to the link change.
|
||||||
"""
|
"""
|
||||||
|
## TODO: Crash if deleting removing linked loose sockets.
|
||||||
if self.locked:
|
if self.locked:
|
||||||
return False
|
return False
|
||||||
if self.is_output:
|
if self.is_output:
|
||||||
|
|
|
@ -105,7 +105,7 @@ class TidyCloudFolders:
|
||||||
cloud_folder.folder_id: cloud_folder for cloud_folder in cloud_folders
|
cloud_folder.folder_id: cloud_folder for cloud_folder in cloud_folders
|
||||||
}
|
}
|
||||||
cls.cache_folders = folders
|
cls.cache_folders = folders
|
||||||
log.info("Retrieved Folders: %s", str(cls.cache_folders))
|
log.info('Retrieved Folders: %s', str(cls.cache_folders))
|
||||||
return folders
|
return folders
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -243,7 +243,11 @@ class TidyCloudTasks:
|
||||||
## Task by-Folder Cache
|
## Task by-Folder Cache
|
||||||
cls.cache_folder_tasks[cloud_folder.folder_id] = set(cloud_tasks)
|
cls.cache_folder_tasks[cloud_folder.folder_id] = set(cloud_tasks)
|
||||||
|
|
||||||
log.info('Retrieved Tasks (folder="%s"): %s)', cloud_folder.folder_id, str(set(cloud_tasks)))
|
log.info(
|
||||||
|
'Retrieved Tasks (folder="%s"): %s)',
|
||||||
|
cloud_folder.folder_id,
|
||||||
|
str(set(cloud_tasks)),
|
||||||
|
)
|
||||||
return cloud_tasks
|
return cloud_tasks
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
|
Loading…
Reference in New Issue