feat: big refactor to fight fundamental crashes
Some rather foundational things were fundamentally broken, especially related to the initialization procedures of fields / cached properties. - We completely revamped `bl_cache`, fixing many to-be-discovered bugs. - We completely streamlined `BLField` property logic into reusable `bl_cache.BLProp` and `bl_cache.BLPropType`. - We implemented `BLInstance` superclass to handle ex. deterministic persistance of dynamic enum items, and other nuanced common functionality that was being duplicated raw. - We implemented inter `cached_bl_property` / `BLField` dependency logic, including the ability to invalidate dynamic enums without @on_value_changed logic. This **greatly** simplifies a vast quantity of nodes that were traditionally very difficult to get working due to the sharp edges imposed by needing manual invalidation logic. - We gave `ExprSocket` a significant usability upgrade, including thorough parsing logic in the `SocketDef`. It's not that existing nodes are as such broken, but their existing bugs are now going to cause problems a lot faster. Which is a good thing. BREAKING CHANGE: Closes #13. Closes #16. Big work on #64. Work on #37.main
parent
929fb2dae9
commit
c9936b8942
|
@ -36,6 +36,7 @@ from .bl import (
|
||||||
PresetName,
|
PresetName,
|
||||||
SocketName,
|
SocketName,
|
||||||
)
|
)
|
||||||
|
from .bl_types import BLEnumStrEnum
|
||||||
from .operator_types import (
|
from .operator_types import (
|
||||||
OperatorType,
|
OperatorType,
|
||||||
)
|
)
|
||||||
|
@ -64,6 +65,9 @@ __all__ = [
|
||||||
'ManagedObjName',
|
'ManagedObjName',
|
||||||
'PresetName',
|
'PresetName',
|
||||||
'SocketName',
|
'SocketName',
|
||||||
|
'BLEnumStrEnum',
|
||||||
|
'BLInstance',
|
||||||
|
'InstanceID',
|
||||||
'OperatorType',
|
'OperatorType',
|
||||||
'PanelType',
|
'PanelType',
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
# blender_maxwell
|
||||||
|
# Copyright (C) 2024 blender_maxwell Project Contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import typing as typ
|
||||||
|
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Blender Enum (w/EnumProperty support)
|
||||||
|
####################
|
||||||
|
class BLEnumStrEnum(typ.Protocol):
|
||||||
|
@staticmethod
|
||||||
|
def to_name(value: typ.Self) -> str: ...
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def to_icon(value: typ.Self) -> str: ...
|
|
@ -44,7 +44,7 @@ def socket_def_from_bl_isocket(
|
||||||
## -> Accounts for any combo of shape/MathType/PhysicalType.
|
## -> Accounts for any combo of shape/MathType/PhysicalType.
|
||||||
if blsck_info.socket_type == ct.SocketType.Expr:
|
if blsck_info.socket_type == ct.SocketType.Expr:
|
||||||
return sockets.ExprSocketDef(
|
return sockets.ExprSocketDef(
|
||||||
shape=blsck_info.size.shape,
|
size=blsck_info.size,
|
||||||
mathtype=blsck_info.mathtype,
|
mathtype=blsck_info.mathtype,
|
||||||
physical_type=blsck_info.physical_type,
|
physical_type=blsck_info.physical_type,
|
||||||
default_unit=ct.UNITS_BLENDER[blsck_info.physical_type],
|
default_unit=ct.UNITS_BLENDER[blsck_info.physical_type],
|
||||||
|
|
|
@ -50,6 +50,7 @@ from .flow_kinds import (
|
||||||
LazyArrayRangeFlow,
|
LazyArrayRangeFlow,
|
||||||
LazyValueFuncFlow,
|
LazyValueFuncFlow,
|
||||||
ParamsFlow,
|
ParamsFlow,
|
||||||
|
ScalingMode,
|
||||||
ValueFlow,
|
ValueFlow,
|
||||||
)
|
)
|
||||||
from .flow_signals import FlowSignal
|
from .flow_signals import FlowSignal
|
||||||
|
@ -116,6 +117,7 @@ __all__ = [
|
||||||
'LazyArrayRangeFlow',
|
'LazyArrayRangeFlow',
|
||||||
'LazyValueFuncFlow',
|
'LazyValueFuncFlow',
|
||||||
'ParamsFlow',
|
'ParamsFlow',
|
||||||
|
'ScalingMode',
|
||||||
'ValueFlow',
|
'ValueFlow',
|
||||||
'FlowSignal',
|
'FlowSignal',
|
||||||
]
|
]
|
||||||
|
|
|
@ -18,7 +18,7 @@ from .array import ArrayFlow
|
||||||
from .capabilities import CapabilitiesFlow
|
from .capabilities import CapabilitiesFlow
|
||||||
from .flow_kinds import FlowKind
|
from .flow_kinds import FlowKind
|
||||||
from .info import InfoFlow
|
from .info import InfoFlow
|
||||||
from .lazy_array_range import LazyArrayRangeFlow
|
from .lazy_array_range import LazyArrayRangeFlow, ScalingMode
|
||||||
from .lazy_value_func import LazyValueFuncFlow
|
from .lazy_value_func import LazyValueFuncFlow
|
||||||
from .params import ParamsFlow
|
from .params import ParamsFlow
|
||||||
from .value import ValueFlow
|
from .value import ValueFlow
|
||||||
|
@ -29,6 +29,7 @@ __all__ = [
|
||||||
'FlowKind',
|
'FlowKind',
|
||||||
'InfoFlow',
|
'InfoFlow',
|
||||||
'LazyArrayRangeFlow',
|
'LazyArrayRangeFlow',
|
||||||
|
'ScalingMode',
|
||||||
'LazyValueFuncFlow',
|
'LazyValueFuncFlow',
|
||||||
'ParamsFlow',
|
'ParamsFlow',
|
||||||
'ValueFlow',
|
'ValueFlow',
|
||||||
|
|
|
@ -96,7 +96,8 @@ class ArrayFlow:
|
||||||
msg = f'Tried to correct unit of unitless LazyDataValueRange "{corrected_unit}"'
|
msg = f'Tried to correct unit of unitless LazyDataValueRange "{corrected_unit}"'
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
def rescale_to_unit(self, unit: spu.Quantity) -> typ.Self:
|
def rescale_to_unit(self, unit: spu.Quantity | None) -> typ.Self:
|
||||||
|
## TODO: Cache by unit would be a very nice speedup for Viz node.
|
||||||
if self.unit is not None:
|
if self.unit is not None:
|
||||||
return ArrayFlow(
|
return ArrayFlow(
|
||||||
values=float(spux.scaling_factor(self.unit, unit)) * self.values,
|
values=float(spux.scaling_factor(self.unit, unit)) * self.values,
|
||||||
|
@ -104,8 +105,8 @@ class ArrayFlow:
|
||||||
is_sorted=self.is_sorted,
|
is_sorted=self.is_sorted,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if unit is None:
|
||||||
|
return self
|
||||||
|
|
||||||
msg = f'Tried to rescale unitless LazyDataValueRange to unit {unit}'
|
msg = f'Tried to rescale unitless LazyDataValueRange to unit {unit}'
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
def rescale_to_unit_system(self, unit: spu.Quantity) -> typ.Self:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
|
@ -62,6 +62,9 @@ class FlowKind(enum.StrEnum):
|
||||||
Params = enum.auto()
|
Params = enum.auto()
|
||||||
Info = enum.auto()
|
Info = enum.auto()
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Class Methods
|
||||||
|
####################
|
||||||
@classmethod
|
@classmethod
|
||||||
def scale_to_unit_system(
|
def scale_to_unit_system(
|
||||||
cls,
|
cls,
|
||||||
|
@ -85,3 +88,43 @@ class FlowKind(enum.StrEnum):
|
||||||
|
|
||||||
msg = 'Tried to scale unknown kind'
|
msg = 'Tried to scale unknown kind'
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Computed
|
||||||
|
####################
|
||||||
|
@property
|
||||||
|
def flow_kind(self) -> str:
|
||||||
|
return {
|
||||||
|
FlowKind.Value: FlowKind.Value,
|
||||||
|
FlowKind.Array: FlowKind.Array,
|
||||||
|
FlowKind.LazyValueFunc: FlowKind.LazyValueFunc,
|
||||||
|
FlowKind.LazyArrayRange: FlowKind.LazyArrayRange,
|
||||||
|
}[self]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def socket_shape(self) -> str:
|
||||||
|
return {
|
||||||
|
FlowKind.Value: 'CIRCLE',
|
||||||
|
FlowKind.Array: 'SQUARE',
|
||||||
|
FlowKind.LazyArrayRange: 'SQUARE',
|
||||||
|
FlowKind.LazyValueFunc: 'DIAMOND',
|
||||||
|
}[self]
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Blender Enum
|
||||||
|
####################
|
||||||
|
@staticmethod
|
||||||
|
def to_name(v: typ.Self) -> str:
|
||||||
|
return {
|
||||||
|
FlowKind.Capabilities: 'Capabilities',
|
||||||
|
FlowKind.Value: 'Value',
|
||||||
|
FlowKind.Array: 'Array',
|
||||||
|
FlowKind.LazyArrayRange: 'Range',
|
||||||
|
FlowKind.LazyValueFunc: 'Lazy Value',
|
||||||
|
FlowKind.Params: 'Parameters',
|
||||||
|
FlowKind.Info: 'Information',
|
||||||
|
}[v]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def to_icon(_: typ.Self) -> str:
|
||||||
|
return ''
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
import enum
|
||||||
import functools
|
import functools
|
||||||
import typing as typ
|
import typing as typ
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
|
@ -33,6 +34,25 @@ from .lazy_value_func import LazyValueFuncFlow
|
||||||
log = logger.get(__name__)
|
log = logger.get(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ScalingMode(enum.StrEnum):
|
||||||
|
Lin = enum.auto()
|
||||||
|
Geom = enum.auto()
|
||||||
|
Log = enum.auto()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def to_name(v: typ.Self) -> str:
|
||||||
|
SM = ScalingMode
|
||||||
|
return {
|
||||||
|
SM.Lin: 'Linear',
|
||||||
|
SM.Geom: 'Geometric',
|
||||||
|
SM.Log: 'Logarithmic',
|
||||||
|
}[v]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def to_icon(_: typ.Self) -> str:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True, kw_only=True)
|
@dataclasses.dataclass(frozen=True, kw_only=True)
|
||||||
class LazyArrayRangeFlow:
|
class LazyArrayRangeFlow:
|
||||||
r"""Represents a linearly/logarithmically spaced array using symbolic boundary expressions, with support for units and lazy evaluation.
|
r"""Represents a linearly/logarithmically spaced array using symbolic boundary expressions, with support for units and lazy evaluation.
|
||||||
|
@ -84,7 +104,7 @@ class LazyArrayRangeFlow:
|
||||||
start: spux.ScalarUnitlessComplexExpr
|
start: spux.ScalarUnitlessComplexExpr
|
||||||
stop: spux.ScalarUnitlessComplexExpr
|
stop: spux.ScalarUnitlessComplexExpr
|
||||||
steps: int
|
steps: int
|
||||||
scaling: typ.Literal['lin', 'geom', 'log'] = 'lin'
|
scaling: ScalingMode = ScalingMode.Lin
|
||||||
|
|
||||||
unit: spux.Unit | None = None
|
unit: spux.Unit | None = None
|
||||||
|
|
||||||
|
@ -295,9 +315,9 @@ class LazyArrayRangeFlow:
|
||||||
A `jax` function that takes a valid `start`, `stop`, and `steps`, and returns a 1D `jax` array.
|
A `jax` function that takes a valid `start`, `stop`, and `steps`, and returns a 1D `jax` array.
|
||||||
"""
|
"""
|
||||||
jnp_nspace = {
|
jnp_nspace = {
|
||||||
'lin': jnp.linspace,
|
ScalingMode.Lin: jnp.linspace,
|
||||||
'geom': jnp.geomspace,
|
ScalingMode.Geom: jnp.geomspace,
|
||||||
'log': jnp.logspace,
|
ScalingMode.Log: jnp.logspace,
|
||||||
}.get(self.scaling)
|
}.get(self.scaling)
|
||||||
if jnp_nspace is None:
|
if jnp_nspace is None:
|
||||||
msg = f'ArrayFlow scaling method {self.scaling} is unsupported'
|
msg = f'ArrayFlow scaling method {self.scaling} is unsupported'
|
||||||
|
|
|
@ -430,17 +430,40 @@ class MaxwellSimTree(bpy.types.NodeTree):
|
||||||
####################
|
####################
|
||||||
# - Post-Load Handler
|
# - Post-Load Handler
|
||||||
####################
|
####################
|
||||||
def initialize_sim_tree_node_link_cache(_: bpy.types.Scene):
|
@bpy.app.handlers.persistent
|
||||||
|
def initialize_sim_tree_node_link_cache(_):
|
||||||
"""Whenever a file is loaded, create/regenerate the NodeLinkCache in all trees."""
|
"""Whenever a file is loaded, create/regenerate the NodeLinkCache in all trees."""
|
||||||
for node_tree in bpy.data.node_groups:
|
for node_tree in bpy.data.node_groups:
|
||||||
if node_tree.bl_idname == 'MaxwellSimTree':
|
if node_tree.bl_idname == 'MaxwellSimTree':
|
||||||
node_tree.on_load()
|
node_tree.on_load()
|
||||||
|
|
||||||
|
|
||||||
|
@bpy.app.handlers.persistent
|
||||||
|
def populate_missing_persistence(_) -> None:
|
||||||
|
"""For all nodes and sockets with elements that don't have persistent elements computed, compute them.
|
||||||
|
|
||||||
|
This is used when new dynamic enum properties are added to nodes and sockets, which need to first be computed and persisted in a context where setting properties is allowed.
|
||||||
|
"""
|
||||||
|
# Iterate over MaxwellSim Trees
|
||||||
|
for node_tree in [
|
||||||
|
_node_tree
|
||||||
|
for _node_tree in bpy.data.node_groups
|
||||||
|
if _node_tree.bl_idname == ct.TreeType.MaxwellSim.value and _node_tree.is_active
|
||||||
|
]:
|
||||||
|
# Iterate over MaxwellSim Nodes
|
||||||
|
# -> Excludes ex. frame and reroute nodes.
|
||||||
|
for node in [_node for _node in node_tree.nodes if hasattr(_node, 'node_type')]:
|
||||||
|
node.regenerate_dynamic_field_persistance()
|
||||||
|
for bl_sockets in [node.inputs, node.outputs]:
|
||||||
|
for bl_socket in bl_sockets:
|
||||||
|
bl_socket.regenerate_dynamic_field_persistance()
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Blender Registration
|
# - Blender Registration
|
||||||
####################
|
####################
|
||||||
bpy.app.handlers.load_post.append(initialize_sim_tree_node_link_cache)
|
bpy.app.handlers.load_post.append(initialize_sim_tree_node_link_cache)
|
||||||
|
bpy.app.handlers.load_post.append(populate_missing_persistence)
|
||||||
## TODO: Move to top-level registration.
|
## TODO: Move to top-level registration.
|
||||||
|
|
||||||
BL_REGISTER = [
|
BL_REGISTER = [
|
||||||
|
|
|
@ -67,8 +67,7 @@ class ExtractDataNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
# - Properties
|
# - Properties
|
||||||
####################
|
####################
|
||||||
extract_filter: enum.Enum = bl_cache.BLField(
|
extract_filter: enum.StrEnum = bl_cache.BLField(
|
||||||
prop_ui=True,
|
|
||||||
enum_cb=lambda self, _: self.search_extract_filters(),
|
enum_cb=lambda self, _: self.search_extract_filters(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -126,10 +126,10 @@ class FilterMathNode(base.MaxwellSimNode):
|
||||||
bl_label = 'Filter Math'
|
bl_label = 'Filter Math'
|
||||||
|
|
||||||
input_sockets: typ.ClassVar = {
|
input_sockets: typ.ClassVar = {
|
||||||
'Expr': sockets.ExprSocketDef(active_kind=ct.FlowKind.Array),
|
'Expr': sockets.ExprSocketDef(active_kind=ct.FlowKind.LazyValueFunc),
|
||||||
}
|
}
|
||||||
output_sockets: typ.ClassVar = {
|
output_sockets: typ.ClassVar = {
|
||||||
'Expr': sockets.ExprSocketDef(active_kind=ct.FlowKind.Array),
|
'Expr': sockets.ExprSocketDef(active_kind=ct.FlowKind.LazyValueFunc),
|
||||||
}
|
}
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
@ -141,12 +141,8 @@ class FilterMathNode(base.MaxwellSimNode):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Dimension Selection
|
# Dimension Selection
|
||||||
dim_0: enum.Enum = bl_cache.BLField(
|
dim_0: enum.StrEnum = bl_cache.BLField(enum_cb=lambda self, _: self.search_dims())
|
||||||
None, prop_ui=True, enum_cb=lambda self, _: self.search_dims()
|
dim_1: enum.StrEnum = bl_cache.BLField(enum_cb=lambda self, _: self.search_dims())
|
||||||
)
|
|
||||||
dim_1: enum.Enum = bl_cache.BLField(
|
|
||||||
None, prop_ui=True, enum_cb=lambda self, _: self.search_dims()
|
|
||||||
)
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Computed
|
# - Computed
|
||||||
|
@ -259,14 +255,14 @@ class FilterMathNode(base.MaxwellSimNode):
|
||||||
# Determine Whether to Declare New Loose Input SOcket
|
# Determine Whether to Declare New Loose Input SOcket
|
||||||
if (
|
if (
|
||||||
current_bl_socket is None
|
current_bl_socket is None
|
||||||
or current_bl_socket.shape is not None
|
or current_bl_socket.size is not spux.NumberSize1D.Scalar
|
||||||
or current_bl_socket.physical_type != pinned_physical_type
|
or current_bl_socket.physical_type != pinned_physical_type
|
||||||
or current_bl_socket.mathtype != wanted_mathtype
|
or current_bl_socket.mathtype != wanted_mathtype
|
||||||
):
|
):
|
||||||
self.loose_input_sockets = {
|
self.loose_input_sockets = {
|
||||||
'Value': sockets.ExprSocketDef(
|
'Value': sockets.ExprSocketDef(
|
||||||
active_kind=ct.FlowKind.Value,
|
active_kind=ct.FlowKind.Value,
|
||||||
shape=None,
|
size=spux.NumberSize1D.Scalar,
|
||||||
physical_type=pinned_physical_type,
|
physical_type=pinned_physical_type,
|
||||||
mathtype=wanted_mathtype,
|
mathtype=wanted_mathtype,
|
||||||
default_unit=pinned_unit,
|
default_unit=pinned_unit,
|
||||||
|
|
|
@ -364,10 +364,10 @@ class MapMathNode(base.MaxwellSimNode):
|
||||||
bl_label = 'Map Math'
|
bl_label = 'Map Math'
|
||||||
|
|
||||||
input_sockets: typ.ClassVar = {
|
input_sockets: typ.ClassVar = {
|
||||||
'Expr': sockets.ExprSocketDef(active_kind=ct.FlowKind.Array),
|
'Expr': sockets.ExprSocketDef(active_kind=ct.FlowKind.LazyValueFunc),
|
||||||
}
|
}
|
||||||
output_sockets: typ.ClassVar = {
|
output_sockets: typ.ClassVar = {
|
||||||
'Expr': sockets.ExprSocketDef(active_kind=ct.FlowKind.Array),
|
'Expr': sockets.ExprSocketDef(active_kind=ct.FlowKind.LazyValueFunc),
|
||||||
}
|
}
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
|
|
@ -71,22 +71,22 @@ class OperateMathNode(base.MaxwellSimNode):
|
||||||
bl_label = 'Operate Math'
|
bl_label = 'Operate Math'
|
||||||
|
|
||||||
input_sockets: typ.ClassVar = {
|
input_sockets: typ.ClassVar = {
|
||||||
'Expr L': sockets.ExprSocketDef(active_kind=ct.FlowKind.Array),
|
'Expr L': sockets.ExprSocketDef(active_kind=ct.FlowKind.LazyValueFunc),
|
||||||
'Expr R': sockets.ExprSocketDef(active_kind=ct.FlowKind.Array),
|
'Expr R': sockets.ExprSocketDef(active_kind=ct.FlowKind.LazyValueFunc),
|
||||||
}
|
}
|
||||||
output_sockets: typ.ClassVar = {
|
output_sockets: typ.ClassVar = {
|
||||||
'Expr': sockets.ExprSocketDef(active_kind=ct.FlowKind.Array),
|
'Expr': sockets.ExprSocketDef(active_kind=ct.FlowKind.LazyValueFunc),
|
||||||
}
|
}
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Properties
|
# - Properties
|
||||||
####################
|
####################
|
||||||
category: enum.Enum = bl_cache.BLField(
|
category: enum.StrEnum = bl_cache.BLField(
|
||||||
prop_ui=True, enum_cb=lambda self, _: self.search_categories()
|
enum_cb=lambda self, _: self.search_categories()
|
||||||
)
|
)
|
||||||
|
|
||||||
operation: enum.Enum = bl_cache.BLField(
|
operation: enum.StrEnum = bl_cache.BLField(
|
||||||
prop_ui=True, enum_cb=lambda self, _: self.search_operations()
|
enum_cb=lambda self, _: self.search_operations()
|
||||||
)
|
)
|
||||||
|
|
||||||
def search_categories(self) -> list[ct.BLEnumElement]:
|
def search_categories(self) -> list[ct.BLEnumElement]:
|
||||||
|
|
|
@ -62,8 +62,8 @@ class TransformMathNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
# - Properties
|
# - Properties
|
||||||
####################
|
####################
|
||||||
operation: enum.Enum = bl_cache.BLField(
|
operation: enum.StrEnum = bl_cache.BLField(
|
||||||
prop_ui=True, enum_cb=lambda self, _: self.search_operations()
|
enum_cb=lambda self, _: self.search_operations()
|
||||||
)
|
)
|
||||||
|
|
||||||
def search_operations(self) -> list[ct.BLEnumElement]:
|
def search_operations(self) -> list[ct.BLEnumElement]:
|
||||||
|
|
|
@ -209,7 +209,7 @@ class VizNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
input_sockets: typ.ClassVar = {
|
input_sockets: typ.ClassVar = {
|
||||||
'Expr': sockets.ExprSocketDef(
|
'Expr': sockets.ExprSocketDef(
|
||||||
active_kind=ct.FlowKind.Array,
|
active_kind=ct.FlowKind.LazyValueFunc,
|
||||||
symbols={_x := sp.Symbol('x', real=True)},
|
symbols={_x := sp.Symbol('x', real=True)},
|
||||||
default_value=2 * _x,
|
default_value=2 * _x,
|
||||||
),
|
),
|
||||||
|
@ -225,31 +225,33 @@ class VizNode(base.MaxwellSimNode):
|
||||||
#####################
|
#####################
|
||||||
## - Properties
|
## - Properties
|
||||||
#####################
|
#####################
|
||||||
viz_mode: enum.Enum = bl_cache.BLField(
|
@bl_cache.cached_bl_property()
|
||||||
prop_ui=True, enum_cb=lambda self, _: self.search_viz_modes()
|
def input_info(self) -> ct.InfoFlow | None:
|
||||||
)
|
|
||||||
viz_target: enum.Enum = bl_cache.BLField(
|
|
||||||
prop_ui=True, enum_cb=lambda self, _: self.search_targets()
|
|
||||||
)
|
|
||||||
|
|
||||||
# Mode-Dependent Properties
|
|
||||||
colormap: image_ops.Colormap = bl_cache.BLField(
|
|
||||||
image_ops.Colormap.Viridis, prop_ui=True
|
|
||||||
)
|
|
||||||
|
|
||||||
#####################
|
|
||||||
## - Mode Searcher
|
|
||||||
#####################
|
|
||||||
@property
|
|
||||||
def data_info(self) -> ct.InfoFlow | None:
|
|
||||||
info = self._compute_input('Expr', kind=ct.FlowKind.Info)
|
info = self._compute_input('Expr', kind=ct.FlowKind.Info)
|
||||||
if not ct.FlowSignal.check(info):
|
if not ct.FlowSignal.check(info):
|
||||||
return info
|
return info
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
viz_mode: enum.StrEnum = bl_cache.BLField(
|
||||||
|
enum_cb=lambda self, _: self.search_viz_modes(),
|
||||||
|
cb_depends_on={'input_info'},
|
||||||
|
)
|
||||||
|
viz_target: enum.StrEnum = bl_cache.BLField(
|
||||||
|
enum_cb=lambda self, _: self.search_targets(),
|
||||||
|
cb_depends_on={'viz_mode'},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mode-Dependent Properties
|
||||||
|
colormap: image_ops.Colormap = bl_cache.BLField(
|
||||||
|
image_ops.Colormap.Viridis,
|
||||||
|
)
|
||||||
|
|
||||||
|
#####################
|
||||||
|
## - Searchers
|
||||||
|
#####################
|
||||||
def search_viz_modes(self) -> list[ct.BLEnumElement]:
|
def search_viz_modes(self) -> list[ct.BLEnumElement]:
|
||||||
if self.data_info is not None:
|
if self.input_info is not None:
|
||||||
return [
|
return [
|
||||||
(
|
(
|
||||||
viz_mode,
|
viz_mode,
|
||||||
|
@ -258,14 +260,11 @@ class VizNode(base.MaxwellSimNode):
|
||||||
VizMode.to_icon(viz_mode),
|
VizMode.to_icon(viz_mode),
|
||||||
i,
|
i,
|
||||||
)
|
)
|
||||||
for i, viz_mode in enumerate(VizMode.valid_modes_for(self.data_info))
|
for i, viz_mode in enumerate(VizMode.valid_modes_for(self.input_info))
|
||||||
]
|
]
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
#####################
|
|
||||||
## - Target Searcher
|
|
||||||
#####################
|
|
||||||
def search_targets(self) -> list[ct.BLEnumElement]:
|
def search_targets(self) -> list[ct.BLEnumElement]:
|
||||||
if self.viz_mode is not None:
|
if self.viz_mode is not None:
|
||||||
return [
|
return [
|
||||||
|
@ -302,20 +301,14 @@ class VizNode(base.MaxwellSimNode):
|
||||||
input_sockets_optional={'Expr': True},
|
input_sockets_optional={'Expr': True},
|
||||||
)
|
)
|
||||||
def on_any_changed(self, input_sockets: dict):
|
def on_any_changed(self, input_sockets: dict):
|
||||||
|
self.input_info = bl_cache.Signal.InvalidateCache
|
||||||
|
|
||||||
info = input_sockets['Expr'][ct.FlowKind.Info]
|
info = input_sockets['Expr'][ct.FlowKind.Info]
|
||||||
params = input_sockets['Expr'][ct.FlowKind.Params]
|
params = input_sockets['Expr'][ct.FlowKind.Params]
|
||||||
|
|
||||||
has_info = not ct.FlowSignal.check(info)
|
has_info = not ct.FlowSignal.check(info)
|
||||||
has_params = not ct.FlowSignal.check(params)
|
has_params = not ct.FlowSignal.check(params)
|
||||||
|
|
||||||
# Reset Viz Mode/Target
|
|
||||||
has_nonpending_info = not ct.FlowSignal.check_single(
|
|
||||||
info, ct.FlowSignal.FlowPending
|
|
||||||
)
|
|
||||||
if has_nonpending_info:
|
|
||||||
self.viz_mode = bl_cache.Signal.ResetEnumItems
|
|
||||||
self.viz_target = bl_cache.Signal.ResetEnumItems
|
|
||||||
|
|
||||||
# Provide Sockets for Symbol Realization
|
# Provide Sockets for Symbol Realization
|
||||||
## -> This happens if Params contains not-yet-realized symbols.
|
## -> This happens if Params contains not-yet-realized symbols.
|
||||||
if has_info and has_params and params.symbols:
|
if has_info and has_params and params.symbols:
|
||||||
|
@ -325,7 +318,7 @@ class VizNode(base.MaxwellSimNode):
|
||||||
self.loose_input_sockets = {
|
self.loose_input_sockets = {
|
||||||
sym.name: sockets.ExprSocketDef(
|
sym.name: sockets.ExprSocketDef(
|
||||||
active_kind=ct.FlowKind.LazyArrayRange,
|
active_kind=ct.FlowKind.LazyArrayRange,
|
||||||
shape=None,
|
size=spux.NumberSize1D.Scalar,
|
||||||
mathtype=info.dim_mathtypes[sym.name],
|
mathtype=info.dim_mathtypes[sym.name],
|
||||||
physical_type=info.dim_physical_types[sym.name],
|
physical_type=info.dim_physical_types[sym.name],
|
||||||
default_min=(
|
default_min=(
|
||||||
|
@ -340,22 +333,13 @@ class VizNode(base.MaxwellSimNode):
|
||||||
),
|
),
|
||||||
default_steps=50,
|
default_steps=50,
|
||||||
)
|
)
|
||||||
for sym in sorted(
|
for sym in params.sorted_symbols
|
||||||
params.symbols, key=lambda el: info.dim_names.index(el.name)
|
|
||||||
)
|
|
||||||
if sym.name in info.dim_names
|
if sym.name in info.dim_names
|
||||||
}
|
}
|
||||||
|
|
||||||
elif self.loose_input_sockets:
|
elif self.loose_input_sockets:
|
||||||
self.loose_input_sockets = {}
|
self.loose_input_sockets = {}
|
||||||
|
|
||||||
@events.on_value_changed(
|
|
||||||
prop_name='viz_mode',
|
|
||||||
run_on_init=True,
|
|
||||||
)
|
|
||||||
def on_viz_mode_changed(self):
|
|
||||||
self.viz_target = bl_cache.Signal.ResetEnumItems
|
|
||||||
|
|
||||||
#####################
|
#####################
|
||||||
## - Plotting
|
## - Plotting
|
||||||
#####################
|
#####################
|
||||||
|
@ -374,12 +358,15 @@ class VizNode(base.MaxwellSimNode):
|
||||||
self, managed_objs, props, input_sockets, loose_input_sockets, unit_systems
|
self, managed_objs, props, input_sockets, loose_input_sockets, unit_systems
|
||||||
):
|
):
|
||||||
# Retrieve Inputs
|
# Retrieve Inputs
|
||||||
|
lazy_value_func = input_sockets['Expr'][ct.FlowKind.LazyValueFunc]
|
||||||
info = input_sockets['Expr'][ct.FlowKind.Info]
|
info = input_sockets['Expr'][ct.FlowKind.Info]
|
||||||
params = input_sockets['Expr'][ct.FlowKind.Params]
|
params = input_sockets['Expr'][ct.FlowKind.Params]
|
||||||
|
|
||||||
has_info = not ct.FlowSignal.check(info)
|
has_info = not ct.FlowSignal.check(info)
|
||||||
has_params = not ct.FlowSignal.check(params)
|
has_params = not ct.FlowSignal.check(params)
|
||||||
|
|
||||||
|
# Invalid Mode | Target
|
||||||
|
## -> To limit branching, return now if things aren't right.
|
||||||
if (
|
if (
|
||||||
not has_info
|
not has_info
|
||||||
or not has_params
|
or not has_params
|
||||||
|
@ -388,18 +375,21 @@ class VizNode(base.MaxwellSimNode):
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Compute Data
|
# Compute LazyArrayRanges for Symbols from Loose Sockets
|
||||||
lazy_value_func = input_sockets['Expr'][ct.FlowKind.LazyValueFunc]
|
## -> These are the concrete values of the symbol for plotting.
|
||||||
symbol_values = (
|
## -> In a quite nice turn of events, all this is cached lookups.
|
||||||
loose_input_sockets
|
## -> ...Unless something changed, in which case, well. It changed.
|
||||||
if not params.symbols
|
symbol_values = {
|
||||||
else {
|
sym: (
|
||||||
sym: loose_input_sockets[sym.name]
|
loose_input_sockets[sym.name]
|
||||||
.realize_array.rescale_to_unit(info.dim_units[sym.name])
|
.realize_array.rescale_to_unit(info.dim_units[sym.name])
|
||||||
.values
|
.values
|
||||||
|
)
|
||||||
for sym in params.sorted_symbols
|
for sym in params.sorted_symbols
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
# Realize LazyValueFunc w/Symbolic Values, Unit System
|
||||||
|
## -> This gives us the actual plot data!
|
||||||
data = lazy_value_func.func_jax(
|
data = lazy_value_func.func_jax(
|
||||||
*params.scaled_func_args(
|
*params.scaled_func_args(
|
||||||
unit_systems['BlenderUnits'], symbol_values=symbol_values
|
unit_systems['BlenderUnits'], symbol_values=symbol_values
|
||||||
|
@ -408,6 +398,9 @@ class VizNode(base.MaxwellSimNode):
|
||||||
unit_systems['BlenderUnits'], symbol_values=symbol_values
|
unit_systems['BlenderUnits'], symbol_values=symbol_values
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Replace InfoFlow Indices w/Realized Symbolic Ranges
|
||||||
|
## -> This ensures correct axis scaling.
|
||||||
if params.symbols:
|
if params.symbols:
|
||||||
info = info.rescale_dim_idxs(loose_input_sockets)
|
info = info.rescale_dim_idxs(loose_input_sockets)
|
||||||
|
|
||||||
|
@ -417,6 +410,7 @@ class VizNode(base.MaxwellSimNode):
|
||||||
lambda ax: VizMode.to_plotter(props['viz_mode'])(data, info, ax),
|
lambda ax: VizMode.to_plotter(props['viz_mode'])(data, info, ax),
|
||||||
bl_select=True,
|
bl_select=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if props['viz_target'] == VizTarget.Pixels:
|
if props['viz_target'] == VizTarget.Pixels:
|
||||||
managed_objs['plot'].map_2d_to_image(
|
managed_objs['plot'].map_2d_to_image(
|
||||||
data,
|
data,
|
||||||
|
|
|
@ -20,17 +20,15 @@ Attributes:
|
||||||
MANDATORY_PROPS: Properties that must be defined on the `MaxwellSimNode`.
|
MANDATORY_PROPS: Properties that must be defined on the `MaxwellSimNode`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
## TODO: Check whether input_socket_sets and output_socket_sets have the right shape? Or just use a type checker...
|
import enum
|
||||||
|
|
||||||
import typing as typ
|
import typing as typ
|
||||||
import uuid
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import sympy as sp
|
import sympy as sp
|
||||||
|
|
||||||
from blender_maxwell.utils import bl_cache, logger
|
from blender_maxwell.utils import bl_cache, bl_instance, logger
|
||||||
|
|
||||||
from .. import contracts as ct
|
from .. import contracts as ct
|
||||||
from .. import managed_objs as _managed_objs
|
from .. import managed_objs as _managed_objs
|
||||||
|
@ -40,10 +38,20 @@ from . import presets as _presets
|
||||||
|
|
||||||
log = logger.get(__name__)
|
log = logger.get(__name__)
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Types
|
||||||
|
####################
|
||||||
|
Sockets: typ.TypeAlias = dict[str, sockets.base.SocketDef]
|
||||||
|
Preset: typ.TypeAlias = dict[str, _presets.PresetDef]
|
||||||
|
ManagedObjs: typ.TypeAlias = dict[ct.ManagedObjName, type[_managed_objs.ManagedObj]]
|
||||||
|
|
||||||
MANDATORY_PROPS: set[str] = {'node_type', 'bl_label'}
|
MANDATORY_PROPS: set[str] = {'node_type', 'bl_label'}
|
||||||
|
|
||||||
|
|
||||||
class MaxwellSimNode(bpy.types.Node):
|
####################
|
||||||
|
# - Node
|
||||||
|
####################
|
||||||
|
class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
|
||||||
"""A specialized Blender node for Maxwell simulations.
|
"""A specialized Blender node for Maxwell simulations.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
@ -58,100 +66,111 @@ class MaxwellSimNode(bpy.types.Node):
|
||||||
locked: Whether the node is currently 'locked' aka. non-editable.
|
locked: Whether the node is currently 'locked' aka. non-editable.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Properties
|
||||||
|
####################
|
||||||
|
node_type: ct.NodeType
|
||||||
|
bl_label: str
|
||||||
|
|
||||||
|
# Features
|
||||||
use_sim_node_name: bool = False
|
use_sim_node_name: bool = False
|
||||||
## TODO: bl_description from first line of __doc__?
|
|
||||||
|
|
||||||
# Sockets
|
# Declarations
|
||||||
input_sockets: typ.ClassVar[dict[str, sockets.base.SocketDef]] = MappingProxyType(
|
input_sockets: typ.ClassVar[Sockets] = MappingProxyType({})
|
||||||
{}
|
output_sockets: typ.ClassVar[Sockets] = MappingProxyType({})
|
||||||
)
|
|
||||||
output_sockets: typ.ClassVar[dict[str, sockets.base.SocketDef]] = MappingProxyType(
|
input_socket_sets: typ.ClassVar[dict[str, Sockets]] = MappingProxyType({})
|
||||||
{}
|
output_socket_sets: typ.ClassVar[dict[str, Sockets]] = MappingProxyType({})
|
||||||
)
|
|
||||||
input_socket_sets: typ.ClassVar[dict[str, dict[str, sockets.base.SocketDef]]] = (
|
managed_obj_types: typ.ClassVar[ManagedObjs] = MappingProxyType({})
|
||||||
MappingProxyType({})
|
presets: typ.ClassVar[dict[str, Preset]] = MappingProxyType({})
|
||||||
)
|
|
||||||
output_socket_sets: typ.ClassVar[dict[str, dict[str, sockets.base.SocketDef]]] = (
|
## __init_subclass__ Computed
|
||||||
MappingProxyType({})
|
bl_idname: str
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Fields
|
||||||
|
####################
|
||||||
|
sim_node_name: str = bl_cache.BLField('')
|
||||||
|
|
||||||
|
# Loose Sockets
|
||||||
|
loose_input_sockets: dict[str, sockets.base.SocketDef] = bl_cache.BLField({})
|
||||||
|
loose_output_sockets: dict[str, sockets.base.SocketDef] = bl_cache.BLField({})
|
||||||
|
|
||||||
|
# UI Options
|
||||||
|
preview_active: bool = bl_cache.BLField(False)
|
||||||
|
locked: bool = bl_cache.BLField(False, use_prop_update=False)
|
||||||
|
|
||||||
|
# Active Socket Set
|
||||||
|
active_socket_set: enum.StrEnum = bl_cache.BLField(
|
||||||
|
enum_cb=lambda self, _: self.socket_sets_bl_enum()
|
||||||
)
|
)
|
||||||
|
|
||||||
# Presets
|
@classmethod
|
||||||
presets: typ.ClassVar[dict[str, dict[str, _presets.PresetDef]]] = MappingProxyType(
|
def socket_sets_bl_enum(cls) -> list[ct.BLEnumElement]:
|
||||||
{}
|
return [
|
||||||
|
(socket_set_name, socket_set_name, socket_set_name, '', i)
|
||||||
|
for i, socket_set_name in enumerate(cls.socket_set_names())
|
||||||
|
]
|
||||||
|
|
||||||
|
# Active Preset
|
||||||
|
active_preset: enum.StrEnum = bl_cache.BLField(
|
||||||
|
enum_cb=lambda self, _: self.presets_bl_enum()
|
||||||
)
|
)
|
||||||
|
|
||||||
# Managed Objects
|
@classmethod
|
||||||
managed_obj_types: typ.ClassVar[
|
def presets_bl_enum(cls) -> list[ct.BLEnumElement]:
|
||||||
dict[ct.ManagedObjName, type[_managed_objs.ManagedObj]]
|
return [
|
||||||
] = MappingProxyType({})
|
(
|
||||||
|
preset_name,
|
||||||
|
preset_def.label,
|
||||||
|
preset_def.description,
|
||||||
|
'',
|
||||||
|
i,
|
||||||
|
)
|
||||||
|
for i, (preset_name, preset_def) in enumerate(cls.presets.items())
|
||||||
|
]
|
||||||
|
|
||||||
def reset_instance_id(self) -> None:
|
####################
|
||||||
self.instance_id = str(uuid.uuid4())
|
# - Managed Objects
|
||||||
|
####################
|
||||||
|
@bl_cache.cached_bl_property(depends_on={'sim_node_name'})
|
||||||
|
def managed_objs(self) -> dict[str, _managed_objs.ManagedObj]:
|
||||||
|
"""Access the constructed managed objects defined in `self.managed_obj_types`.
|
||||||
|
|
||||||
# BLFields
|
Managed objects are special in that they **don't keep any non-reproducible state**.
|
||||||
blfields: typ.ClassVar[dict[str, str]] = MappingProxyType({})
|
In fact, all managed object state can generally be derived entirely from the managed object's `name` attribute.
|
||||||
ui_blfields: typ.ClassVar[set[str]] = frozenset()
|
As a result, **consistency in namespacing is of the utmost importance**, if reproducibility of managed objects is to be guaranteed.
|
||||||
|
|
||||||
|
This name must be in sync with the name of the managed "thing", which is where this computed property comes in.
|
||||||
|
The node's half of the responsibility is to push a new name whenever `self.sim_node_name` changes.
|
||||||
|
"""
|
||||||
|
if self.managed_obj_types:
|
||||||
|
return {
|
||||||
|
mobj_name: mobj_type(self.sim_node_name)
|
||||||
|
for mobj_name, mobj_type in self.managed_obj_types.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Class Methods
|
# - Class Methods
|
||||||
####################
|
####################
|
||||||
@classmethod
|
@classmethod
|
||||||
def _assert_attrs_valid(cls) -> None:
|
def socket_set_names(cls) -> list[str]:
|
||||||
"""Asserts that all mandatory attributes are defined on the class.
|
"""Retrieve the names of socket sets, in an order-preserving way.
|
||||||
|
|
||||||
The list of mandatory objects is sourced from `base.MANDATORY_PROPS`.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If a mandatory attribute defined in `base.MANDATORY_PROPS` is not defined on the class.
|
|
||||||
"""
|
|
||||||
for cls_attr in MANDATORY_PROPS:
|
|
||||||
if not hasattr(cls, cls_attr):
|
|
||||||
msg = f'Node class {cls} does not define mandatory attribute "{cls_attr}".'
|
|
||||||
raise ValueError(msg)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def declare_blfield(
|
|
||||||
cls, attr_name: str, bl_attr_name: str, prop_ui: bool = False
|
|
||||||
) -> None:
|
|
||||||
cls.blfields = cls.blfields | {attr_name: bl_attr_name}
|
|
||||||
|
|
||||||
if prop_ui:
|
|
||||||
cls.ui_blfields = cls.ui_blfields | {attr_name}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def set_prop(
|
|
||||||
cls,
|
|
||||||
prop_name: str,
|
|
||||||
prop: bpy.types.Property,
|
|
||||||
no_update: bool = False,
|
|
||||||
update_with_name: str | None = None,
|
|
||||||
**kwargs,
|
|
||||||
) -> None:
|
|
||||||
"""Adds a Blender property to a class via `__annotations__`, so it initializes with any subclass.
|
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Blender properties can't be set within `__init_subclass__` simply by adding attributes to the class; they must be added as type annotations.
|
Semantically similar to `list(set(...) | set(...))`.
|
||||||
- Must be called **within** `__init_subclass__`.
|
|
||||||
|
|
||||||
Parameters:
|
Returns:
|
||||||
name: The name of the property to set.
|
List of socket set names, without duplicates, in definition order.
|
||||||
prop: The `bpy.types.Property` to instantiate and attach..
|
|
||||||
no_update: Don't attach a `self.on_prop_changed()` callback to the property's `update`.
|
|
||||||
"""
|
"""
|
||||||
_update_with_name = prop_name if update_with_name is None else update_with_name
|
return (_input_socket_set_names := list(cls.input_socket_sets.keys())) + [
|
||||||
extra_kwargs = (
|
output_socket_set_name
|
||||||
{
|
for output_socket_set_name in cls.output_socket_sets
|
||||||
'update': lambda self, context: self.on_prop_changed(
|
if output_socket_set_name not in _input_socket_set_names
|
||||||
_update_with_name, context
|
]
|
||||||
),
|
|
||||||
}
|
|
||||||
if not no_update
|
|
||||||
else {}
|
|
||||||
)
|
|
||||||
cls.__annotations__[prop_name] = prop(
|
|
||||||
**kwargs,
|
|
||||||
**extra_kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _gather_event_methods(cls) -> dict[str, typ.Callable[[], None]]:
|
def _gather_event_methods(cls) -> dict[str, typ.Callable[[], None]]:
|
||||||
|
@ -169,6 +188,7 @@ class MaxwellSimNode(bpy.types.Node):
|
||||||
for attr_name in dir(cls)
|
for attr_name in dir(cls)
|
||||||
if hasattr(method := getattr(cls, attr_name), 'event')
|
if hasattr(method := getattr(cls, attr_name), 'event')
|
||||||
and method.event in set(ct.FlowEvent)
|
and method.event in set(ct.FlowEvent)
|
||||||
|
## Forbidding blfields prevents triggering __get__ on bl_property
|
||||||
]
|
]
|
||||||
event_methods_by_event = {event: [] for event in set(ct.FlowEvent)}
|
event_methods_by_event = {event: [] for event in set(ct.FlowEvent)}
|
||||||
for method in event_methods:
|
for method in event_methods:
|
||||||
|
@ -176,22 +196,6 @@ class MaxwellSimNode(bpy.types.Node):
|
||||||
|
|
||||||
return event_methods_by_event
|
return event_methods_by_event
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def socket_set_names(cls) -> list[str]:
|
|
||||||
"""Retrieve the names of socket sets, in an order-preserving way.
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
Semantically similar to `list(set(...) | set(...))`.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of socket set names, without duplicates, in definition order.
|
|
||||||
"""
|
|
||||||
return (_input_socket_set_names := list(cls.input_socket_sets.keys())) + [
|
|
||||||
output_socket_set_name
|
|
||||||
for output_socket_set_name in cls.output_socket_sets
|
|
||||||
if output_socket_set_name not in _input_socket_set_names
|
|
||||||
]
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Subclass Initialization
|
# - Subclass Initialization
|
||||||
####################
|
####################
|
||||||
|
@ -204,64 +208,20 @@ class MaxwellSimNode(bpy.types.Node):
|
||||||
"""
|
"""
|
||||||
log.debug('Initializing Node: %s', cls.node_type)
|
log.debug('Initializing Node: %s', cls.node_type)
|
||||||
super().__init_subclass__(**kwargs)
|
super().__init_subclass__(**kwargs)
|
||||||
cls._assert_attrs_valid()
|
|
||||||
|
# Check Attribute Validity
|
||||||
|
cls.assert_attrs_valid(MANDATORY_PROPS)
|
||||||
|
|
||||||
# Node Properties
|
# Node Properties
|
||||||
## Identifiers
|
|
||||||
cls.bl_idname: str = str(cls.node_type.value)
|
cls.bl_idname: str = str(cls.node_type.value)
|
||||||
cls.set_prop('instance_id', bpy.props.StringProperty, no_update=True)
|
|
||||||
cls.set_prop('sim_node_name', bpy.props.StringProperty, default='')
|
|
||||||
|
|
||||||
## Special States
|
|
||||||
cls.set_prop('preview_active', bpy.props.BoolProperty, default=False)
|
|
||||||
cls.set_prop('locked', bpy.props.BoolProperty, no_update=True, default=False)
|
|
||||||
|
|
||||||
## Event Method Callbacks
|
|
||||||
cls.event_methods_by_event = cls._gather_event_methods()
|
cls.event_methods_by_event = cls._gather_event_methods()
|
||||||
|
|
||||||
## Active Socket Set
|
|
||||||
if len(cls.input_socket_sets) + len(cls.output_socket_sets) > 0:
|
|
||||||
socket_set_names = cls.socket_set_names()
|
|
||||||
cls.set_prop(
|
|
||||||
'active_socket_set',
|
|
||||||
bpy.props.EnumProperty,
|
|
||||||
name='Active Socket Set',
|
|
||||||
items=[
|
|
||||||
(socket_set_name, socket_set_name, socket_set_name)
|
|
||||||
for socket_set_name in socket_set_names
|
|
||||||
],
|
|
||||||
default=socket_set_names[0],
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
cls.active_socket_set = None
|
|
||||||
|
|
||||||
## Active Preset
|
|
||||||
## TODO: Validate Presets
|
|
||||||
if cls.presets:
|
|
||||||
cls.set_prop(
|
|
||||||
'active_preset',
|
|
||||||
bpy.props.EnumProperty,
|
|
||||||
name='Active Preset',
|
|
||||||
description='The currently active preset',
|
|
||||||
items=[
|
|
||||||
(
|
|
||||||
preset_name,
|
|
||||||
preset_def.label,
|
|
||||||
preset_def.description,
|
|
||||||
)
|
|
||||||
for preset_name, preset_def in cls.presets.items()
|
|
||||||
],
|
|
||||||
default=next(cls.presets.keys()),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
cls.active_preset = None
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Events: Default
|
# - Events: Sim Node Name | Active Socket Set | Active Preset
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
prop_name='sim_node_name',
|
prop_name='sim_node_name',
|
||||||
props={'sim_node_name'},
|
props={'sim_node_name', 'managed_objs'},
|
||||||
stop_propagation=True,
|
stop_propagation=True,
|
||||||
)
|
)
|
||||||
def _on_sim_node_name_changed(self, props):
|
def _on_sim_node_name_changed(self, props):
|
||||||
|
@ -273,7 +233,7 @@ class MaxwellSimNode(bpy.types.Node):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Set Name of Managed Objects
|
# Set Name of Managed Objects
|
||||||
for mobj in self.managed_objs.values():
|
for mobj in props['managed_objs'].values():
|
||||||
mobj.name = props['sim_node_name']
|
mobj.name = props['sim_node_name']
|
||||||
|
|
||||||
## Invalidate Cache
|
## Invalidate Cache
|
||||||
|
@ -290,7 +250,10 @@ class MaxwellSimNode(bpy.types.Node):
|
||||||
self._sync_sockets()
|
self._sync_sockets()
|
||||||
|
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
prop_name='active_preset', props=['presets', 'active_preset']
|
prop_name='active_preset',
|
||||||
|
run_on_init=True,
|
||||||
|
props={'presets', 'active_preset'},
|
||||||
|
stop_propagation=True,
|
||||||
)
|
)
|
||||||
def _on_active_preset_changed(self, props: dict):
|
def _on_active_preset_changed(self, props: dict):
|
||||||
if props['active_preset'] is not None:
|
if props['active_preset'] is not None:
|
||||||
|
@ -313,6 +276,9 @@ class MaxwellSimNode(bpy.types.Node):
|
||||||
## TODO: Account for FlowKind
|
## TODO: Account for FlowKind
|
||||||
bl_socket.value = socket_value
|
bl_socket.value = socket_value
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Events: Preview | Plot
|
||||||
|
####################
|
||||||
@events.on_show_plot(stop_propagation=False)
|
@events.on_show_plot(stop_propagation=False)
|
||||||
def _on_show_plot(self):
|
def _on_show_plot(self):
|
||||||
node_tree = self.id_data
|
node_tree = self.id_data
|
||||||
|
@ -339,6 +305,9 @@ class MaxwellSimNode(bpy.types.Node):
|
||||||
for mobj in self.managed_objs.values():
|
for mobj in self.managed_objs.values():
|
||||||
mobj.hide_preview()
|
mobj.hide_preview()
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Events: Lock
|
||||||
|
####################
|
||||||
@events.on_enable_lock()
|
@events.on_enable_lock()
|
||||||
def _on_enabled_lock(self):
|
def _on_enabled_lock(self):
|
||||||
# Set Locked to Active
|
# Set Locked to Active
|
||||||
|
@ -354,11 +323,8 @@ class MaxwellSimNode(bpy.types.Node):
|
||||||
self.locked = False
|
self.locked = False
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Loose Sockets w/Events
|
# - Events: Loose Sockets
|
||||||
####################
|
####################
|
||||||
loose_input_sockets: dict[str, sockets.base.SocketDef] = bl_cache.BLField({})
|
|
||||||
loose_output_sockets: dict[str, sockets.base.SocketDef] = bl_cache.BLField({})
|
|
||||||
|
|
||||||
@events.on_value_changed(prop_name={'loose_input_sockets', 'loose_output_sockets'})
|
@events.on_value_changed(prop_name={'loose_input_sockets', 'loose_output_sockets'})
|
||||||
def _on_loose_sockets_changed(self):
|
def _on_loose_sockets_changed(self):
|
||||||
self._sync_sockets()
|
self._sync_sockets()
|
||||||
|
@ -516,23 +482,6 @@ class MaxwellSimNode(bpy.types.Node):
|
||||||
self._prune_inactive_sockets()
|
self._prune_inactive_sockets()
|
||||||
self._add_new_active_sockets()
|
self._add_new_active_sockets()
|
||||||
|
|
||||||
####################
|
|
||||||
# - Managed Objects
|
|
||||||
####################
|
|
||||||
@bl_cache.cached_bl_property(persist=True)
|
|
||||||
def managed_objs(self) -> dict[str, _managed_objs.ManagedObj]:
|
|
||||||
"""Access the managed objects defined on this node.
|
|
||||||
|
|
||||||
Persistent cache ensures that the managed objects are only created on first access, even across file reloads.
|
|
||||||
"""
|
|
||||||
if self.managed_obj_types:
|
|
||||||
return {
|
|
||||||
mobj_name: mobj_type(self.sim_node_name)
|
|
||||||
for mobj_name, mobj_type in self.managed_obj_types.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {}
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Event Methods
|
# - Event Methods
|
||||||
####################
|
####################
|
||||||
|
@ -733,7 +682,7 @@ class MaxwellSimNode(bpy.types.Node):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@bl_cache.cached_bl_property(persist=False)
|
@bl_cache.cached_bl_property()
|
||||||
def _dependent_outputs(
|
def _dependent_outputs(
|
||||||
self,
|
self,
|
||||||
) -> dict[
|
) -> dict[
|
||||||
|
@ -904,7 +853,7 @@ class MaxwellSimNode(bpy.types.Node):
|
||||||
####################
|
####################
|
||||||
# - Property Event: On Update
|
# - Property Event: On Update
|
||||||
####################
|
####################
|
||||||
def on_prop_changed(self, prop_name: str, _: bpy.types.Context) -> None:
|
def on_prop_changed(self, prop_name: str) -> None:
|
||||||
"""Report that a particular property has changed, which may cause certain caches to regenerate.
|
"""Report that a particular property has changed, which may cause certain caches to regenerate.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
@ -916,10 +865,6 @@ class MaxwellSimNode(bpy.types.Node):
|
||||||
prop_name: The name of the property that changed.
|
prop_name: The name of the property that changed.
|
||||||
"""
|
"""
|
||||||
if hasattr(self, prop_name):
|
if hasattr(self, prop_name):
|
||||||
# Invalidate UI BLField Caches
|
|
||||||
if prop_name in self.ui_blfields:
|
|
||||||
setattr(self, prop_name, bl_cache.Signal.InvalidateCache)
|
|
||||||
|
|
||||||
# Trigger Event
|
# Trigger Event
|
||||||
self.trigger_event(ct.FlowEvent.DataChanged, prop_name=prop_name)
|
self.trigger_event(ct.FlowEvent.DataChanged, prop_name=prop_name)
|
||||||
else:
|
else:
|
||||||
|
@ -952,16 +897,16 @@ class MaxwellSimNode(bpy.types.Node):
|
||||||
layout.enabled = False
|
layout.enabled = False
|
||||||
|
|
||||||
if self.active_socket_set:
|
if self.active_socket_set:
|
||||||
layout.prop(self, 'active_socket_set', text='')
|
layout.prop(self, self.blfields['active_socket_set'], text='')
|
||||||
|
|
||||||
if self.active_preset is not None:
|
if self.active_preset is not None:
|
||||||
layout.prop(self, 'active_preset', text='')
|
layout.prop(self, self.blfields['active_preset'], text='')
|
||||||
|
|
||||||
# Draw Name
|
# Draw Name
|
||||||
if self.use_sim_node_name:
|
if self.use_sim_node_name:
|
||||||
row = layout.row(align=True)
|
row = layout.row(align=True)
|
||||||
row.label(text='', icon='FILE_TEXT')
|
row.label(text='', icon='FILE_TEXT')
|
||||||
row.prop(self, 'sim_node_name', text='')
|
row.prop(self, self.blfields['sim_node_name'], text='')
|
||||||
|
|
||||||
# Draw Name
|
# Draw Name
|
||||||
self.draw_props(context, layout)
|
self.draw_props(context, layout)
|
||||||
|
@ -1029,24 +974,20 @@ class MaxwellSimNode(bpy.types.Node):
|
||||||
Notes:
|
Notes:
|
||||||
Run by Blender when a new instance of a node is added to a tree.
|
Run by Blender when a new instance of a node is added to a tree.
|
||||||
"""
|
"""
|
||||||
|
# Initialize Sockets
|
||||||
|
## -> Ensures the availability of static sockets before items/methods.
|
||||||
|
## -> Ensures the availability of static sockets before items/methods.
|
||||||
|
self._sync_sockets()
|
||||||
|
|
||||||
# Initialize Instance ID
|
# Initialize Instance ID
|
||||||
## This is used by various caches from 'bl_cache'.
|
## -> This is used by various caches from 'bl_cache'.
|
||||||
|
## -> Also generates (first-time) the various enums.
|
||||||
self.reset_instance_id()
|
self.reset_instance_id()
|
||||||
|
|
||||||
# Initialize Name
|
# Initialize Name
|
||||||
## This is used whenever a unique name pointing to this node is needed.
|
## -> Ensures the availability of sim_node_name immediately.
|
||||||
## Contrary to self.name, it can be altered by the user as a property.
|
|
||||||
self.sim_node_name = self.name
|
self.sim_node_name = self.name
|
||||||
|
|
||||||
# Initialize Sockets
|
|
||||||
## This initializes any nodes that need initializing
|
|
||||||
self._sync_sockets()
|
|
||||||
|
|
||||||
# Apply Preset
|
|
||||||
## This applies the default preset, if any.
|
|
||||||
if self.active_preset:
|
|
||||||
self._on_active_preset_changed()
|
|
||||||
|
|
||||||
# Event Methods
|
# Event Methods
|
||||||
## Run any 'DataChanged' methods with 'run_on_init' set.
|
## Run any 'DataChanged' methods with 'run_on_init' set.
|
||||||
## Semantically: Creating data _arguably_ changes it.
|
## Semantically: Creating data _arguably_ changes it.
|
||||||
|
|
|
@ -61,7 +61,7 @@ class AdiabAbsorbBoundCondNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
input_sockets: typ.ClassVar = {
|
input_sockets: typ.ClassVar = {
|
||||||
'Layers': sockets.ExprSocketDef(
|
'Layers': sockets.ExprSocketDef(
|
||||||
shape=None,
|
size=spux.NumberSize1D.Scalar,
|
||||||
mathtype=spux.MathType.Integer,
|
mathtype=spux.MathType.Integer,
|
||||||
abs_min=1,
|
abs_min=1,
|
||||||
default_value=40,
|
default_value=40,
|
||||||
|
@ -71,14 +71,13 @@ class AdiabAbsorbBoundCondNode(base.MaxwellSimNode):
|
||||||
'Simple': {},
|
'Simple': {},
|
||||||
'Full': {
|
'Full': {
|
||||||
'σ Order': sockets.ExprSocketDef(
|
'σ Order': sockets.ExprSocketDef(
|
||||||
shape=None,
|
size=spux.NumberSize1D.Scalar,
|
||||||
mathtype=spux.MathType.Integer,
|
mathtype=spux.MathType.Integer,
|
||||||
abs_min=1,
|
abs_min=1,
|
||||||
default_value=3,
|
default_value=3,
|
||||||
),
|
),
|
||||||
'σ Range': sockets.ExprSocketDef(
|
'σ Range': sockets.ExprSocketDef(
|
||||||
shape=(2,),
|
size=spux.NumberSize1D.Vec2,
|
||||||
mathtype=spux.MathType.Real,
|
|
||||||
default_value=sp.Matrix([0, 1.5]),
|
default_value=sp.Matrix([0, 1.5]),
|
||||||
abs_min=0,
|
abs_min=0,
|
||||||
),
|
),
|
||||||
|
|
|
@ -64,7 +64,7 @@ class PMLBoundCondNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
input_sockets: typ.ClassVar = {
|
input_sockets: typ.ClassVar = {
|
||||||
'Layers': sockets.ExprSocketDef(
|
'Layers': sockets.ExprSocketDef(
|
||||||
shape=None,
|
size=spux.NumberSize1D.Scalar,
|
||||||
mathtype=spux.MathType.Integer,
|
mathtype=spux.MathType.Integer,
|
||||||
abs_min=1,
|
abs_min=1,
|
||||||
default_value=12,
|
default_value=12,
|
||||||
|
@ -74,37 +74,37 @@ class PMLBoundCondNode(base.MaxwellSimNode):
|
||||||
'Simple': {},
|
'Simple': {},
|
||||||
'Full': {
|
'Full': {
|
||||||
'σ Order': sockets.ExprSocketDef(
|
'σ Order': sockets.ExprSocketDef(
|
||||||
shape=None,
|
size=spux.NumberSize1D.Scalar,
|
||||||
mathtype=spux.MathType.Integer,
|
mathtype=spux.MathType.Integer,
|
||||||
abs_min=1,
|
abs_min=1,
|
||||||
default_value=3,
|
default_value=3,
|
||||||
),
|
),
|
||||||
'σ Range': sockets.ExprSocketDef(
|
'σ Range': sockets.ExprSocketDef(
|
||||||
shape=(2,),
|
size=spux.NumberSize1D.Vec2,
|
||||||
mathtype=spux.MathType.Real,
|
mathtype=spux.MathType.Real,
|
||||||
default_value=sp.Matrix([0, 1.5]),
|
default_value=sp.Matrix([0, 1.5]),
|
||||||
abs_min=0,
|
abs_min=0,
|
||||||
),
|
),
|
||||||
'κ Order': sockets.ExprSocketDef(
|
'κ Order': sockets.ExprSocketDef(
|
||||||
shape=None,
|
size=spux.NumberSize1D.Scalar,
|
||||||
mathtype=spux.MathType.Integer,
|
mathtype=spux.MathType.Integer,
|
||||||
abs_min=1,
|
abs_min=1,
|
||||||
default_value=3,
|
default_value=3,
|
||||||
),
|
),
|
||||||
'κ Range': sockets.ExprSocketDef(
|
'κ Range': sockets.ExprSocketDef(
|
||||||
shape=(2,),
|
size=spux.NumberSize1D.Vec2,
|
||||||
mathtype=spux.MathType.Real,
|
mathtype=spux.MathType.Real,
|
||||||
default_value=sp.Matrix([0, 1.5]),
|
default_value=sp.Matrix([0, 1.5]),
|
||||||
abs_min=0,
|
abs_min=0,
|
||||||
),
|
),
|
||||||
'α Order': sockets.ExprSocketDef(
|
'α Order': sockets.ExprSocketDef(
|
||||||
shape=None,
|
size=spux.NumberSize1D.Scalar,
|
||||||
mathtype=spux.MathType.Integer,
|
mathtype=spux.MathType.Integer,
|
||||||
abs_min=1,
|
abs_min=1,
|
||||||
default_value=3,
|
default_value=3,
|
||||||
),
|
),
|
||||||
'α Range': sockets.ExprSocketDef(
|
'α Range': sockets.ExprSocketDef(
|
||||||
shape=(2,),
|
size=spux.NumberSize1D.Vec2,
|
||||||
mathtype=spux.MathType.Real,
|
mathtype=spux.MathType.Real,
|
||||||
default_value=sp.Matrix([0, 1.5]),
|
default_value=sp.Matrix([0, 1.5]),
|
||||||
abs_min=0,
|
abs_min=0,
|
||||||
|
|
|
@ -59,7 +59,6 @@ class PhysicalConstantNode(base.MaxwellSimNode):
|
||||||
|
|
||||||
size: spux.NumberSize1D = bl_cache.BLField(
|
size: spux.NumberSize1D = bl_cache.BLField(
|
||||||
enum_cb=lambda self, _: self.search_sizes(),
|
enum_cb=lambda self, _: self.search_sizes(),
|
||||||
prop_ui=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
@ -75,7 +74,7 @@ class PhysicalConstantNode(base.MaxwellSimNode):
|
||||||
return [
|
return [
|
||||||
spux.NumberSize1D.from_shape(shape).bl_enum_element(i)
|
spux.NumberSize1D.from_shape(shape).bl_enum_element(i)
|
||||||
for i, shape in enumerate(self.physical_type.valid_shapes)
|
for i, shape in enumerate(self.physical_type.valid_shapes)
|
||||||
if spux.NumberSize1D.supports_shape(shape)
|
if spux.NumberSize1D.has_shape(shape)
|
||||||
]
|
]
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
|
|
@ -16,11 +16,11 @@
|
||||||
|
|
||||||
"""Implements `SceneNode`."""
|
"""Implements `SceneNode`."""
|
||||||
|
|
||||||
import enum
|
|
||||||
import typing as typ
|
import typing as typ
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import sympy as sp
|
import sympy as sp
|
||||||
|
import sympy.physics.units as spu
|
||||||
|
|
||||||
from blender_maxwell.utils import bl_cache, logger
|
from blender_maxwell.utils import bl_cache, logger
|
||||||
from blender_maxwell.utils import extra_sympy_units as spux
|
from blender_maxwell.utils import extra_sympy_units as spux
|
||||||
|
@ -45,7 +45,11 @@ class SceneNode(base.MaxwellSimNode):
|
||||||
input_sockets: typ.ClassVar = {
|
input_sockets: typ.ClassVar = {
|
||||||
'Frames / Unit': sockets.ExprSocketDef(
|
'Frames / Unit': sockets.ExprSocketDef(
|
||||||
mathtype=spux.MathType.Integer,
|
mathtype=spux.MathType.Integer,
|
||||||
default_value=24,
|
default_value=48,
|
||||||
|
),
|
||||||
|
'Unit': sockets.ExprSocketDef(
|
||||||
|
default_unit=spu.ps,
|
||||||
|
default_value=1,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
output_sockets: typ.ClassVar = {
|
output_sockets: typ.ClassVar = {
|
||||||
|
@ -60,7 +64,7 @@ class SceneNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
# - Properties: Frame
|
# - Properties: Frame
|
||||||
####################
|
####################
|
||||||
@property
|
@bl_cache.cached_bl_property()
|
||||||
def scene_frame(self) -> int:
|
def scene_frame(self) -> int:
|
||||||
"""Retrieve the current frame of the scene.
|
"""Retrieve the current frame of the scene.
|
||||||
|
|
||||||
|
@ -71,6 +75,7 @@ class SceneNode(base.MaxwellSimNode):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def scene_frame_range(self) -> ct.LazyArrayRangeFlow:
|
def scene_frame_range(self) -> ct.LazyArrayRangeFlow:
|
||||||
|
"""Retrieve the current start/end frame of the scene, with `steps` corresponding to single-frame steps."""
|
||||||
frame_start = bpy.context.scene.frame_start
|
frame_start = bpy.context.scene.frame_start
|
||||||
frame_stop = bpy.context.scene.frame_end
|
frame_stop = bpy.context.scene.frame_end
|
||||||
return ct.LazyArrayRangeFlow(
|
return ct.LazyArrayRangeFlow(
|
||||||
|
@ -79,69 +84,20 @@ class SceneNode(base.MaxwellSimNode):
|
||||||
steps=frame_stop - frame_start + 1,
|
steps=frame_stop - frame_start + 1,
|
||||||
)
|
)
|
||||||
|
|
||||||
####################
|
|
||||||
# - Property: Time Unit
|
|
||||||
####################
|
|
||||||
active_time_unit: enum.Enum = bl_cache.BLField(
|
|
||||||
enum_cb=lambda self, _: self.search_units(), prop_ui=True
|
|
||||||
)
|
|
||||||
|
|
||||||
def search_units(self) -> list[ct.BLEnumElement]:
|
|
||||||
return [
|
|
||||||
(sp.sstr(unit), spux.sp_to_str(unit), sp.sstr(unit), '', i)
|
|
||||||
for i, unit in enumerate(spux.PhysicalType.Time.valid_units)
|
|
||||||
]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def time_unit(self) -> spux.Unit | None:
|
|
||||||
"""Gets the current active unit.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The current active `sympy` unit.
|
|
||||||
|
|
||||||
If the socket expression is unitless, this returns `None`.
|
|
||||||
"""
|
|
||||||
if self.active_time_unit is not None:
|
|
||||||
return spux.unit_str_to_unit(self.active_time_unit)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
@time_unit.setter
|
|
||||||
def time_unit(self, time_unit: spux.Unit | None) -> None:
|
|
||||||
"""Set the unit, without touching the `raw_*` UI properties.
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
To set a new unit, **and** convert the `raw_*` UI properties to the new unit, use `self.convert_unit()` instead.
|
|
||||||
"""
|
|
||||||
if time_unit in spux.PhysicalType.Time.valid_units:
|
|
||||||
self.active_time_unit = sp.sstr(time_unit)
|
|
||||||
else:
|
|
||||||
msg = f'Tried to set invalid time unit {time_unit}'
|
|
||||||
raise ValueError(msg)
|
|
||||||
|
|
||||||
####################
|
|
||||||
# - UI
|
|
||||||
####################
|
|
||||||
def draw_props(self, _: bpy.types.Context, col: bpy.types.UILayout) -> None:
|
|
||||||
"""Draws the button that allows toggling between single and range output.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
col: Target for defining UI elements.
|
|
||||||
"""
|
|
||||||
col.prop(self, self.blfields['active_time_unit'], toggle=True, text='Unit')
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - FlowKinds
|
# - FlowKinds
|
||||||
####################
|
####################
|
||||||
@events.computes_output_socket(
|
@events.computes_output_socket(
|
||||||
'Time',
|
'Time',
|
||||||
kind=ct.FlowKind.Value,
|
kind=ct.FlowKind.Value,
|
||||||
input_sockets={'Frames / Unit'},
|
input_sockets={'Frames / Unit', 'Unit'},
|
||||||
props={'scene_frame', 'active_time_unit', 'time_unit'},
|
props={'scene_frame'},
|
||||||
)
|
)
|
||||||
def compute_time(self, props, input_sockets) -> sp.Expr:
|
def compute_time(self, props, input_sockets) -> sp.Expr:
|
||||||
return (
|
return (
|
||||||
props['scene_frame'] / input_sockets['Frames / Unit'] * props['time_unit']
|
props['scene_frame']
|
||||||
|
/ input_sockets['Frames / Unit']
|
||||||
|
* input_sockets['Unit']
|
||||||
)
|
)
|
||||||
|
|
||||||
@events.computes_output_socket(
|
@events.computes_output_socket(
|
||||||
|
@ -159,10 +115,18 @@ class SceneNode(base.MaxwellSimNode):
|
||||||
BL_REGISTER = [
|
BL_REGISTER = [
|
||||||
SceneNode,
|
SceneNode,
|
||||||
]
|
]
|
||||||
|
BL_NODES = {ct.NodeType.Scene: (ct.NodeCategory.MAXWELLSIM_INPUTS)}
|
||||||
|
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Blender Handlers
|
||||||
|
####################
|
||||||
@bpy.app.handlers.persistent
|
@bpy.app.handlers.persistent
|
||||||
def update_scene_node_after_frame_changed(scene, depsgraph) -> None:
|
def update_scene_node_after_frame_changed(
|
||||||
|
scene: bpy.types.Scene, # noqa: ARG001
|
||||||
|
depsgraph: bpy.types.Depsgraph, # noqa: ARG001
|
||||||
|
) -> None:
|
||||||
|
"""Invalidate the cached scene frame on all `SceneNode`s in all active simulation node trees, whenever the frame changes."""
|
||||||
for node_tree in [
|
for node_tree in [
|
||||||
_node_tree
|
_node_tree
|
||||||
for _node_tree in bpy.data.node_groups
|
for _node_tree in bpy.data.node_groups
|
||||||
|
@ -173,9 +137,7 @@ def update_scene_node_after_frame_changed(scene, depsgraph) -> None:
|
||||||
for _node in node_tree.nodes
|
for _node in node_tree.nodes
|
||||||
if hasattr(_node, 'node_type') and _node.node_type == ct.NodeType.Scene
|
if hasattr(_node, 'node_type') and _node.node_type == ct.NodeType.Scene
|
||||||
]:
|
]:
|
||||||
node.trigger_event(ct.FlowEvent.DataChanged, prop_name='scene_frame')
|
node.scene_frame = bl_cache.Signal.InvalidateCache
|
||||||
|
|
||||||
|
|
||||||
bpy.app.handlers.frame_change_post.append(update_scene_node_after_frame_changed)
|
bpy.app.handlers.frame_change_post.append(update_scene_node_after_frame_changed)
|
||||||
|
|
||||||
BL_NODES = {ct.NodeType.Scene: (ct.NodeCategory.MAXWELLSIM_INPUTS)}
|
|
||||||
|
|
|
@ -49,36 +49,28 @@ class WaveConstantNode(base.MaxwellSimNode):
|
||||||
input_socket_sets: typ.ClassVar = {
|
input_socket_sets: typ.ClassVar = {
|
||||||
'Wavelength': {
|
'Wavelength': {
|
||||||
'WL': sockets.ExprSocketDef(
|
'WL': sockets.ExprSocketDef(
|
||||||
active_kind=ct.FlowKind.Value,
|
|
||||||
physical_type=spux.PhysicalType.Length,
|
|
||||||
# Defaults
|
|
||||||
default_unit=spu.nm,
|
default_unit=spu.nm,
|
||||||
default_value=500,
|
default_value=500,
|
||||||
default_min=200,
|
default_min=200,
|
||||||
default_max=700,
|
default_max=700,
|
||||||
default_steps=2,
|
default_steps=50,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
'Frequency': {
|
'Frequency': {
|
||||||
'Freq': sockets.ExprSocketDef(
|
'Freq': sockets.ExprSocketDef(
|
||||||
active_kind=ct.FlowKind.Value,
|
|
||||||
physical_type=spux.PhysicalType.Freq,
|
|
||||||
# Defaults
|
|
||||||
default_unit=spux.THz,
|
default_unit=spux.THz,
|
||||||
default_value=1,
|
default_value=1,
|
||||||
default_min=0.3,
|
default_min=0.3,
|
||||||
default_max=3,
|
default_max=3,
|
||||||
default_steps=2,
|
default_steps=50,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
output_sockets: typ.ClassVar = {
|
output_sockets: typ.ClassVar = {
|
||||||
'WL': sockets.ExprSocketDef(
|
'WL': sockets.ExprSocketDef(
|
||||||
active_kind=ct.FlowKind.Value,
|
|
||||||
physical_type=spux.PhysicalType.Length,
|
physical_type=spux.PhysicalType.Length,
|
||||||
),
|
),
|
||||||
'Freq': sockets.ExprSocketDef(
|
'Freq': sockets.ExprSocketDef(
|
||||||
active_kind=ct.FlowKind.Value,
|
|
||||||
physical_type=spux.PhysicalType.Freq,
|
physical_type=spux.PhysicalType.Freq,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
@ -86,7 +78,7 @@ class WaveConstantNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
# - Properties
|
# - Properties
|
||||||
####################
|
####################
|
||||||
use_range: bool = bl_cache.BLField(False, prop_ui=True)
|
use_range: bool = bl_cache.BLField(False)
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - UI
|
# - UI
|
||||||
|
@ -192,6 +184,7 @@ class WaveConstantNode(base.MaxwellSimNode):
|
||||||
sci_constants.vac_speed_of_light / (freq.start * freq.unit), spu.um
|
sci_constants.vac_speed_of_light / (freq.start * freq.unit), spu.um
|
||||||
),
|
),
|
||||||
steps=freq.steps,
|
steps=freq.steps,
|
||||||
|
scaling=freq.scaling,
|
||||||
unit=spu.um,
|
unit=spu.um,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -220,6 +213,7 @@ class WaveConstantNode(base.MaxwellSimNode):
|
||||||
sci_constants.vac_speed_of_light / (wl.start * wl.unit), spux.THz
|
sci_constants.vac_speed_of_light / (wl.start * wl.unit), spux.THz
|
||||||
),
|
),
|
||||||
steps=wl.steps,
|
steps=wl.steps,
|
||||||
|
scaling=wl.scaling,
|
||||||
unit=spux.THz,
|
unit=spux.THz,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -109,11 +109,7 @@ class LibraryMediumNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
# - Sockets
|
# - Sockets
|
||||||
####################
|
####################
|
||||||
input_sockets: typ.ClassVar = {
|
input_sockets: typ.ClassVar = {}
|
||||||
'Generated Steps': sockets.ExprSocketDef(
|
|
||||||
mathtype=spux.MathType.Integer, default_value=2, abs_min=2
|
|
||||||
)
|
|
||||||
}
|
|
||||||
output_sockets: typ.ClassVar = {
|
output_sockets: typ.ClassVar = {
|
||||||
'Medium': sockets.MaxwellMediumSocketDef(),
|
'Medium': sockets.MaxwellMediumSocketDef(),
|
||||||
'Valid Freqs': sockets.ExprSocketDef(
|
'Valid Freqs': sockets.ExprSocketDef(
|
||||||
|
@ -133,9 +129,9 @@ class LibraryMediumNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
# - Properties
|
# - Properties
|
||||||
####################
|
####################
|
||||||
vendored_medium: VendoredMedium = bl_cache.BLField(VendoredMedium.Au, prop_ui=True)
|
vendored_medium: VendoredMedium = bl_cache.BLField(VendoredMedium.Au)
|
||||||
variant_name: enum.Enum = bl_cache.BLField(
|
variant_name: enum.StrEnum = bl_cache.BLField(
|
||||||
prop_ui=True, enum_cb=lambda self, _: self.search_variants()
|
enum_cb=lambda self, _: self.search_variants()
|
||||||
)
|
)
|
||||||
|
|
||||||
def search_variants(self) -> list[ct.BLEnumElement]:
|
def search_variants(self) -> list[ct.BLEnumElement]:
|
||||||
|
@ -145,28 +141,28 @@ class LibraryMediumNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
# - Computed
|
# - Computed
|
||||||
####################
|
####################
|
||||||
@property
|
@bl_cache.cached_bl_property(depends_on={'vendored_medium', 'variant_name'})
|
||||||
def variant(self) -> Tidy3DMediumVariant:
|
def variant(self) -> Tidy3DMediumVariant:
|
||||||
"""Deduce the actual medium variant from `self.vendored_medium` and `self.variant_name`."""
|
"""Deduce the actual medium variant from `self.vendored_medium` and `self.variant_name`."""
|
||||||
return self.vendored_medium.medium_variants[self.variant_name]
|
return self.vendored_medium.medium_variants[self.variant_name]
|
||||||
|
|
||||||
@property
|
@bl_cache.cached_bl_property(depends_on={'variant'})
|
||||||
def medium(self) -> td.PoleResidue:
|
def medium(self) -> td.PoleResidue:
|
||||||
"""Deduce the actual currently selected `PoleResidue` medium from `self.variant`."""
|
"""Deduce the actual currently selected `PoleResidue` medium from `self.variant`."""
|
||||||
return self.variant.medium
|
return self.variant.medium
|
||||||
|
|
||||||
@property
|
@bl_cache.cached_bl_property(depends_on={'variant'})
|
||||||
def data_url(self) -> str | None:
|
def data_url(self) -> str | None:
|
||||||
"""Deduce the URL associated with the currently selected medium from `self.variant`."""
|
"""Deduce the URL associated with the currently selected medium from `self.variant`."""
|
||||||
return self.variant.data_url
|
return self.variant.data_url
|
||||||
|
|
||||||
@property
|
@bl_cache.cached_bl_property(depends_on={'variant'})
|
||||||
def references(self) -> td.PoleResidue:
|
def references(self) -> td.PoleResidue:
|
||||||
"""Deduce the references associated with the currently selected `PoleResidue` medium from `self.variant`."""
|
"""Deduce the references associated with the currently selected `PoleResidue` medium from `self.variant`."""
|
||||||
return self.variant.reference
|
return self.variant.reference
|
||||||
|
|
||||||
@property
|
@bl_cache.cached_bl_property(depends_on={'medium'})
|
||||||
def freq_range(self) -> spux.SympyExpr:
|
def freq_range(self) -> sp.Expr:
|
||||||
"""Deduce the frequency range as a unit-aware (THz, for convenience) column vector.
|
"""Deduce the frequency range as a unit-aware (THz, for convenience) column vector.
|
||||||
|
|
||||||
A rational approximation to each frequency bound is computed with `sp.nsimplify`, in order to **guarantee** lack of precision-loss as computations are performed on the frequency.
|
A rational approximation to each frequency bound is computed with `sp.nsimplify`, in order to **guarantee** lack of precision-loss as computations are performed on the frequency.
|
||||||
|
@ -178,8 +174,8 @@ class LibraryMediumNode(base.MaxwellSimNode):
|
||||||
spux.terahertz,
|
spux.terahertz,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@bl_cache.cached_bl_property(depends_on={'freq_range'})
|
||||||
def wl_range(self) -> spux.SympyExpr:
|
def wl_range(self) -> sp.Expr:
|
||||||
"""Deduce the vacuum wavelength range as a unit-aware (nanometer, for convenience) column vector."""
|
"""Deduce the vacuum wavelength range as a unit-aware (nanometer, for convenience) column vector."""
|
||||||
return sp.Matrix(
|
return sp.Matrix(
|
||||||
self.freq_range.applyfunc(
|
self.freq_range.applyfunc(
|
||||||
|
@ -203,12 +199,12 @@ class LibraryMediumNode(base.MaxwellSimNode):
|
||||||
formatted_str = f'{number:.2e}'
|
formatted_str = f'{number:.2e}'
|
||||||
return formatted_str
|
return formatted_str
|
||||||
|
|
||||||
@bl_cache.cached_bl_property()
|
@bl_cache.cached_bl_property(depends_on={'freq_range'})
|
||||||
def ui_freq_range(self) -> tuple[str, str]:
|
def ui_freq_range(self) -> tuple[str, str]:
|
||||||
"""Cached mirror of `self.wl_range` which contains UI-ready strings."""
|
"""Cached mirror of `self.wl_range` which contains UI-ready strings."""
|
||||||
return tuple([self._ui_range_format(el) for el in self.freq_range])
|
return tuple([self._ui_range_format(el) for el in self.freq_range])
|
||||||
|
|
||||||
@bl_cache.cached_bl_property()
|
@bl_cache.cached_bl_property(depends_on={'wl_range'})
|
||||||
def ui_wl_range(self) -> tuple[str, str]:
|
def ui_wl_range(self) -> tuple[str, str]:
|
||||||
"""Cached mirror of `self.wl_range` which contains UI-ready strings."""
|
"""Cached mirror of `self.wl_range` which contains UI-ready strings."""
|
||||||
return tuple([self._ui_range_format(el) for el in self.wl_range])
|
return tuple([self._ui_range_format(el) for el in self.wl_range])
|
||||||
|
@ -279,14 +275,13 @@ class LibraryMediumNode(base.MaxwellSimNode):
|
||||||
'Valid Freqs',
|
'Valid Freqs',
|
||||||
kind=ct.FlowKind.LazyArrayRange,
|
kind=ct.FlowKind.LazyArrayRange,
|
||||||
props={'freq_range'},
|
props={'freq_range'},
|
||||||
input_sockets={'Generated Steps'},
|
|
||||||
)
|
)
|
||||||
def compute_valid_freqs_lazy(self, props, input_sockets) -> sp.Expr:
|
def compute_valid_freqs_lazy(self, props) -> sp.Expr:
|
||||||
return ct.LazyArrayRangeFlow(
|
return ct.LazyArrayRangeFlow(
|
||||||
start=props['freq_range'][0] / spux.THz,
|
start=props['freq_range'][0] / spux.THz,
|
||||||
stop=props['freq_range'][1] / spux.THz,
|
stop=props['freq_range'][1] / spux.THz,
|
||||||
steps=input_sockets['Generated Steps'],
|
steps=0,
|
||||||
scaling='lin',
|
scaling=ct.ScalingMode.Lin,
|
||||||
unit=spux.THz,
|
unit=spux.THz,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -301,14 +296,13 @@ class LibraryMediumNode(base.MaxwellSimNode):
|
||||||
'Valid WLs',
|
'Valid WLs',
|
||||||
kind=ct.FlowKind.LazyArrayRange,
|
kind=ct.FlowKind.LazyArrayRange,
|
||||||
props={'wl_range'},
|
props={'wl_range'},
|
||||||
input_sockets={'Generated Steps'},
|
|
||||||
)
|
)
|
||||||
def compute_valid_wls_lazy(self, props, input_sockets) -> sp.Expr:
|
def compute_valid_wls_lazy(self, props) -> sp.Expr:
|
||||||
return ct.LazyArrayRangeFlow(
|
return ct.LazyArrayRangeFlow(
|
||||||
start=props['wl_range'][0] / spu.nm,
|
start=props['wl_range'][0] / spu.nm,
|
||||||
stop=props['wl_range'][0] / spu.nm,
|
stop=props['wl_range'][0] / spu.nm,
|
||||||
steps=input_sockets['Generated Steps'],
|
steps=0,
|
||||||
scaling='lin',
|
scaling=ct.ScalingMode.Lin,
|
||||||
unit=spu.nm,
|
unit=spu.nm,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -43,16 +43,16 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
input_sockets: typ.ClassVar = {
|
input_sockets: typ.ClassVar = {
|
||||||
'Center': sockets.ExprSocketDef(
|
'Center': sockets.ExprSocketDef(
|
||||||
shape=(3,),
|
size=spux.NumberSize1D.Vec3,
|
||||||
physical_type=spux.PhysicalType.Length,
|
physical_type=spux.PhysicalType.Length,
|
||||||
),
|
),
|
||||||
'Size': sockets.ExprSocketDef(
|
'Size': sockets.ExprSocketDef(
|
||||||
shape=(3,),
|
size=spux.NumberSize1D.Vec3,
|
||||||
physical_type=spux.PhysicalType.Length,
|
physical_type=spux.PhysicalType.Length,
|
||||||
default_value=sp.Matrix([1, 1, 1]),
|
default_value=sp.Matrix([1, 1, 1]),
|
||||||
),
|
),
|
||||||
'Spatial Subdivs': sockets.ExprSocketDef(
|
'Spatial Subdivs': sockets.ExprSocketDef(
|
||||||
shape=(3,),
|
size=spux.NumberSize1D.Vec3,
|
||||||
mathtype=spux.MathType.Integer,
|
mathtype=spux.MathType.Integer,
|
||||||
default_value=sp.Matrix([10, 10, 10]),
|
default_value=sp.Matrix([10, 10, 10]),
|
||||||
),
|
),
|
||||||
|
|
|
@ -41,16 +41,16 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
input_sockets: typ.ClassVar = {
|
input_sockets: typ.ClassVar = {
|
||||||
'Center': sockets.ExprSocketDef(
|
'Center': sockets.ExprSocketDef(
|
||||||
shape=(3,),
|
size=spux.NumberSize1D.Vec3,
|
||||||
physical_type=spux.PhysicalType.Length,
|
physical_type=spux.PhysicalType.Length,
|
||||||
),
|
),
|
||||||
'Size': sockets.ExprSocketDef(
|
'Size': sockets.ExprSocketDef(
|
||||||
shape=(3,),
|
size=spux.NumberSize1D.Vec3,
|
||||||
physical_type=spux.PhysicalType.Length,
|
physical_type=spux.PhysicalType.Length,
|
||||||
default_value=sp.Matrix([1, 1, 1]),
|
default_value=sp.Matrix([1, 1, 1]),
|
||||||
),
|
),
|
||||||
'Samples/Space': sockets.ExprSocketDef(
|
'Samples/Space': sockets.ExprSocketDef(
|
||||||
shape=(3,),
|
size=spux.NumberSize1D.Vec3,
|
||||||
mathtype=spux.MathType.Integer,
|
mathtype=spux.MathType.Integer,
|
||||||
default_value=sp.Matrix([10, 10, 10]),
|
default_value=sp.Matrix([10, 10, 10]),
|
||||||
),
|
),
|
||||||
|
|
|
@ -19,8 +19,8 @@ import typing as typ
|
||||||
import bpy
|
import bpy
|
||||||
import sympy as sp
|
import sympy as sp
|
||||||
|
|
||||||
|
from blender_maxwell.utils import bl_cache, logger
|
||||||
from blender_maxwell.utils import extra_sympy_units as spux
|
from blender_maxwell.utils import extra_sympy_units as spux
|
||||||
from blender_maxwell.utils import logger
|
|
||||||
|
|
||||||
from ... import contracts as ct
|
from ... import contracts as ct
|
||||||
from ... import sockets
|
from ... import sockets
|
||||||
|
@ -73,33 +73,15 @@ class ViewerNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
# - Properties
|
# - Properties
|
||||||
####################
|
####################
|
||||||
print_kind: bpy.props.EnumProperty(
|
print_kind: ct.FlowKind = bl_cache.BLField(ct.FlowKind.Value)
|
||||||
name='Print Kind',
|
auto_plot: bool = bl_cache.BLField(False)
|
||||||
description='FlowKind of the input socket to print',
|
auto_3d_preview: bool = bl_cache.BLField(True)
|
||||||
items=[(kind, kind.name, kind.name) for kind in list(ct.FlowKind)],
|
|
||||||
default=ct.FlowKind.Value,
|
|
||||||
update=lambda self, context: self.on_prop_changed('print_kind', context),
|
|
||||||
)
|
|
||||||
|
|
||||||
auto_plot: bpy.props.BoolProperty(
|
|
||||||
name='Auto-Plot',
|
|
||||||
description='Whether to auto-plot anything plugged into the viewer node',
|
|
||||||
default=False,
|
|
||||||
update=lambda self, context: self.on_prop_changed('auto_plot', context),
|
|
||||||
)
|
|
||||||
|
|
||||||
auto_3d_preview: bpy.props.BoolProperty(
|
|
||||||
name='Auto 3D Preview',
|
|
||||||
description="Whether to auto-preview anything 3D, that's plugged into the viewer node",
|
|
||||||
default=True,
|
|
||||||
update=lambda self, context: self.on_prop_changed('auto_3d_preview', context),
|
|
||||||
)
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - UI
|
# - UI
|
||||||
####################
|
####################
|
||||||
def draw_props(self, _: bpy.types.Context, layout: bpy.types.UILayout):
|
def draw_props(self, _: bpy.types.Context, layout: bpy.types.UILayout):
|
||||||
layout.prop(self, 'print_kind', text='')
|
layout.prop(self, self.blfields['print_kind'], text='')
|
||||||
|
|
||||||
def draw_operators(self, _: bpy.types.Context, layout: bpy.types.UILayout):
|
def draw_operators(self, _: bpy.types.Context, layout: bpy.types.UILayout):
|
||||||
split = layout.split(factor=0.4)
|
split = layout.split(factor=0.4)
|
||||||
|
@ -118,7 +100,7 @@ class ViewerNode(base.MaxwellSimNode):
|
||||||
|
|
||||||
## Plot Options
|
## Plot Options
|
||||||
row = col.row(align=True)
|
row = col.row(align=True)
|
||||||
row.prop(self, 'auto_plot', text='Plot', toggle=True)
|
row.prop(self, self.blfields['auto_plot'], text='Plot', toggle=True)
|
||||||
row.operator(
|
row.operator(
|
||||||
RefreshPlotViewOperator.bl_idname,
|
RefreshPlotViewOperator.bl_idname,
|
||||||
text='',
|
text='',
|
||||||
|
@ -127,7 +109,7 @@ class ViewerNode(base.MaxwellSimNode):
|
||||||
|
|
||||||
## 3D Preview Options
|
## 3D Preview Options
|
||||||
row = col.row(align=True)
|
row = col.row(align=True)
|
||||||
row.prop(self, 'auto_3d_preview', text='3D Preview', toggle=True)
|
row.prop(self, self.blfields['auto_3d_preview'], text='3D Preview', toggle=True)
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Methods
|
# - Methods
|
||||||
|
|
|
@ -144,7 +144,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
# - Computed - Sim
|
# - Computed - Sim
|
||||||
####################
|
####################
|
||||||
@bl_cache.cached_bl_property(persist=False)
|
@bl_cache.cached_bl_property()
|
||||||
def sim(self) -> td.Simulation | None:
|
def sim(self) -> td.Simulation | None:
|
||||||
sim = self._compute_input('Sim')
|
sim = self._compute_input('Sim')
|
||||||
has_sim = not ct.FlowSignal.check(sim)
|
has_sim = not ct.FlowSignal.check(sim)
|
||||||
|
@ -153,7 +153,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
|
||||||
return sim
|
return sim
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@bl_cache.cached_bl_property(persist=False)
|
@bl_cache.cached_bl_property()
|
||||||
def total_monitor_data(self) -> float | None:
|
def total_monitor_data(self) -> float | None:
|
||||||
if self.sim is not None:
|
if self.sim is not None:
|
||||||
return sum(self.sim.monitors_data_size.values())
|
return sum(self.sim.monitors_data_size.values())
|
||||||
|
@ -188,8 +188,9 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
|
||||||
If one can't be loaded, return None.
|
If one can't be loaded, return None.
|
||||||
"""
|
"""
|
||||||
has_uploaded_task = self.uploaded_task_id != ''
|
has_uploaded_task = self.uploaded_task_id != ''
|
||||||
|
has_new_cloud_task = self.new_cloud_task is not None
|
||||||
|
|
||||||
if has_uploaded_task:
|
if has_uploaded_task and has_new_cloud_task:
|
||||||
return tdcloud.TidyCloudTasks.tasks(self.new_cloud_task.cloud_folder).get(
|
return tdcloud.TidyCloudTasks.tasks(self.new_cloud_task.cloud_folder).get(
|
||||||
self.uploaded_task_id
|
self.uploaded_task_id
|
||||||
)
|
)
|
||||||
|
@ -206,7 +207,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
|
||||||
return tdcloud.TidyCloudTasks.task_info(self.uploaded_task_id)
|
return tdcloud.TidyCloudTasks.task_info(self.uploaded_task_id)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@bl_cache.cached_bl_property(persist=False)
|
@bl_cache.cached_bl_property()
|
||||||
def uploaded_est_cost(self) -> float | None:
|
def uploaded_est_cost(self) -> float | None:
|
||||||
task_info = self.uploaded_task_info
|
task_info = self.uploaded_task_info
|
||||||
if task_info is not None:
|
if task_info is not None:
|
||||||
|
@ -219,7 +220,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
# - Computed - Combined
|
# - Computed - Combined
|
||||||
####################
|
####################
|
||||||
@bl_cache.cached_bl_property(persist=False)
|
@bl_cache.cached_bl_property()
|
||||||
def is_sim_uploadable(self) -> bool:
|
def is_sim_uploadable(self) -> bool:
|
||||||
if (
|
if (
|
||||||
self.sim is not None
|
self.sim is not None
|
||||||
|
|
|
@ -43,14 +43,14 @@ class SimDomainNode(base.MaxwellSimNode):
|
||||||
abs_min=0,
|
abs_min=0,
|
||||||
),
|
),
|
||||||
'Center': sockets.ExprSocketDef(
|
'Center': sockets.ExprSocketDef(
|
||||||
shape=(3,),
|
size=spux.NumberSize1D.Vec3,
|
||||||
mathtype=spux.MathType.Real,
|
mathtype=spux.MathType.Real,
|
||||||
physical_type=spux.PhysicalType.Length,
|
physical_type=spux.PhysicalType.Length,
|
||||||
default_unit=spu.micrometer,
|
default_unit=spu.micrometer,
|
||||||
default_value=sp.Matrix([0, 0, 0]),
|
default_value=sp.Matrix([0, 0, 0]),
|
||||||
),
|
),
|
||||||
'Size': sockets.ExprSocketDef(
|
'Size': sockets.ExprSocketDef(
|
||||||
shape=(3,),
|
size=spux.NumberSize1D.Vec3,
|
||||||
mathtype=spux.MathType.Real,
|
mathtype=spux.MathType.Real,
|
||||||
physical_type=spux.PhysicalType.Length,
|
physical_type=spux.PhysicalType.Length,
|
||||||
default_unit=spu.micrometer,
|
default_unit=spu.micrometer,
|
||||||
|
|
|
@ -54,13 +54,13 @@ class GaussianBeamSourceNode(base.MaxwellSimNode):
|
||||||
input_sockets: typ.ClassVar = {
|
input_sockets: typ.ClassVar = {
|
||||||
'Temporal Shape': sockets.MaxwellTemporalShapeSocketDef(),
|
'Temporal Shape': sockets.MaxwellTemporalShapeSocketDef(),
|
||||||
'Center': sockets.ExprSocketDef(
|
'Center': sockets.ExprSocketDef(
|
||||||
shape=(3,),
|
size=spux.NumberSize1D.Vec3,
|
||||||
mathtype=spux.MathType.Real,
|
mathtype=spux.MathType.Real,
|
||||||
physical_type=spux.PhysicalType.Length,
|
physical_type=spux.PhysicalType.Length,
|
||||||
default_value=sp.Matrix([0, 0, 0]),
|
default_value=sp.Matrix([0, 0, 0]),
|
||||||
),
|
),
|
||||||
'Size': sockets.ExprSocketDef(
|
'Size': sockets.ExprSocketDef(
|
||||||
shape=(2,),
|
size=spux.NumberSize1D.Vec2,
|
||||||
mathtype=spux.MathType.Real,
|
mathtype=spux.MathType.Real,
|
||||||
physical_type=spux.PhysicalType.Length,
|
physical_type=spux.PhysicalType.Length,
|
||||||
default_value=sp.Matrix([1, 1]),
|
default_value=sp.Matrix([1, 1]),
|
||||||
|
@ -77,7 +77,7 @@ class GaussianBeamSourceNode(base.MaxwellSimNode):
|
||||||
abs_min=0.01,
|
abs_min=0.01,
|
||||||
),
|
),
|
||||||
'Spherical': sockets.ExprSocketDef(
|
'Spherical': sockets.ExprSocketDef(
|
||||||
shape=(2,),
|
size=spux.NumberSize1D.Vec2,
|
||||||
mathtype=spux.MathType.Real,
|
mathtype=spux.MathType.Real,
|
||||||
physical_type=spux.PhysicalType.Angle,
|
physical_type=spux.PhysicalType.Angle,
|
||||||
default_value=sp.Matrix([0, 0]),
|
default_value=sp.Matrix([0, 0]),
|
||||||
|
|
|
@ -52,13 +52,13 @@ class PlaneWaveSourceNode(base.MaxwellSimNode):
|
||||||
input_sockets: typ.ClassVar = {
|
input_sockets: typ.ClassVar = {
|
||||||
'Temporal Shape': sockets.MaxwellTemporalShapeSocketDef(),
|
'Temporal Shape': sockets.MaxwellTemporalShapeSocketDef(),
|
||||||
'Center': sockets.ExprSocketDef(
|
'Center': sockets.ExprSocketDef(
|
||||||
shape=(3,),
|
size=spux.NumberSize1D.Vec3,
|
||||||
mathtype=spux.MathType.Real,
|
mathtype=spux.MathType.Real,
|
||||||
physical_type=spux.PhysicalType.Length,
|
physical_type=spux.PhysicalType.Length,
|
||||||
default_value=sp.Matrix([0, 0, 0]),
|
default_value=sp.Matrix([0, 0, 0]),
|
||||||
),
|
),
|
||||||
'Spherical': sockets.ExprSocketDef(
|
'Spherical': sockets.ExprSocketDef(
|
||||||
shape=(2,),
|
size=spux.NumberSize1D.Vec2,
|
||||||
mathtype=spux.MathType.Real,
|
mathtype=spux.MathType.Real,
|
||||||
physical_type=spux.PhysicalType.Angle,
|
physical_type=spux.PhysicalType.Angle,
|
||||||
default_value=sp.Matrix([0, 0]),
|
default_value=sp.Matrix([0, 0]),
|
||||||
|
|
|
@ -42,7 +42,7 @@ class PointDipoleSourceNode(base.MaxwellSimNode):
|
||||||
input_sockets: typ.ClassVar = {
|
input_sockets: typ.ClassVar = {
|
||||||
'Temporal Shape': sockets.MaxwellTemporalShapeSocketDef(),
|
'Temporal Shape': sockets.MaxwellTemporalShapeSocketDef(),
|
||||||
'Center': sockets.ExprSocketDef(
|
'Center': sockets.ExprSocketDef(
|
||||||
shape=(3,),
|
size=spux.NumberSize1D.Vec3,
|
||||||
mathtype=spux.MathType.Real,
|
mathtype=spux.MathType.Real,
|
||||||
physical_type=spux.PhysicalType.Length,
|
physical_type=spux.PhysicalType.Length,
|
||||||
default_value=sp.Matrix([0, 0, 0]),
|
default_value=sp.Matrix([0, 0, 0]),
|
||||||
|
|
|
@ -42,7 +42,7 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
|
||||||
'GeoNodes': sockets.BlenderGeoNodesSocketDef(),
|
'GeoNodes': sockets.BlenderGeoNodesSocketDef(),
|
||||||
'Medium': sockets.MaxwellMediumSocketDef(),
|
'Medium': sockets.MaxwellMediumSocketDef(),
|
||||||
'Center': sockets.ExprSocketDef(
|
'Center': sockets.ExprSocketDef(
|
||||||
shape=(3,),
|
size=spux.NumberSize1D.Vec3,
|
||||||
mathtype=spux.MathType.Real,
|
mathtype=spux.MathType.Real,
|
||||||
physical_type=spux.PhysicalType.Length,
|
physical_type=spux.PhysicalType.Length,
|
||||||
default_unit=spu.micrometer,
|
default_unit=spu.micrometer,
|
||||||
|
|
|
@ -42,14 +42,14 @@ class BoxStructureNode(base.MaxwellSimNode):
|
||||||
input_sockets: typ.ClassVar = {
|
input_sockets: typ.ClassVar = {
|
||||||
'Medium': sockets.MaxwellMediumSocketDef(),
|
'Medium': sockets.MaxwellMediumSocketDef(),
|
||||||
'Center': sockets.ExprSocketDef(
|
'Center': sockets.ExprSocketDef(
|
||||||
shape=(3,),
|
size=spux.NumberSize1D.Vec3,
|
||||||
mathtype=spux.MathType.Real,
|
mathtype=spux.MathType.Real,
|
||||||
physical_type=spux.PhysicalType.Length,
|
physical_type=spux.PhysicalType.Length,
|
||||||
default_unit=spu.micrometer,
|
default_unit=spu.micrometer,
|
||||||
default_value=sp.Matrix([0, 0, 0]),
|
default_value=sp.Matrix([0, 0, 0]),
|
||||||
),
|
),
|
||||||
'Size': sockets.ExprSocketDef(
|
'Size': sockets.ExprSocketDef(
|
||||||
shape=(3,),
|
size=spux.NumberSize1D.Vec3,
|
||||||
mathtype=spux.MathType.Real,
|
mathtype=spux.MathType.Real,
|
||||||
physical_type=spux.PhysicalType.Length,
|
physical_type=spux.PhysicalType.Length,
|
||||||
default_unit=spu.nanometer,
|
default_unit=spu.nanometer,
|
||||||
|
|
|
@ -42,14 +42,11 @@ class SphereStructureNode(base.MaxwellSimNode):
|
||||||
input_sockets: typ.ClassVar = {
|
input_sockets: typ.ClassVar = {
|
||||||
'Medium': sockets.MaxwellMediumSocketDef(),
|
'Medium': sockets.MaxwellMediumSocketDef(),
|
||||||
'Center': sockets.ExprSocketDef(
|
'Center': sockets.ExprSocketDef(
|
||||||
shape=(3,),
|
size=spux.NumberSize1D.Vec3,
|
||||||
mathtype=spux.MathType.Real,
|
|
||||||
physical_type=spux.PhysicalType.Length,
|
|
||||||
default_unit=spu.micrometer,
|
default_unit=spu.micrometer,
|
||||||
default_value=sp.Matrix([0, 0, 0]),
|
default_value=sp.Matrix([0, 0, 0]),
|
||||||
),
|
),
|
||||||
'Radius': sockets.ExprSocketDef(
|
'Radius': sockets.ExprSocketDef(
|
||||||
physical_type=spux.PhysicalType.Length,
|
|
||||||
default_unit=spu.nanometer,
|
default_unit=spu.nanometer,
|
||||||
default_value=150,
|
default_value=150,
|
||||||
),
|
),
|
||||||
|
|
|
@ -16,13 +16,11 @@
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
import typing as typ
|
import typing as typ
|
||||||
import uuid
|
|
||||||
from types import MappingProxyType
|
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import pydantic as pyd
|
import pydantic as pyd
|
||||||
|
|
||||||
from blender_maxwell.utils import bl_cache, logger, serialize
|
from blender_maxwell.utils import bl_cache, bl_instance, logger, serialize
|
||||||
|
|
||||||
from .. import contracts as ct
|
from .. import contracts as ct
|
||||||
|
|
||||||
|
@ -60,7 +58,7 @@ class SocketDef(pyd.BaseModel, abc.ABC):
|
||||||
Parameters:
|
Parameters:
|
||||||
bl_socket: The Blender node socket to alter using data from this SocketDef.
|
bl_socket: The Blender node socket to alter using data from this SocketDef.
|
||||||
"""
|
"""
|
||||||
bl_socket.initializing = False
|
bl_socket.is_initializing = False
|
||||||
bl_socket.on_active_kind_changed()
|
bl_socket.on_active_kind_changed()
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
|
@ -116,9 +114,12 @@ class SocketDef(pyd.BaseModel, abc.ABC):
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - SocketDef
|
# - Socket
|
||||||
####################
|
####################
|
||||||
class MaxwellSimSocket(bpy.types.NodeSocket):
|
MANDATORY_PROPS: set[str] = {'socket_type', 'bl_label'}
|
||||||
|
|
||||||
|
|
||||||
|
class MaxwellSimSocket(bpy.types.NodeSocket, bl_instance.BLInstance):
|
||||||
"""A specialized Blender socket for nodes in a Maxwell simulation.
|
"""A specialized Blender socket for nodes in a Maxwell simulation.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
@ -128,112 +129,45 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
|
||||||
locked: The lock-state of a particular socket, which determines the socket's user editability
|
locked: The lock-state of a particular socket, which determines the socket's user editability
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Fundamentals
|
# Properties
|
||||||
|
## Class
|
||||||
socket_type: ct.SocketType
|
socket_type: ct.SocketType
|
||||||
bl_label: str
|
bl_label: str
|
||||||
|
|
||||||
# Style
|
## Computed by Subclass
|
||||||
display_shape: typ.Literal[
|
|
||||||
'CIRCLE',
|
|
||||||
'SQUARE',
|
|
||||||
'DIAMOND',
|
|
||||||
'CIRCLE_DOT',
|
|
||||||
'SQUARE_DOT',
|
|
||||||
'DIAMOND_DOT',
|
|
||||||
]
|
|
||||||
## We use the following conventions for shapes:
|
|
||||||
## - CIRCLE: Single Value.
|
|
||||||
## - SQUARE: Container of Value.
|
|
||||||
## - DIAMOND: Pointer Value.
|
|
||||||
## - +DOT: Uses Units
|
|
||||||
socket_color: tuple
|
|
||||||
|
|
||||||
# Options
|
|
||||||
use_prelock: bool = False
|
|
||||||
use_info_draw: bool = False
|
|
||||||
|
|
||||||
# Computed
|
|
||||||
bl_idname: str
|
bl_idname: str
|
||||||
|
|
||||||
# BLFields
|
# BLFields
|
||||||
blfields: typ.ClassVar[dict[str, str]] = MappingProxyType({})
|
## Identifying
|
||||||
ui_blfields: typ.ClassVar[set[str]] = frozenset()
|
is_initializing: bool = bl_cache.BLField(True, use_prop_update=False)
|
||||||
|
|
||||||
|
active_kind: ct.FlowKind = bl_cache.BLField(ct.FlowKind.Value)
|
||||||
|
|
||||||
|
## UI
|
||||||
|
use_info_draw: bool = bl_cache.BLField(False, use_prop_update=False)
|
||||||
|
use_prelock: bool = bl_cache.BLField(False, use_prop_update=False)
|
||||||
|
|
||||||
|
locked: bool = bl_cache.BLField(False, use_prop_update=False)
|
||||||
|
|
||||||
|
use_socket_color: bool = bl_cache.BLField(False, use_prop_update=False)
|
||||||
|
socket_color: tuple[float, float, float, float] = bl_cache.BLField(
|
||||||
|
(0, 0, 0, 0), use_prop_update=False
|
||||||
|
)
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Initialization
|
# - Initialization
|
||||||
####################
|
####################
|
||||||
## TODO: Common implementation of this for both sockets and nodes - perhaps a BLInstance base class?
|
def __init_subclass__(cls, **kwargs: typ.Any):
|
||||||
def reset_instance_id(self) -> None:
|
"""Initializes socket properties and attributes for use.
|
||||||
self.instance_id = str(uuid.uuid4())
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def declare_blfield(
|
|
||||||
cls, attr_name: str, bl_attr_name: str, prop_ui: bool = False
|
|
||||||
) -> None:
|
|
||||||
cls.blfields = cls.blfields | {attr_name: bl_attr_name}
|
|
||||||
|
|
||||||
if prop_ui:
|
|
||||||
cls.ui_blfields = cls.ui_blfields | {attr_name}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def set_prop(
|
|
||||||
cls,
|
|
||||||
prop_name: str,
|
|
||||||
prop: bpy.types.Property,
|
|
||||||
no_update: bool = False,
|
|
||||||
update_with_name: str | None = None,
|
|
||||||
**kwargs,
|
|
||||||
) -> None:
|
|
||||||
"""Adds a Blender property to a class via `__annotations__`, so it initializes with any subclass.
|
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Blender properties can't be set within `__init_subclass__` simply by adding attributes to the class; they must be added as type annotations.
|
Run when initializing any subclass of MaxwellSimSocket.
|
||||||
- Must be called **within** `__init_subclass__`.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
name: The name of the property to set.
|
|
||||||
prop: The `bpy.types.Property` to instantiate and attach..
|
|
||||||
no_update: Don't attach a `self.on_prop_changed()` callback to the property's `update`.
|
|
||||||
"""
|
"""
|
||||||
_update_with_name = prop_name if update_with_name is None else update_with_name
|
|
||||||
extra_kwargs = (
|
|
||||||
{
|
|
||||||
'update': lambda self, context: self.on_prop_changed(
|
|
||||||
_update_with_name, context
|
|
||||||
),
|
|
||||||
}
|
|
||||||
if not no_update
|
|
||||||
else {}
|
|
||||||
)
|
|
||||||
cls.__annotations__[prop_name] = prop(
|
|
||||||
**kwargs,
|
|
||||||
**extra_kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init_subclass__(cls, **kwargs: typ.Any):
|
|
||||||
log.debug('Initializing Socket: %s', cls.socket_type)
|
log.debug('Initializing Socket: %s', cls.socket_type)
|
||||||
super().__init_subclass__(**kwargs)
|
super().__init_subclass__(**kwargs)
|
||||||
# cls._assert_attrs_valid()
|
cls.assert_attrs_valid(MANDATORY_PROPS)
|
||||||
## TODO: Implement this :)
|
|
||||||
|
|
||||||
# Socket Properties
|
|
||||||
## Identifiers
|
|
||||||
cls.bl_idname: str = str(cls.socket_type.value)
|
cls.bl_idname: str = str(cls.socket_type.value)
|
||||||
cls.set_prop('instance_id', bpy.props.StringProperty, no_update=True)
|
|
||||||
cls.set_prop(
|
|
||||||
'initializing', bpy.props.BoolProperty, default=True, no_update=True
|
|
||||||
)
|
|
||||||
|
|
||||||
## Special States
|
|
||||||
cls.set_prop('locked', bpy.props.BoolProperty, no_update=True, default=False)
|
|
||||||
|
|
||||||
# Setup Style
|
|
||||||
cls.socket_color = ct.SOCKET_COLORS[cls.socket_type]
|
|
||||||
|
|
||||||
# Setup List
|
|
||||||
cls.set_prop(
|
|
||||||
'active_kind', bpy.props.StringProperty, default=str(ct.FlowKind.Value)
|
|
||||||
)
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Property Event: On Update
|
# - Property Event: On Update
|
||||||
|
@ -244,12 +178,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
|
||||||
Notes:
|
Notes:
|
||||||
Called by `self.on_prop_changed()` when `self.active_kind` was changed.
|
Called by `self.on_prop_changed()` when `self.active_kind` was changed.
|
||||||
"""
|
"""
|
||||||
self.display_shape = {
|
self.display_shape = self.active_kind.socket_shape
|
||||||
ct.FlowKind.Value: 'CIRCLE',
|
|
||||||
ct.FlowKind.Array: 'SQUARE',
|
|
||||||
ct.FlowKind.LazyArrayRange: 'SQUARE',
|
|
||||||
ct.FlowKind.LazyValueFunc: 'DIAMOND',
|
|
||||||
}[self.active_kind]
|
|
||||||
|
|
||||||
def on_socket_prop_changed(self, prop_name: str) -> None:
|
def on_socket_prop_changed(self, prop_name: str) -> None:
|
||||||
"""Called when a property has been updated.
|
"""Called when a property has been updated.
|
||||||
|
@ -264,7 +193,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
|
||||||
prop_name: The name of the property that was changed.
|
prop_name: The name of the property that was changed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def on_prop_changed(self, prop_name: str, _: bpy.types.Context) -> None:
|
def on_prop_changed(self, prop_name: str) -> None:
|
||||||
"""Called when a property has been updated.
|
"""Called when a property has been updated.
|
||||||
|
|
||||||
Contrary to `node.on_prop_changed()`, socket-specific callbacks are baked into this function:
|
Contrary to `node.on_prop_changed()`, socket-specific callbacks are baked into this function:
|
||||||
|
@ -275,17 +204,13 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
|
||||||
prop_name: The name of the property that was changed.
|
prop_name: The name of the property that was changed.
|
||||||
"""
|
"""
|
||||||
## TODO: Evaluate this properly
|
## TODO: Evaluate this properly
|
||||||
if self.initializing:
|
if self.is_initializing:
|
||||||
log.debug(
|
log.debug(
|
||||||
'%s: Rejected on_prop_changed("%s") while initializing',
|
'%s: Rejected on_prop_changed("%s") while initializing',
|
||||||
self.bl_label,
|
self.bl_label,
|
||||||
prop_name,
|
prop_name,
|
||||||
)
|
)
|
||||||
elif hasattr(self, prop_name):
|
elif hasattr(self, prop_name):
|
||||||
# Invalidate UI BLField Caches
|
|
||||||
if prop_name in self.ui_blfields:
|
|
||||||
setattr(self, prop_name, bl_cache.Signal.InvalidateCache)
|
|
||||||
|
|
||||||
# Property Callbacks: Active Kind
|
# Property Callbacks: Active Kind
|
||||||
if prop_name == 'active_kind':
|
if prop_name == 'active_kind':
|
||||||
self.on_active_kind_changed()
|
self.on_active_kind_changed()
|
||||||
|
@ -469,13 +394,13 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
|
||||||
if event in [ct.FlowEvent.EnableLock, ct.FlowEvent.DisableLock]:
|
if event in [ct.FlowEvent.EnableLock, ct.FlowEvent.DisableLock]:
|
||||||
self.locked = event == ct.FlowEvent.EnableLock
|
self.locked = event == ct.FlowEvent.EnableLock
|
||||||
|
|
||||||
# Input Socket | Input Flow
|
# Event by Socket Orientation | Flow Direction
|
||||||
if not self.is_output and flow_direction == 'input':
|
match (self.is_output, flow_direction):
|
||||||
|
case (False, 'input'):
|
||||||
for link in self.links:
|
for link in self.links:
|
||||||
link.from_socket.trigger_event(event, socket_kinds=socket_kinds)
|
link.from_socket.trigger_event(event, socket_kinds=socket_kinds)
|
||||||
|
|
||||||
# Input Socket | Output Flow
|
case (False, 'output'):
|
||||||
if not self.is_output and flow_direction == 'output':
|
|
||||||
if event == ct.FlowEvent.LinkChanged:
|
if event == ct.FlowEvent.LinkChanged:
|
||||||
self.node.trigger_event(
|
self.node.trigger_event(
|
||||||
ct.FlowEvent.DataChanged,
|
ct.FlowEvent.DataChanged,
|
||||||
|
@ -487,14 +412,12 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
|
||||||
event, socket_name=self.name, socket_kinds=socket_kinds
|
event, socket_name=self.name, socket_kinds=socket_kinds
|
||||||
)
|
)
|
||||||
|
|
||||||
# Output Socket | Input Flow
|
case (True, 'input'):
|
||||||
if self.is_output and flow_direction == 'input':
|
|
||||||
self.node.trigger_event(
|
self.node.trigger_event(
|
||||||
event, socket_name=self.name, socket_kinds=socket_kinds
|
event, socket_name=self.name, socket_kinds=socket_kinds
|
||||||
)
|
)
|
||||||
|
|
||||||
# Output Socket | Output Flow
|
case (True, 'output'):
|
||||||
if self.is_output and flow_direction == 'output':
|
|
||||||
for link in self.links:
|
for link in self.links:
|
||||||
link.to_socket.trigger_event(event, socket_kinds=socket_kinds)
|
link.to_socket.trigger_event(event, socket_kinds=socket_kinds)
|
||||||
|
|
||||||
|
@ -729,19 +652,24 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
|
||||||
raise NotImplementedError(msg)
|
raise NotImplementedError(msg)
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Theme
|
# - UI - Color
|
||||||
####################
|
####################
|
||||||
@classmethod
|
def draw_color(
|
||||||
def draw_color_simple(cls) -> ct.BLColorRGBA:
|
self,
|
||||||
"""Sets the socket's color to `cls.socket_color`.
|
_: bpy.types.Context,
|
||||||
|
node: bpy.types.Node, # noqa: ARG002
|
||||||
|
) -> tuple[float, float, float, float]:
|
||||||
|
"""Draw the socket color depending on context.
|
||||||
|
|
||||||
|
When `self.use_socket_color` is set, the property `socket_color` can be used to control the socket color directly.
|
||||||
|
Otherwise, a default based on `self.socket_type` will be used.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
Blender calls this method to determine the socket color.
|
Called by Blender to call the socket color.
|
||||||
|
|
||||||
Returns:
|
|
||||||
A Blender-compatible RGBA value, with no explicit color space.
|
|
||||||
"""
|
"""
|
||||||
return cls.socket_color
|
if self.use_socket_color:
|
||||||
|
return self.socket_color
|
||||||
|
return ct.SOCKET_COLORS[self.socket_type]
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - UI
|
# - UI
|
||||||
|
|
|
@ -18,9 +18,13 @@ from pathlib import Path
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
|
from blender_maxwell.utils import bl_cache, logger
|
||||||
|
|
||||||
from ... import contracts as ct
|
from ... import contracts as ct
|
||||||
from .. import base
|
from .. import base
|
||||||
|
|
||||||
|
log = logger.get(__name__)
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Blender Socket
|
# - Blender Socket
|
||||||
|
@ -32,30 +36,25 @@ class FilePathBLSocket(base.MaxwellSimSocket):
|
||||||
####################
|
####################
|
||||||
# - Properties
|
# - Properties
|
||||||
####################
|
####################
|
||||||
raw_value: bpy.props.StringProperty(
|
raw_value: Path = bl_cache.BLField(Path(), path_type='file')
|
||||||
name='File Path',
|
|
||||||
description='Represents the path to a file',
|
|
||||||
subtype='FILE_PATH',
|
|
||||||
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
|
|
||||||
)
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Socket UI
|
# - Socket UI
|
||||||
####################
|
####################
|
||||||
def draw_value(self, col: bpy.types.UILayout) -> None:
|
def draw_value(self, col: bpy.types.UILayout) -> None:
|
||||||
col_row = col.row(align=True)
|
# col_row = col.row(align=True)
|
||||||
col_row.prop(self, 'raw_value', text='')
|
col.prop(self, self.blfields['raw_value'], text='')
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Computation of Default Value
|
# - FlowKind: Value
|
||||||
####################
|
####################
|
||||||
@property
|
@property
|
||||||
def value(self) -> Path:
|
def value(self) -> Path:
|
||||||
return Path(bpy.path.abspath(self.raw_value))
|
return self.raw_value
|
||||||
|
|
||||||
@value.setter
|
@value.setter
|
||||||
def value(self, value: Path) -> None:
|
def value(self, value: Path) -> None:
|
||||||
self.raw_value = bpy.path.relpath(str(value))
|
self.raw_value = value
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -99,18 +99,16 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
|
||||||
socket_type = ct.SocketType.Tidy3DCloudTask
|
socket_type = ct.SocketType.Tidy3DCloudTask
|
||||||
bl_label = 'Tidy3D Cloud Task'
|
bl_label = 'Tidy3D Cloud Task'
|
||||||
|
|
||||||
use_prelock = True
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Properties
|
# - Properties
|
||||||
####################
|
####################
|
||||||
api_key: str = bl_cache.BLField('', prop_ui=True, str_secret=True)
|
api_key: str = bl_cache.BLField('', prop_ui=True, str_secret=True)
|
||||||
should_exist: bool = bl_cache.BLField(False)
|
should_exist: bool = bl_cache.BLField(False)
|
||||||
|
|
||||||
existing_folder_id: enum.Enum = bl_cache.BLField(
|
existing_folder_id: enum.StrEnum = bl_cache.BLField(
|
||||||
prop_ui=True, enum_cb=lambda self, _: self.search_cloud_folders()
|
prop_ui=True, enum_cb=lambda self, _: self.search_cloud_folders()
|
||||||
)
|
)
|
||||||
existing_task_id: enum.Enum = bl_cache.BLField(
|
existing_task_id: enum.StrEnum = bl_cache.BLField(
|
||||||
prop_ui=True, enum_cb=lambda self, _: self.search_cloud_tasks()
|
prop_ui=True, enum_cb=lambda self, _: self.search_cloud_tasks()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -299,6 +297,7 @@ class Tidy3DCloudTaskSocketDef(base.SocketDef):
|
||||||
|
|
||||||
def init(self, bl_socket: Tidy3DCloudTaskBLSocket) -> None:
|
def init(self, bl_socket: Tidy3DCloudTaskBLSocket) -> None:
|
||||||
bl_socket.should_exist = self.should_exist
|
bl_socket.should_exist = self.should_exist
|
||||||
|
bl_socket.use_prelock = True
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,36 @@
|
||||||
|
# blender_maxwell
|
||||||
|
# Copyright (C) 2024 blender_maxwell Project Contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""Package providing various tools to handle cached data on Blender objects, especially nodes and node socket classes."""
|
||||||
|
|
||||||
|
from .bl_field import BLField
|
||||||
|
from .bl_prop import BLProp, BLPropType
|
||||||
|
from .cached_bl_property import CachedBLProperty, cached_bl_property
|
||||||
|
from .keyed_cache import KeyedCache, keyed_cache
|
||||||
|
from .managed_cache import invalidate_nonpersist_instance_id
|
||||||
|
from .signal import Signal
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'BLField',
|
||||||
|
'BLProp',
|
||||||
|
'BLPropType',
|
||||||
|
'CachedBLProperty',
|
||||||
|
'cached_bl_property',
|
||||||
|
'KeyedCache',
|
||||||
|
'keyed_cache',
|
||||||
|
'invalidate_nonpersist_instance_id',
|
||||||
|
'Signal',
|
||||||
|
]
|
|
@ -0,0 +1,424 @@
|
||||||
|
# blender_maxwell
|
||||||
|
# Copyright (C) 2024 blender_maxwell Project Contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""Implements various key caches on instances of Blender objects, especially nodes and sockets."""
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import inspect
|
||||||
|
import typing as typ
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
from blender_maxwell import contracts as ct
|
||||||
|
from blender_maxwell.utils import bl_instance, logger
|
||||||
|
|
||||||
|
from .bl_prop import BLProp
|
||||||
|
from .bl_prop_type import BLPropType
|
||||||
|
from .signal import Signal
|
||||||
|
|
||||||
|
log = logger.get(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
StringPropSubType: typ.TypeAlias = typ.Literal[
|
||||||
|
'FILE_PATH', 'DIR_PATH', 'FILE_NAME', 'BYTE_STRING', 'PASSWORD', 'NONE'
|
||||||
|
]
|
||||||
|
|
||||||
|
StrMethod: typ.TypeAlias = typ.Callable[
|
||||||
|
[bl_instance.BLInstance, bpy.types.Context, str], list[tuple[str, str]]
|
||||||
|
]
|
||||||
|
EnumMethod: typ.TypeAlias = typ.Callable[
|
||||||
|
[bl_instance.BLInstance, bpy.types.Context], list[ct.BLEnumElement]
|
||||||
|
]
|
||||||
|
|
||||||
|
DEFAULT_ENUM_ITEMS_SINGLE = [('NONE', 'None', 'No items...', '', 0)]
|
||||||
|
DEFAULT_ENUM_ITEMS_MANY = [('NONE', 'None', 'No items...', '', 2**0)]
|
||||||
|
|
||||||
|
|
||||||
|
@functools.cache
|
||||||
|
def default_enum_items(enum_many: bool) -> list[ct.BLEnumElement]:
|
||||||
|
return DEFAULT_ENUM_ITEMS_MANY if enum_many else DEFAULT_ENUM_ITEMS_SINGLE
|
||||||
|
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - BLField
|
||||||
|
####################
|
||||||
|
class BLField:
|
||||||
|
"""A descriptor that allows persisting arbitrary types in Blender objects, with cached reads."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
default_value: typ.Any = None,
|
||||||
|
use_prop_update: bool = True,
|
||||||
|
## Static
|
||||||
|
prop_ui: bool = False, ## TODO: Remove
|
||||||
|
abs_min: int | float | None = None,
|
||||||
|
abs_max: int | float | None = None,
|
||||||
|
soft_min: int | float | None = None,
|
||||||
|
soft_max: int | float | None = None,
|
||||||
|
float_step: int | None = None,
|
||||||
|
float_prec: int | None = None,
|
||||||
|
str_secret: bool | None = None,
|
||||||
|
path_type: typ.Literal['dir', 'file'] | None = None,
|
||||||
|
# blptr_type: typ.Any | None = None, ## A Blender ID type
|
||||||
|
## TODO: Test/Implement
|
||||||
|
## Dynamic
|
||||||
|
str_cb: StrMethod | None = None,
|
||||||
|
enum_cb: EnumMethod | None = None,
|
||||||
|
cb_depends_on: set[str] | None = None,
|
||||||
|
) -> typ.Self:
|
||||||
|
"""Initializes and sets the attribute to a given default value.
|
||||||
|
|
||||||
|
The attribute **must** declare a type annotation, and it **must** match the type of `default_value`.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
default_value: The default value to use if the value is read before it's set.
|
||||||
|
use_prop_update: If True, `BLField` will consent to `bl_instance.on_prop_changed(attr_name)` being run whenever the field is changed.
|
||||||
|
UI changes done to the property via Blender **always** trigger `bl_instance.on_bl_prop_changed`; however, the `BLField` decides whether `on_prop_changed` should be run as well.
|
||||||
|
That control is offered through `use_prop_update`.
|
||||||
|
abs_min: Sets the absolute minimum value of the property.
|
||||||
|
Only meaningful for numerical properties.
|
||||||
|
abs_max: Sets the absolute maximum value of the property.
|
||||||
|
Only meaningful for numerical properties.
|
||||||
|
soft_min: Sets a value which will feel like a minimum in the UI, but which can be overridden by setting a value directly.
|
||||||
|
In practice, "scrolling" through values will stop here.
|
||||||
|
Only meaningful for numerical properties.
|
||||||
|
soft_max: Sets a value which will feel like a maximum in the UI, but which can be overridden by setting a value directly.
|
||||||
|
In practice, "scrolling" through values will stop here.
|
||||||
|
Only meaningful for numerical properties.
|
||||||
|
float_step: Sets the interval (/100) of each step when "scrolling" through the values of a float property, aka. the speed.
|
||||||
|
Only meaningful for float-like properties.
|
||||||
|
float_step: Sets the decimal places of precision to display.
|
||||||
|
Only meaningful for float-like properties.
|
||||||
|
str_secret: Marks the string as "secret", which prevents its save-persistance, and causes the UI to display dots instead of characters.
|
||||||
|
**DO NOT** rely on this property for "real" security.
|
||||||
|
_If in doubt, this isn't good enough._
|
||||||
|
Only meaningful for `str` properties.
|
||||||
|
path_type: Makes the path as pointing to a folder or to a file.
|
||||||
|
Only meaningful for `pathlib.Path` properties.
|
||||||
|
**NOTE**: No effort is made to make paths portable between operating systems.
|
||||||
|
Use with care.
|
||||||
|
str_cb: Method used to determine all valid strings, which presents to the user as a fuzzy-style search dropdown.
|
||||||
|
Only meaningful for `str` properties.
|
||||||
|
Results are not persisted, and must therefore re-run when reloading the file.
|
||||||
|
Otherwise, it is cached, but is re-run whenever `Signal.ResetStrSearch` is set.
|
||||||
|
enum_cb: Method used to determine all valid enum elements, which presents to the user as a dropdown.
|
||||||
|
The caveats with dynamic `bpy.props.EnumProperty`s are **absurdly sharp**.
|
||||||
|
Those caveats are entirely mitigated when using this callback, at the cost of manual resets.
|
||||||
|
Is re-run whenever `Signal.ResetEnumItems` is set, and otherwise cached both persistently and non-persistently.
|
||||||
|
cb_depends_on: Declares that `str_cb` / `enum_cb` should be regenerated whenever any of the given property names change.
|
||||||
|
This allows fully automating the invocation of `Signal.ResetEnumItems` / `Signal.ResetStrSearch` in common cases.
|
||||||
|
"""
|
||||||
|
log.debug(
|
||||||
|
'Initializing BLField (default_value=%s, use_prop_update=%s)',
|
||||||
|
str(default_value),
|
||||||
|
str(use_prop_update),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.use_dynamic_enum = enum_cb is not None
|
||||||
|
self.use_str_search = str_cb is not None
|
||||||
|
|
||||||
|
## TODO: Use prop_flags
|
||||||
|
self.prop_info = {
|
||||||
|
'default': default_value,
|
||||||
|
'use_prop_update': use_prop_update,
|
||||||
|
# Int* | Float*: Bounds
|
||||||
|
'min': abs_min,
|
||||||
|
'max': abs_max,
|
||||||
|
'soft_min': soft_min,
|
||||||
|
'soft_max': soft_max,
|
||||||
|
# Float*: UI
|
||||||
|
'step': float_step,
|
||||||
|
'precision': float_prec,
|
||||||
|
# BLPointer: ID Type
|
||||||
|
#'blptr_type': blptr_type,
|
||||||
|
# Str | Path | Enum: Flag Setters
|
||||||
|
'str_secret': str_secret,
|
||||||
|
'path_type': path_type,
|
||||||
|
# Search: Str
|
||||||
|
'str_search': self.use_str_search,
|
||||||
|
'safe_str_cb': lambda _self, context, edit_text: self.safe_str_cb(
|
||||||
|
_self, context, edit_text
|
||||||
|
),
|
||||||
|
# Search: Enum
|
||||||
|
'enum_dynamic': self.use_dynamic_enum,
|
||||||
|
'safe_enum_cb': lambda _self, context: self.safe_enum_cb(_self, context),
|
||||||
|
}
|
||||||
|
|
||||||
|
# BLProp
|
||||||
|
self.bl_prop: BLProp | None = None
|
||||||
|
self.bl_prop_enum_items: BLProp | None = None
|
||||||
|
self.bl_prop_str_search: BLProp | None = None
|
||||||
|
|
||||||
|
self.enum_cb = enum_cb
|
||||||
|
self.str_cb = str_cb
|
||||||
|
|
||||||
|
self.cb_depends_on: set[str] | None = cb_depends_on
|
||||||
|
|
||||||
|
# Update Suppressing
|
||||||
|
self.suppress_update: dict[str, bool] = {}
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Descriptor Setup
|
||||||
|
####################
|
||||||
|
def __set_name__(self, owner: type[bl_instance.BLInstance], name: str) -> None:
|
||||||
|
"""Sets up this descriptor on the class, preparing it for per-instance use.
|
||||||
|
|
||||||
|
A `BLProp` is constructed using this descriptor's attribute name on `owner`, and the `self.prop_info` previously created during `self.__init__()`.
|
||||||
|
Then, a corresponding / underlying `bpy.types.Property` is initialized on `owner` using `self.bl_prop.init_bl_type(owner)`
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
Run by Python when setting an instance of a "descriptor" class, to an attribute of another class (denoted `owner`).
|
||||||
|
For more, search for "Python descriptor protocol".
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
prop_type = inspect.get_annotations(owner).get(name)
|
||||||
|
self.bl_prop = BLProp(
|
||||||
|
name=name,
|
||||||
|
prop_info=self.prop_info,
|
||||||
|
prop_type=prop_type,
|
||||||
|
bl_prop_type=BLPropType.from_type(prop_type),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initialize Field on BLClass
|
||||||
|
self.bl_prop.init_bl_type(
|
||||||
|
owner,
|
||||||
|
enum_depends_on=self.cb_depends_on,
|
||||||
|
strsearch_depends_on=self.cb_depends_on,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Dynamic Enum: Initialize Persistent Enum Items
|
||||||
|
if self.prop_info['enum_dynamic']:
|
||||||
|
self.bl_prop_enum_items = BLProp(
|
||||||
|
name=self.bl_prop.enum_cache_key,
|
||||||
|
prop_info={'default': [], 'use_prop_update': False},
|
||||||
|
prop_type=list[ct.BLEnumElement],
|
||||||
|
bl_prop_type=BLPropType.Serialized,
|
||||||
|
)
|
||||||
|
self.bl_prop_enum_items.init_bl_type(owner)
|
||||||
|
|
||||||
|
# Searched Str: Initialize Persistent Str List
|
||||||
|
if self.prop_info['str_search']:
|
||||||
|
self.bl_prop_str_search = BLProp(
|
||||||
|
name=self.bl_prop.str_cache_key,
|
||||||
|
prop_info={'default': [], 'use_prop_update': False},
|
||||||
|
prop_type=list[str],
|
||||||
|
bl_prop_type=BLPropType.Serialized,
|
||||||
|
)
|
||||||
|
self.bl_prop_str_search.init_bl_type(owner)
|
||||||
|
|
||||||
|
def __get__(
|
||||||
|
self,
|
||||||
|
bl_instance: bl_instance.BLInstance | None,
|
||||||
|
owner: type[bl_instance.BLInstance],
|
||||||
|
) -> typ.Any:
|
||||||
|
"""Retrieves the value described by the BLField.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
Run by Python when the attribute described by the descriptor is accessed.
|
||||||
|
For more, search for "Python descriptor protocol".
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
bl_instance: Instance that is accessing the attribute.
|
||||||
|
owner: The class that owns the instance.
|
||||||
|
"""
|
||||||
|
# Compute Value (if available)
|
||||||
|
cached_value = self.bl_prop.read_nonpersist(bl_instance)
|
||||||
|
if cached_value is Signal.CacheNotReady or cached_value is Signal.CacheEmpty:
|
||||||
|
if bl_instance is not None:
|
||||||
|
persisted_value = self.bl_prop.read(bl_instance)
|
||||||
|
self.bl_prop.write_nonpersist(bl_instance, persisted_value)
|
||||||
|
return persisted_value
|
||||||
|
return self.bl_prop.default_value ## TODO: Good idea?
|
||||||
|
return cached_value
|
||||||
|
|
||||||
|
def suppress_next_update(self, bl_instance) -> None:
|
||||||
|
self.suppress_update[bl_instance.instance_id] = True
|
||||||
|
## TODO: Make it a context manager to prevent the worst of surprises
|
||||||
|
|
||||||
|
def __set__(
|
||||||
|
self, bl_instance: bl_instance.BLInstance | None, value: typ.Any
|
||||||
|
) -> None:
|
||||||
|
"""Sets the value described by the BLField.
|
||||||
|
|
||||||
|
In general, any BLField modified in the UI will set `InvalidateCache` on this descriptor.
|
||||||
|
If `self.prop_info['use_prop_update']` is set, the method `bl_instance.on_prop_changed(self.bl_prop.name)` will then be called and start a `FlowKind.DataChanged` event chain.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
Run by Python when the attribute described by the descriptor is set.
|
||||||
|
For more, search for "Python descriptor protocol".
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
bl_instance: Instance that is accessing the attribute.
|
||||||
|
owner: The class that owns the instance.
|
||||||
|
"""
|
||||||
|
# Perform Update Chain
|
||||||
|
## -> We still respect 'use_prop_update', since it is user-sourced.
|
||||||
|
if value is Signal.DoUpdate:
|
||||||
|
if self.prop_info['use_prop_update']:
|
||||||
|
bl_instance.on_prop_changed(self.bl_prop.name)
|
||||||
|
|
||||||
|
# Invalidate Cache
|
||||||
|
## -> This empties the non-persistent cache.
|
||||||
|
## -> As a result, the value must be reloaded from the property.
|
||||||
|
## The 'on_prop_changed' method on the bl_instance might also be called.
|
||||||
|
elif value is Signal.InvalidateCache or value is Signal.InvalidateCacheNoUpdate:
|
||||||
|
self.bl_prop.invalidate_nonpersist(bl_instance)
|
||||||
|
|
||||||
|
# Update Suppression
|
||||||
|
if self.suppress_update.get(bl_instance.instance_id):
|
||||||
|
self.suppress_update[bl_instance.instance_id] = False
|
||||||
|
|
||||||
|
# ELSE: Trigger Update Chain
|
||||||
|
elif self.prop_info['use_prop_update'] and value is Signal.InvalidateCache:
|
||||||
|
bl_instance.on_prop_changed(self.bl_prop.name)
|
||||||
|
|
||||||
|
# Reset Enum Items
|
||||||
|
elif value is Signal.ResetEnumItems:
|
||||||
|
# Retrieve Old Items
|
||||||
|
## -> This is verbatim what is being persisted, currently.
|
||||||
|
## -> len(0): Manually replaced w/fallback to guarantee >=len(1)
|
||||||
|
## -> Fallback element is 'NONE'.
|
||||||
|
_old_items: list[ct.BLEnumElement] = self.bl_prop_enum_items.read(
|
||||||
|
bl_instance
|
||||||
|
)
|
||||||
|
old_items = (
|
||||||
|
_old_items
|
||||||
|
if _old_items
|
||||||
|
else default_enum_items(self.bl_prop.is_enum_many)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Retrieve Current Items
|
||||||
|
## -> len(0): Manually replaced w/fallback to guarantee >=len(1)
|
||||||
|
## -> Manually replaced fallback element is 'NONE'.
|
||||||
|
_current_items: list[ct.BLEnumElement] = self.enum_cb(bl_instance, None)
|
||||||
|
current_items = (
|
||||||
|
_current_items
|
||||||
|
if _current_items
|
||||||
|
else default_enum_items(self.bl_prop.is_enum_many)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Compare Old | Current
|
||||||
|
## -> We don't involve non-persistent caches (they lie!)
|
||||||
|
## -> Since we persist the user callback directly, we can compare.
|
||||||
|
if old_items != current_items:
|
||||||
|
# Retrieve Old Enum Item
|
||||||
|
## -> This is verbatim what is being used.
|
||||||
|
## -> De-Coerce None -> 'NONE' to avoid special-cased search.
|
||||||
|
_old_item = self.bl_prop.read(bl_instance)
|
||||||
|
old_item = 'NONE' if _old_item is None else _old_item
|
||||||
|
|
||||||
|
# Swap Enum Items
|
||||||
|
## -> This is the hot stuff - the enum elements are overwritten.
|
||||||
|
## -> The safe_enum_cb will pick up on this immediately.
|
||||||
|
self.suppress_next_update(bl_instance)
|
||||||
|
self.bl_prop_enum_items.write(bl_instance, current_items)
|
||||||
|
|
||||||
|
# Old Item in Current Items
|
||||||
|
## -> It's possible that the old enum key is in the new enum.
|
||||||
|
## -> If so, the user will expect it to "remain".
|
||||||
|
## -> Thus, we set it - Blender sees a change, user doesn't.
|
||||||
|
## -> DO NOT trigger on_prop_changed (since "nothing changed").
|
||||||
|
if any(old_item == item[0] for item in current_items):
|
||||||
|
self.suppress_next_update(bl_instance)
|
||||||
|
self.bl_prop.write(bl_instance, old_item)
|
||||||
|
## -> TODO: Don't write if not needed.
|
||||||
|
|
||||||
|
# Old Item Not in Current Items
|
||||||
|
## -> In this case, fallback to the first current item.
|
||||||
|
## -> DO trigger on_prop_changed (since it changed!)
|
||||||
|
else:
|
||||||
|
_first_current_item = current_items[0][0]
|
||||||
|
first_current_item = (
|
||||||
|
_first_current_item if _first_current_item != 'NONE' else None
|
||||||
|
)
|
||||||
|
|
||||||
|
self.suppress_next_update(bl_instance)
|
||||||
|
self.bl_prop.write(bl_instance, first_current_item)
|
||||||
|
|
||||||
|
if self.prop_info['use_prop_update']:
|
||||||
|
bl_instance.on_prop_changed(self.bl_prop.name)
|
||||||
|
|
||||||
|
# Reset Str Search
|
||||||
|
elif value is Signal.ResetStrSearch:
|
||||||
|
self.bl_prop_str_search.invalidate_nonpersist(bl_instance)
|
||||||
|
|
||||||
|
# General __set__
|
||||||
|
else:
|
||||||
|
self.bl_prop.write(bl_instance, value)
|
||||||
|
|
||||||
|
# Update Semantics
|
||||||
|
if self.suppress_update.get(bl_instance.instance_id):
|
||||||
|
self.suppress_update[bl_instance.instance_id] = False
|
||||||
|
|
||||||
|
elif self.prop_info['use_prop_update']:
|
||||||
|
bl_instance.on_prop_changed(self.bl_prop.name)
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Safe Callbacks
|
||||||
|
####################
|
||||||
|
def safe_str_cb(
|
||||||
|
self, _self: bl_instance.BLInstance, context: bpy.types.Context, edit_text: str
|
||||||
|
):
|
||||||
|
"""Wrapper around `StringProperty.search` which keeps a non-persistent cache around search results.
|
||||||
|
|
||||||
|
Reset by setting the descriptor to `Signal.ResetStrSearch`.
|
||||||
|
"""
|
||||||
|
cached_items = self.bl_prop_str_search.read_nonpersist(_self)
|
||||||
|
if cached_items is not Signal.CacheNotReady:
|
||||||
|
if cached_items is Signal.CacheEmpty:
|
||||||
|
computed_items = self.str_cb(_self, context, edit_text)
|
||||||
|
self.bl_prop_str_search.write_nonpersist(_self, computed_items)
|
||||||
|
return computed_items
|
||||||
|
return cached_items
|
||||||
|
return []
|
||||||
|
|
||||||
|
def safe_enum_cb(
|
||||||
|
self, _self: bl_instance.BLInstance, context: bpy.types.Context
|
||||||
|
) -> list[ct.BLEnumElement]:
|
||||||
|
"""Wrapper around `EnumProperty.items` callback, which **guarantees** that returned strings will not be GCed by keeping a persistent + non-persistent cache.
|
||||||
|
|
||||||
|
When a persistent cache exists, then the non-persistent cache will be filled at-will, since this is always guaranteed possible.
|
||||||
|
Otherwise, the persistent cache will only be regenerated when `Signal.ResetEnumItems` is run.
|
||||||
|
The original callback won't ever run other than then.
|
||||||
|
|
||||||
|
Until then, `DEFAULT_ENUM_ITEMS_MANY` or `DEFAULT_ENUM_ITEMS_SINGLE` will be used as defaults (guaranteed to not dereference so long as the module is loaded).
|
||||||
|
"""
|
||||||
|
# Compute Value (if available)
|
||||||
|
cached_items = self.bl_prop_enum_items.read_nonpersist(_self)
|
||||||
|
if cached_items is Signal.CacheNotReady or cached_items is Signal.CacheEmpty:
|
||||||
|
if _self is not None:
|
||||||
|
persisted_items = self.bl_prop_enum_items.read(_self)
|
||||||
|
if not persisted_items:
|
||||||
|
computed_items = self.enum_cb(_self, context)
|
||||||
|
_items = computed_items
|
||||||
|
else:
|
||||||
|
_items = persisted_items
|
||||||
|
else:
|
||||||
|
computed_items = self.enum_cb(_self, context)
|
||||||
|
_items = computed_items
|
||||||
|
|
||||||
|
# Fallback for Empty Persisted Items
|
||||||
|
## -> Use [('NONE', ...)]
|
||||||
|
## -> This guarantees that the enum items always has >=len(1)
|
||||||
|
items = _items if _items else default_enum_items(self.bl_prop.is_enum_many)
|
||||||
|
|
||||||
|
# Write Items -> Non-Persistent Cache
|
||||||
|
self.bl_prop_enum_items.write_nonpersist(_self, items)
|
||||||
|
return items
|
||||||
|
return cached_items
|
|
@ -0,0 +1,235 @@
|
||||||
|
# blender_maxwell
|
||||||
|
# Copyright (C) 2024 blender_maxwell Project Contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""Defines `BLProp`, a high-level wrapper for interacting with Blender properties."""
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
|
import functools
|
||||||
|
import typing as typ
|
||||||
|
|
||||||
|
from blender_maxwell.utils import bl_instance, logger
|
||||||
|
|
||||||
|
from . import managed_cache
|
||||||
|
from .bl_prop_type import BLPropInfo, BLPropType
|
||||||
|
from .signal import Signal
|
||||||
|
|
||||||
|
log = logger.get(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Blender Property (Abstraction)
|
||||||
|
####################
|
||||||
|
@dataclasses.dataclass(kw_only=True, frozen=True)
|
||||||
|
class BLProp:
|
||||||
|
"""A high-level wrapper encapsulating access to a Blender property.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
name: The name of the Blender property, as one uses it.
|
||||||
|
prop_info: Specifies the property's particular behavior, including subtype and UI.
|
||||||
|
prop_type: The type to associate with the property.
|
||||||
|
Especially relevant for structured deserialization.
|
||||||
|
bl_prop_type: Identifier encapsulating which Blender property used for data storage, and how.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
prop_info: BLPropInfo ## TODO: Validate / Typing
|
||||||
|
prop_type: type
|
||||||
|
bl_prop_type: BLPropType
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Computed
|
||||||
|
####################
|
||||||
|
@functools.cached_property
|
||||||
|
def bl_name(self):
|
||||||
|
"""Deduces the actual attribute name at which the Blender property will be available."""
|
||||||
|
return f'blfield__{self.name}'
|
||||||
|
|
||||||
|
@functools.cached_property
|
||||||
|
def enum_cache_key(self):
|
||||||
|
"""Deduces an attribute name for use by the persistent cache component of `EnumProperty.items`.
|
||||||
|
|
||||||
|
For dynamic enums, a persistent cache is not enough - a non-persistent cache must also be used to guarantee that returned strings will not dereference.
|
||||||
|
**Letting dynamic enum strings dereference causes Blender to crash**.
|
||||||
|
|
||||||
|
Use of a non-persistent cache alone introduces a massive startup burden, as _all_ of the potentially expensive `EnumProperty.items` methods must re-run.
|
||||||
|
Should any depend on ex. internet connectivity, which is no longer available, elaborate failure modes may trigger.
|
||||||
|
|
||||||
|
By using this key, we can persist `items` for re-caching on startup, to reap the benefits of both schemes and make dynamic `EnumProperty` usable in practice.
|
||||||
|
"""
|
||||||
|
return self.name + '__enum_cache'
|
||||||
|
|
||||||
|
@functools.cached_property
|
||||||
|
def str_cache_key(self):
|
||||||
|
"""Deduce an internal name for string-search names distinct from the property name.
|
||||||
|
|
||||||
|
Compared to dynamic enums, string-search names are very gentle.
|
||||||
|
However, the mechanism is otherwise almost same, so similar logic makes a lot of sense.
|
||||||
|
"""
|
||||||
|
return self.name + '__str_cache'
|
||||||
|
|
||||||
|
@functools.cached_property
|
||||||
|
def display_name(self):
|
||||||
|
"""Deduce a display name for the Blender property, assigned to the `name=` attribute."""
|
||||||
|
return (
|
||||||
|
'[JSON] ' if self.bl_prop_type == BLPropType.Serialized else ''
|
||||||
|
) + f'BLField: {self.name}'
|
||||||
|
|
||||||
|
@functools.cached_property
|
||||||
|
def is_enum_many(self):
|
||||||
|
return self.bl_prop_type in [BLPropType.SetEnum, BLPropType.SetDynEnum]
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Low-Level Methods
|
||||||
|
####################
|
||||||
|
def encode(self, value: typ.Any):
|
||||||
|
"""Encode a value for compatibility with this Blender property, using the encapsulated types.
|
||||||
|
|
||||||
|
A convenience method for `BLPropType.encode()`.
|
||||||
|
"""
|
||||||
|
return self.bl_prop_type.encode(value)
|
||||||
|
|
||||||
|
@functools.cached_property
|
||||||
|
def default_value(self) -> typ.Any:
|
||||||
|
return self.prop_info.get('default')
|
||||||
|
|
||||||
|
def decode(self, value: typ.Any):
|
||||||
|
"""Encode a value for compatibility with this Blender property, using the encapsulated types.
|
||||||
|
|
||||||
|
A convenience method for `BLPropType.decode()`.
|
||||||
|
"""
|
||||||
|
return self.bl_prop_type.decode(value, self.prop_type)
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Initialization
|
||||||
|
####################
|
||||||
|
def init_bl_type(
|
||||||
|
self,
|
||||||
|
bl_type: type[bl_instance.BLInstance],
|
||||||
|
depends_on: frozenset[str] = frozenset(),
|
||||||
|
enum_depends_on: frozenset[str] | None = None,
|
||||||
|
strsearch_depends_on: frozenset[str] | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Declare the Blender property on a Blender class, ensuring that the property will be available to all `bl_instance.BLInstance` respecting instances of that class.
|
||||||
|
|
||||||
|
- **Declare BLField**: Runs `bl_type.declare_blfield()` to ensure that `on_prop_changed` will invalidate the cache for this property.
|
||||||
|
- **Set Property**: Runs `bl_type.set_prop()` to ensure that the Blender property will be available on instances of `bl_type`.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
obj_type: The exact object type that will be stored in the Blender property.
|
||||||
|
**Must** be chosen such that `BLPropType.from_type(obj_type) == self`.
|
||||||
|
"""
|
||||||
|
# Parse KWArgs for Blender Property
|
||||||
|
kwargs_prop = self.bl_prop_type.parse_kwargs(
|
||||||
|
self.prop_type,
|
||||||
|
self.prop_info,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set Blender Property
|
||||||
|
bl_type.declare_blfield(
|
||||||
|
self.name,
|
||||||
|
self.bl_name,
|
||||||
|
use_dynamic_enum=self.prop_info.get('enum_dynamic', False),
|
||||||
|
use_str_search=self.prop_info.get('str_search', False),
|
||||||
|
)
|
||||||
|
bl_type.set_prop(
|
||||||
|
self.bl_name,
|
||||||
|
self.bl_prop_type.bl_prop,
|
||||||
|
# Property Options
|
||||||
|
name=self.display_name,
|
||||||
|
**kwargs_prop,
|
||||||
|
) ## TODO: Parse __doc__ for property descs
|
||||||
|
|
||||||
|
for src_prop_name in depends_on:
|
||||||
|
bl_type.declare_blfield_dep(src_prop_name, self.name)
|
||||||
|
|
||||||
|
if self.prop_info.get('enum_dynamic', False) and enum_depends_on is not None:
|
||||||
|
for src_prop_name in enum_depends_on:
|
||||||
|
bl_type.declare_blfield_dep(
|
||||||
|
src_prop_name, self.name, method='reset_enum'
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.prop_info.get('str_search', False) and strsearch_depends_on is not None:
|
||||||
|
for src_prop_name in strsearch_depends_on:
|
||||||
|
bl_type.declare_blfield_dep(
|
||||||
|
src_prop_name, self.name, method='reset_strsearch'
|
||||||
|
)
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Instance Methods
|
||||||
|
####################
|
||||||
|
def read_nonpersist(self, bl_instance: bl_instance.BLInstance | None) -> typ.Any:
|
||||||
|
"""Read the non-persistent cache value for this property.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Generally, the cache value, with two exceptions.
|
||||||
|
|
||||||
|
- `Signal.CacheNotReady`: When either `bl_instance` is None, or it doesn't yet have a unique `bl_instance.instance_id`.
|
||||||
|
Indicates that the instance is not yet ready for use.
|
||||||
|
For nodes, `init()` has not yet run.
|
||||||
|
For sockets, `preinit()` has not yet run.
|
||||||
|
|
||||||
|
- `Signal.CacheEmpty`: When the cache has no entry.
|
||||||
|
A good idea might be to fill it immediately with `self.write_nonpersist(bl_instance)`.
|
||||||
|
"""
|
||||||
|
return managed_cache.read(
|
||||||
|
bl_instance,
|
||||||
|
self.bl_name,
|
||||||
|
use_nonpersist=True,
|
||||||
|
use_persist=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def read(self, bl_instance: bl_instance.BLInstance) -> typ.Any:
|
||||||
|
"""Read the Blender property's particular value on the given `bl_instance`."""
|
||||||
|
persisted_value = self.decode(
|
||||||
|
managed_cache.read(
|
||||||
|
bl_instance,
|
||||||
|
self.bl_name,
|
||||||
|
use_nonpersist=False,
|
||||||
|
use_persist=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if persisted_value is not Signal.CacheEmpty:
|
||||||
|
return persisted_value
|
||||||
|
|
||||||
|
msg = f"{self.name}: Can't read BLProp from instance {bl_instance}"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
def write(self, bl_instance: bl_instance.BLInstance, value: typ.Any) -> None:
|
||||||
|
managed_cache.write(
|
||||||
|
bl_instance,
|
||||||
|
self.bl_name,
|
||||||
|
self.encode(value),
|
||||||
|
use_nonpersist=False,
|
||||||
|
use_persist=True,
|
||||||
|
)
|
||||||
|
self.write_nonpersist(bl_instance, value)
|
||||||
|
|
||||||
|
def write_nonpersist(
|
||||||
|
self, bl_instance: bl_instance.BLInstance, value: typ.Any
|
||||||
|
) -> None:
|
||||||
|
managed_cache.write(
|
||||||
|
bl_instance,
|
||||||
|
self.bl_name,
|
||||||
|
value,
|
||||||
|
use_nonpersist=True,
|
||||||
|
use_persist=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def invalidate_nonpersist(self, bl_instance: bl_instance.BLInstance | None) -> None:
|
||||||
|
managed_cache.invalidate_nonpersist(
|
||||||
|
bl_instance,
|
||||||
|
self.bl_name,
|
||||||
|
)
|
|
@ -0,0 +1,755 @@
|
||||||
|
# blender_maxwell
|
||||||
|
# Copyright (C) 2024 blender_maxwell Project Contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""Defines `BLPropType`, which provides stronger lower-level interfaces for interacting with data that can be conformed to work with Blender properties."""
|
||||||
|
|
||||||
|
import builtins
|
||||||
|
import enum
|
||||||
|
import functools
|
||||||
|
import inspect
|
||||||
|
import pathlib
|
||||||
|
import typing as typ
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from blender_maxwell import contracts as ct
|
||||||
|
from blender_maxwell.utils import logger, serialize
|
||||||
|
from blender_maxwell.utils.staticproperty import staticproperty
|
||||||
|
|
||||||
|
from .signal import Signal
|
||||||
|
|
||||||
|
log = logger.get(__name__)
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Types
|
||||||
|
####################
|
||||||
|
BLIDStructs = typ.get_args(ct.BLIDStruct)
|
||||||
|
Shape: typ.TypeAlias = None | tuple[int, ...]
|
||||||
|
BLPropInfo: typ.TypeAlias = dict[str, typ.Any]
|
||||||
|
|
||||||
|
|
||||||
|
@functools.cache
|
||||||
|
def _parse_vector_size(obj_type: type[tuple[int, ...]]) -> int:
|
||||||
|
"""Parse the size of an arbitrarily sized generic tuple type, which is representing a vector.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
obj_type: The type of a flat, generic tuple integer, representing a static vector shape.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The length of any object that has the given type.
|
||||||
|
"""
|
||||||
|
return len(typ.get_args(obj_type))
|
||||||
|
|
||||||
|
|
||||||
|
@functools.cache
|
||||||
|
def _parse_matrix_size(obj_type: type[tuple[int, ...], ...]) -> tuple[int, int]:
|
||||||
|
"""Parse the rows and columns of an arbitrarily sized generic tuple-of-tuple type, which is representing a row-major matrix.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
obj_type: The type of a singly-nested, generic tuple integer, representing a static matrix shape.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The rows and columns of any object that has the given type.
|
||||||
|
"""
|
||||||
|
rows = len(typ.get_args(obj_type))
|
||||||
|
cols = len(typ.get_args(typ.get_args(obj_type)[0]))
|
||||||
|
|
||||||
|
for i, col_generic in enumerate(typ.get_args(obj_type)):
|
||||||
|
col_els = typ.get_args(col_generic)
|
||||||
|
if len(col_els) != cols:
|
||||||
|
msg = f'Value {obj_type} has mismatching column length {i} (to column 0)'
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
return (rows, cols)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_strenum(T: type) -> bool: # noqa: N803
|
||||||
|
return inspect.isclass(T) and issubclass(T, enum.StrEnum)
|
||||||
|
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Blender Property Type
|
||||||
|
####################
|
||||||
|
class BLPropType(enum.StrEnum):
|
||||||
|
"""A type identifier which can be directly associated with a Blender property.
|
||||||
|
|
||||||
|
For general use, the higher-level interface `BLProp` is more appropriate.
|
||||||
|
|
||||||
|
This is a low-level interface to Blender properties, allowing for directly identifying and utilizing a subset of types that are trivially representable using Blender's property system.
|
||||||
|
This hard-coded approach is especially required when managing the nuances of UI methods.
|
||||||
|
|
||||||
|
`BLPropType` should generally be treated as a "dumb" enum identifying the low-level representation of an object in a Blender property.
|
||||||
|
Use of `BLPropType.from_type` is encouraged; use of other methods is generally discouraged outside of higher-level encapsulating interfaces.
|
||||||
|
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
Bool: A boolean.
|
||||||
|
Int: An integer.
|
||||||
|
Float: A floating point number.
|
||||||
|
BoolVector: Between 2 and 32 booleans.
|
||||||
|
IntVector: Between 2 and 32 integers.
|
||||||
|
FloatVector: Between 2 and 32 floats.
|
||||||
|
BoolVector: 2D booleans of 2 - 32 elements per axis.
|
||||||
|
IntVector: 2D integers of 2 - 32 elements per axis.
|
||||||
|
FloatVector: 2D floats of 2 - 32 elements per axis.
|
||||||
|
Str: A simple string.
|
||||||
|
Path: A particular filesystem path.
|
||||||
|
SingleEnum: A single string value from a statically known `StrEnum`.
|
||||||
|
SetEnum: A set of string values, each from a statically known `StrEnum`.
|
||||||
|
SingleDynEnum: A single string value from a dynamically computed set of string values.
|
||||||
|
SetDynEnum: A set of string value, each from a dynamically computed set of string values.
|
||||||
|
BLPointer: A reference to a Blender object.
|
||||||
|
Blender manages correctly reconstructing this reference on startup, and the underlying pointer value is not stable.
|
||||||
|
Serialized: An arbitrary, serialized representation of an object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Scalar
|
||||||
|
Bool = enum.auto()
|
||||||
|
Int = enum.auto()
|
||||||
|
Float = enum.auto()
|
||||||
|
## TODO: Support complex
|
||||||
|
|
||||||
|
# Vector
|
||||||
|
BoolVector = enum.auto()
|
||||||
|
IntVector = enum.auto()
|
||||||
|
FloatVector = enum.auto()
|
||||||
|
|
||||||
|
# Matrix
|
||||||
|
BoolMatrix = enum.auto()
|
||||||
|
IntMatrix = enum.auto()
|
||||||
|
FloatMatrix = enum.auto()
|
||||||
|
|
||||||
|
## TODO: Support jaxtyping JAX arrays (as serialized) directly?
|
||||||
|
|
||||||
|
# String
|
||||||
|
Str = enum.auto()
|
||||||
|
Path = enum.auto()
|
||||||
|
## TODO: OS checks for Path
|
||||||
|
|
||||||
|
# Enums
|
||||||
|
SingleEnum = enum.auto()
|
||||||
|
SetEnum = enum.auto()
|
||||||
|
|
||||||
|
SingleDynEnum = enum.auto()
|
||||||
|
SetDynEnum = enum.auto()
|
||||||
|
|
||||||
|
# Special
|
||||||
|
BLPointer = enum.auto()
|
||||||
|
Serialized = enum.auto()
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Static
|
||||||
|
####################
|
||||||
|
@staticproperty
|
||||||
|
def vector_types() -> frozenset[typ.Self]:
|
||||||
|
"""The set of `BLPropType`s that are considered "vectors"."""
|
||||||
|
BPT = BLPropType
|
||||||
|
return frozenset({BPT.BoolVector, BPT.IntVector, BPT.FloatVector})
|
||||||
|
|
||||||
|
@staticproperty
|
||||||
|
def matrix_types() -> frozenset[typ.Self]:
|
||||||
|
"""The set of `BLPropType`s that are considered "matrices"."""
|
||||||
|
BPT = BLPropType
|
||||||
|
return frozenset({BPT.BoolMatrix, BPT.IntMatrix, BPT.FloatMatrix})
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Computed
|
||||||
|
####################
|
||||||
|
@functools.cached_property
|
||||||
|
def is_vector(self) -> bool:
|
||||||
|
"""Checks whether this `BLPropType` is considered a vector.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A boolean indicating "vectorness".
|
||||||
|
"""
|
||||||
|
return self in BLPropType.vector_types
|
||||||
|
|
||||||
|
@functools.cached_property
|
||||||
|
def is_matrix(self) -> bool:
|
||||||
|
"""Checks whether this `BLPropType` is considered a matrix.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A boolean indicating "matrixness".
|
||||||
|
"""
|
||||||
|
return self in BLPropType.matrix_types
|
||||||
|
|
||||||
|
@functools.cached_property
|
||||||
|
def bl_prop(self) -> bpy.types.Property:
|
||||||
|
"""Deduce which `bpy.props.*` type should implement this `BLPropType` in practice.
|
||||||
|
|
||||||
|
In practice, `self.parse_kwargs()` collects arguments usable by the type returned by this property.
|
||||||
|
Thus, this property provides the key bridge between `BLPropType` and vanilla Blender properties.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A Blender property type, for use as a constructor.
|
||||||
|
"""
|
||||||
|
BPT = BLPropType
|
||||||
|
return {
|
||||||
|
# Scalar
|
||||||
|
BPT.Bool: bpy.props.BoolProperty,
|
||||||
|
BPT.Int: bpy.props.IntProperty,
|
||||||
|
BPT.Float: bpy.props.FloatProperty,
|
||||||
|
# Vector
|
||||||
|
BPT.BoolVector: bpy.props.BoolVectorProperty,
|
||||||
|
BPT.IntVector: bpy.props.IntVectorProperty,
|
||||||
|
BPT.FloatVector: bpy.props.FloatVectorProperty,
|
||||||
|
# Matrix
|
||||||
|
BPT.BoolMatrix: bpy.props.BoolVectorProperty,
|
||||||
|
BPT.IntMatrix: bpy.props.IntVectorProperty,
|
||||||
|
BPT.FloatMatrix: bpy.props.FloatVectorProperty,
|
||||||
|
# String
|
||||||
|
BPT.Str: bpy.props.StringProperty,
|
||||||
|
BPT.Path: bpy.props.StringProperty,
|
||||||
|
# Enum
|
||||||
|
BPT.SingleEnum: bpy.props.EnumProperty,
|
||||||
|
BPT.SetEnum: bpy.props.EnumProperty,
|
||||||
|
BPT.SingleDynEnum: bpy.props.EnumProperty,
|
||||||
|
BPT.SetDynEnum: bpy.props.EnumProperty,
|
||||||
|
# Special
|
||||||
|
BPT.BLPointer: bpy.props.PointerProperty,
|
||||||
|
BPT.Serialized: bpy.props.StringProperty,
|
||||||
|
}[self]
|
||||||
|
|
||||||
|
@functools.cached_property
|
||||||
|
def primitive_type(self) -> type:
|
||||||
|
"""The "primitive" type representable using this property.
|
||||||
|
|
||||||
|
Generally, "primitive" types are Python standard library types.
|
||||||
|
However, exceptions may exist for a ubiquitously used type.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A type guaranteed to be representable as a Blender property via. `self.encode()`.
|
||||||
|
|
||||||
|
Note that any relevant constraints on the type are not taken into account in this type.
|
||||||
|
For example, `SingleEnum` has `str`, even though all strings are not valid.
|
||||||
|
Similarly for ex. non-negative integers simply returning `int`.
|
||||||
|
"""
|
||||||
|
BPT = BLPropType
|
||||||
|
return {
|
||||||
|
# Scalar
|
||||||
|
BPT.Bool: bool,
|
||||||
|
BPT.Int: int,
|
||||||
|
BPT.Float: float,
|
||||||
|
# Vector
|
||||||
|
BPT.BoolVector: bool,
|
||||||
|
BPT.IntVector: int,
|
||||||
|
BPT.FloatVector: float,
|
||||||
|
# Matrix
|
||||||
|
BPT.BoolMatrix: bool,
|
||||||
|
BPT.IntMatrix: int,
|
||||||
|
BPT.FloatMatrix: float,
|
||||||
|
# String
|
||||||
|
BPT.Str: str,
|
||||||
|
BPT.Path: Path,
|
||||||
|
# Enum
|
||||||
|
BPT.SingleEnum: str,
|
||||||
|
BPT.SetEnum: set[str],
|
||||||
|
BPT.SingleDynEnum: str,
|
||||||
|
BPT.SetDynEnum: set[str],
|
||||||
|
# Special
|
||||||
|
BPT.BLPointer: None,
|
||||||
|
BPT.Serialized: str,
|
||||||
|
}[self]
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Parser Methods
|
||||||
|
####################
|
||||||
|
def parse_size(self, obj_type: type) -> Shape:
|
||||||
|
"""Retrieve the shape / shape of data associated with this `BLPropType`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Vectors have `(size,)`.
|
||||||
|
Matrices have `(rows, cols)`.
|
||||||
|
|
||||||
|
Otherwise, `None` indicates a single value/scalar.
|
||||||
|
"""
|
||||||
|
BPT = BLPropType
|
||||||
|
|
||||||
|
match self:
|
||||||
|
case BPT.BoolVector | BPT.IntVector | BPT.FloatVector:
|
||||||
|
return _parse_vector_size(obj_type)
|
||||||
|
case BPT.BoolMatrix | BPT.IntMatrix | BPT.FloatMatrix:
|
||||||
|
return _parse_matrix_size(obj_type)
|
||||||
|
case _:
|
||||||
|
return None
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - KWArg Parsers
|
||||||
|
####################
|
||||||
|
@functools.cached_property
|
||||||
|
def required_info(self) -> list[str]:
|
||||||
|
"""Retrieve a list of required keyword arguments to the constructor returned by `self.bl_prop`.
|
||||||
|
|
||||||
|
Mainly useful via `self.check_info()`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of required keys for the Blender property constructor.
|
||||||
|
"""
|
||||||
|
BPT = BLPropType
|
||||||
|
return {
|
||||||
|
# Scalar
|
||||||
|
BPT.Bool: ['default'],
|
||||||
|
BPT.Int: ['default'],
|
||||||
|
BPT.Float: ['default'],
|
||||||
|
# Vector
|
||||||
|
BPT.BoolVector: ['default'],
|
||||||
|
BPT.IntVector: ['default'],
|
||||||
|
BPT.FloatVector: ['default'],
|
||||||
|
# Matrix
|
||||||
|
BPT.BoolMatrix: ['default'],
|
||||||
|
BPT.IntMatrix: ['default'],
|
||||||
|
BPT.FloatMatrix: ['default'],
|
||||||
|
# String
|
||||||
|
BPT.Str: ['default', 'str_search'],
|
||||||
|
BPT.Path: ['default', 'path_type'],
|
||||||
|
# Enum
|
||||||
|
BPT.SingleEnum: ['default'],
|
||||||
|
BPT.SetEnum: ['default'],
|
||||||
|
BPT.SingleDynEnum: ['enum_dynamic'],
|
||||||
|
BPT.SetDynEnum: ['enum_dynamic'],
|
||||||
|
# Special
|
||||||
|
BPT.BLPointer: ['blptr_type'],
|
||||||
|
BPT.Serialized: [],
|
||||||
|
}[self]
|
||||||
|
|
||||||
|
def check_info(self, prop_info: BLPropInfo) -> bool:
|
||||||
|
"""Validate that a dictionary contains all required entries needed when creating a Blender property.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the provided dictionary is guaranteed to result in a valid Blender property when used as keyword arguments in the `self.bl_prop` constructor.
|
||||||
|
"""
|
||||||
|
return all(
|
||||||
|
required_info_key in prop_info for required_info_key in self.required_info
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_kwargs( # noqa: PLR0915, PLR0912, C901
|
||||||
|
self,
|
||||||
|
obj_type: type,
|
||||||
|
prop_info: BLPropInfo,
|
||||||
|
) -> BLPropInfo:
|
||||||
|
"""Parse the kwargs dictionary used to construct the Blender property.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
obj_type: The exact object type that will be stored in the Blender property.
|
||||||
|
**Generally** should be chosen such that `BLPropType.from_type(obj_type) == self`.
|
||||||
|
prop_info: The property info.
|
||||||
|
**Must** contain keys such that `required_info`
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Keyword arguments, which can be passed directly as to `self.bl_type` to construct a Blender property according to the `prop_info`.
|
||||||
|
|
||||||
|
In total, creating a Blender property can be done simply using `self.bl_type(**parse_kwargs(...))`.
|
||||||
|
"""
|
||||||
|
BPT = BLPropType
|
||||||
|
|
||||||
|
# Check Availability of Required Information
|
||||||
|
## -> All required fields must be defined.
|
||||||
|
if not self.check_info(prop_info):
|
||||||
|
msg = f'{self} ({obj_type}): Required property attribute is missing from prop_info="{prop_info}"'
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
# Define Information -> KWArg Getter
|
||||||
|
def g_kwarg(name: str, force_key: str | None = None):
|
||||||
|
key = force_key if force_key is not None else name
|
||||||
|
return {key: prop_info[name]} if prop_info.get(name) is not None else {}
|
||||||
|
|
||||||
|
# Encode Default Value
|
||||||
|
if prop_info.get('default', Signal.CacheEmpty) is not Signal.CacheEmpty:
|
||||||
|
encoded_default = {'default': self.encode(prop_info.get('default'))}
|
||||||
|
else:
|
||||||
|
encoded_default = {}
|
||||||
|
|
||||||
|
# Assemble KWArgs
|
||||||
|
kwargs = {}
|
||||||
|
match self:
|
||||||
|
case BPT.Bool if obj_type is bool:
|
||||||
|
kwargs |= encoded_default
|
||||||
|
|
||||||
|
case BPT.Int | BPT.IntVector | BPT.IntMatrix:
|
||||||
|
kwargs |= encoded_default
|
||||||
|
kwargs |= g_kwarg('abs_min')
|
||||||
|
kwargs |= g_kwarg('abs_max')
|
||||||
|
kwargs |= g_kwarg('soft_min')
|
||||||
|
kwargs |= g_kwarg('soft_max')
|
||||||
|
|
||||||
|
case BPT.Float | BPT.FloatVector | BPT.FloatMatrix:
|
||||||
|
kwargs |= encoded_default
|
||||||
|
kwargs |= g_kwarg('abs_min')
|
||||||
|
kwargs |= g_kwarg('abs_max')
|
||||||
|
kwargs |= g_kwarg('soft_min')
|
||||||
|
kwargs |= g_kwarg('soft_max')
|
||||||
|
kwargs |= g_kwarg('step')
|
||||||
|
kwargs |= g_kwarg('precision')
|
||||||
|
|
||||||
|
case BPT.Str if obj_type is str:
|
||||||
|
kwargs |= encoded_default
|
||||||
|
|
||||||
|
# Str: Secret
|
||||||
|
if prop_info.get('str_secret'):
|
||||||
|
kwargs |= {'subtype': 'PASSWORD', 'options': {'SKIP_SAVE'}}
|
||||||
|
|
||||||
|
# Str: Search
|
||||||
|
if prop_info.get('str_search'):
|
||||||
|
kwargs |= g_kwarg('safe_str_cb', force_key='search')
|
||||||
|
|
||||||
|
case BPT.Path if obj_type is Path:
|
||||||
|
kwargs |= encoded_default
|
||||||
|
|
||||||
|
# Path: File/Dir
|
||||||
|
if prop_info.get('path_type'):
|
||||||
|
kwargs |= {
|
||||||
|
'subtype': (
|
||||||
|
'FILE_PATH'
|
||||||
|
if prop_info['path_type'] == 'file'
|
||||||
|
else 'DIR_PATH'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Explicit Enums
|
||||||
|
case BPT.SingleEnum:
|
||||||
|
SubStrEnum = obj_type
|
||||||
|
|
||||||
|
# Static | Dynamic Enum
|
||||||
|
## -> Dynamic enums are responsible for respecting type.
|
||||||
|
if prop_info.get('enum_dynamic'):
|
||||||
|
kwargs |= g_kwarg('safe_enum_cb', force_key='items')
|
||||||
|
else:
|
||||||
|
kwargs |= encoded_default
|
||||||
|
kwargs |= {
|
||||||
|
'items': [
|
||||||
|
## TODO: Parse __doc__ for item descs
|
||||||
|
(
|
||||||
|
str(value),
|
||||||
|
SubStrEnum.to_name(value),
|
||||||
|
SubStrEnum.to_name(value),
|
||||||
|
SubStrEnum.to_icon(value),
|
||||||
|
i,
|
||||||
|
)
|
||||||
|
for i, value in enumerate(list(obj_type))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
case BPT.SetEnum:
|
||||||
|
SubStrEnum = typ.get_args(obj_type)[0]
|
||||||
|
|
||||||
|
# Enum Set: Use ENUM_FLAG option.
|
||||||
|
kwargs |= {'options': {'ENUM_FLAG'}}
|
||||||
|
|
||||||
|
# Static | Dynamic Enum
|
||||||
|
## -> Dynamic enums are responsible for respecting type.
|
||||||
|
if prop_info.get('enum_dynamic'):
|
||||||
|
kwargs |= g_kwarg('safe_enum_cb', force_key='items')
|
||||||
|
else:
|
||||||
|
kwargs |= encoded_default
|
||||||
|
kwargs |= {
|
||||||
|
'items': [
|
||||||
|
## TODO: Parse __doc__ for item descs
|
||||||
|
(
|
||||||
|
str(value),
|
||||||
|
SubStrEnum.to_name(value),
|
||||||
|
SubStrEnum.to_name(value),
|
||||||
|
SubStrEnum.to_icon(value),
|
||||||
|
2**i,
|
||||||
|
)
|
||||||
|
for i, value in enumerate(list(SubStrEnum))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Anonymous Enums
|
||||||
|
case BPT.SingleDynEnum:
|
||||||
|
kwargs |= g_kwarg('safe_enum_cb', force_key='items')
|
||||||
|
|
||||||
|
case BPT.SetDynEnum:
|
||||||
|
kwargs |= g_kwarg('safe_enum_cb', force_key='items')
|
||||||
|
|
||||||
|
# Enum Set: Use ENUM_FLAG option.
|
||||||
|
kwargs |= {'options': {'ENUM_FLAG'}}
|
||||||
|
|
||||||
|
# BLPointer
|
||||||
|
case BPT.BLPointer:
|
||||||
|
kwargs |= encoded_default
|
||||||
|
|
||||||
|
# BLPointer: ID Type
|
||||||
|
kwargs |= g_kwarg('blptr_type', force_key='type')
|
||||||
|
|
||||||
|
# BLPointer
|
||||||
|
case BPT.Serialized:
|
||||||
|
kwargs |= encoded_default
|
||||||
|
|
||||||
|
# Match Size
|
||||||
|
## -> Matrices have inverted order to mitigate the Matrix Display Bug.
|
||||||
|
size = self.parse_size(obj_type)
|
||||||
|
if size is not None:
|
||||||
|
if self in [BPT.BoolVector, BPT.IntVector, BPT.FloatVector]:
|
||||||
|
kwargs |= {'size': size}
|
||||||
|
if self in [BPT.BoolMatrix, BPT.IntMatrix, BPT.FloatMatrix]:
|
||||||
|
kwargs |= {'size': size[::-1]}
|
||||||
|
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Encode Value
|
||||||
|
####################
|
||||||
|
def encode(self, value: typ.Any) -> typ.Any: # noqa: PLR0911
|
||||||
|
"""Transform a value to a form that can be directly written to a Blender property.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
value: A value which should be transformed into a form that can be written to the Blender property returned by `self.bl_type`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A value that can be written directly to the Blender property returned by `self.bl_type`.
|
||||||
|
"""
|
||||||
|
BPT = BLPropType
|
||||||
|
match self:
|
||||||
|
# Scalars: Coerce Losslessly
|
||||||
|
## -> We choose to be very strict, except for float.is_integer() -> int
|
||||||
|
case BPT.Bool if isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
case BPT.Int if isinstance(value, int):
|
||||||
|
return value
|
||||||
|
case BPT.Int if isinstance(value, float) and value.is_integer():
|
||||||
|
return int(value)
|
||||||
|
case BPT.Float if isinstance(value, int | float):
|
||||||
|
return float(value)
|
||||||
|
|
||||||
|
# Vectors | Matrices: list()
|
||||||
|
## -> We could use tuple(), but list() works just as fine when writing.
|
||||||
|
## -> Later, we read back as tuple() to respect the type annotation.
|
||||||
|
## -> Part of the workaround for the Matrix Display Bug happens here.
|
||||||
|
case BPT.BoolVector | BPT.IntVector | BPT.FloatVector:
|
||||||
|
return list(value)
|
||||||
|
case BPT.BoolMatrix | BPT.IntMatrix | BPT.FloatMatrix:
|
||||||
|
rows = len(value)
|
||||||
|
cols = len(value[0])
|
||||||
|
return (
|
||||||
|
np.array(value, dtype=self.primitive_type)
|
||||||
|
.flatten()
|
||||||
|
.reshape([cols, rows])
|
||||||
|
).tolist()
|
||||||
|
|
||||||
|
# String
|
||||||
|
## -> NOTE: This will happily encode StrEnums->str if an enum isn't requested.
|
||||||
|
case BPT.Str if isinstance(value, str):
|
||||||
|
return value
|
||||||
|
|
||||||
|
# Path: Use Absolute-Resolved Stringified Path
|
||||||
|
## -> TODO: Watch out for OS-dependence.
|
||||||
|
case BPT.Path if isinstance(value, Path):
|
||||||
|
return str(value.resolve())
|
||||||
|
|
||||||
|
# Empty Enums
|
||||||
|
## -> Coerce None to 'NONE', since 'NONE' is injected by convention.
|
||||||
|
case (
|
||||||
|
BPT.SingleEnum
|
||||||
|
| BPT.SetEnum
|
||||||
|
| BPT.SingleDynEnum
|
||||||
|
| BPT.SetDynEnum
|
||||||
|
) if value is None:
|
||||||
|
return 'NONE'
|
||||||
|
|
||||||
|
# Single Enum: Coerce to str
|
||||||
|
## -> isinstance(StrEnum.Entry, str) always returns True; thus, a good sanity check.
|
||||||
|
## -> Explicit/Dynamic both encode to str; only decode() coersion differentiates.
|
||||||
|
case BPT.SingleEnum | BPT.SingleDynEnum if isinstance(value, str):
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
# Single Enum: Coerce to set[str]
|
||||||
|
case BPT.SetEnum | BPT.SetDynEnum if isinstance(value, set):
|
||||||
|
return {str(v) for v in value}
|
||||||
|
|
||||||
|
# BLPointer: Don't Alter
|
||||||
|
case BPT.BLPointer if value in BLIDStructs or value is None:
|
||||||
|
return value
|
||||||
|
|
||||||
|
# Serialized: Serialize To UTF-8
|
||||||
|
## -> TODO: Check serializability
|
||||||
|
case BPT.Serialized:
|
||||||
|
return serialize.encode(value).decode('utf-8')
|
||||||
|
|
||||||
|
msg = f'{self}: No encoder defined for argument {value}'
|
||||||
|
raise NotImplementedError(msg)
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Decode Value
|
||||||
|
####################
|
||||||
|
def decode(self, raw_value: typ.Any, obj_type: type) -> typ.Any: # noqa: PLR0911
|
||||||
|
"""Transform a raw value from a form read directly from the Blender property returned by `self.bl_type`, to its intended value of approximate type `obj_type`.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
`obj_type` is only a hint - for example, `obj_type = enum.StrEnum` is an indicator for a dynamic enum.
|
||||||
|
Its purpose is to guide ex. sizing and `StrEnum` coersion, not to guarantee a particular output type.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
value: A value which should be transformed into a form that can be written to the Blender property returned by `self.bl_type`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A value that can be written directly to the Blender property returned by `self.bl_type`.
|
||||||
|
"""
|
||||||
|
BPT = BLPropType
|
||||||
|
match self:
|
||||||
|
# Scalars: Inverse Coerce (~Losslessly)
|
||||||
|
## -> We choose to be very strict, except for float.is_integer() -> int
|
||||||
|
case BPT.Bool if isinstance(raw_value, bool):
|
||||||
|
return raw_value
|
||||||
|
case BPT.Int if isinstance(raw_value, int):
|
||||||
|
return raw_value
|
||||||
|
case BPT.Int if isinstance(raw_value, float) and raw_value.is_integer():
|
||||||
|
return int(raw_value)
|
||||||
|
case BPT.Float if isinstance(raw_value, float):
|
||||||
|
return float(raw_value)
|
||||||
|
|
||||||
|
# Vectors | Matrices: tuple() to match declared type annotation.
|
||||||
|
## -> Part of the workaround for the Matrix Display Bug happens here.
|
||||||
|
case BPT.BoolVector | BPT.IntVector | BPT.FloatVector:
|
||||||
|
return tuple(raw_value)
|
||||||
|
case BPT.BoolMatrix | BPT.IntMatrix | BPT.FloatMatrix:
|
||||||
|
rows, cols = self.parse_size(obj_type)
|
||||||
|
return tuple(
|
||||||
|
map(tuple, np.array(raw_value).flatten().reshape([rows, cols]))
|
||||||
|
)
|
||||||
|
|
||||||
|
# String
|
||||||
|
## -> NOTE: This will happily decode StrEnums->str if an enum isn't requested.
|
||||||
|
case BPT.Str if isinstance(raw_value, str):
|
||||||
|
return raw_value
|
||||||
|
|
||||||
|
# Path: Use 'Path(abspath(*))'
|
||||||
|
## -> TODO: Watch out for OS-dependence.
|
||||||
|
case BPT.Path if isinstance(raw_value, str):
|
||||||
|
return Path(bpy.path.abspath(raw_value))
|
||||||
|
|
||||||
|
# Empty Enums
|
||||||
|
## -> Coerce 'NONE' to None, since 'NONE' is injected by convention.
|
||||||
|
## -> Using coerced 'NONE' as guaranteed len=0 element is extremely helpful.
|
||||||
|
case (
|
||||||
|
BPT.SingleEnum
|
||||||
|
| BPT.SetEnum
|
||||||
|
| BPT.SingleDynEnum
|
||||||
|
| BPT.SetDynEnum
|
||||||
|
) if raw_value in ['NONE']:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Explicit Enum: Coerce to predefined StrEnum
|
||||||
|
## -> This happens independent of whether there's a enum_cb.
|
||||||
|
case BPT.SingleEnum if isinstance(raw_value, str):
|
||||||
|
return obj_type(raw_value)
|
||||||
|
case BPT.SetEnum if isinstance(raw_value, set):
|
||||||
|
return {obj_type(v) for v in raw_value}
|
||||||
|
|
||||||
|
## Dynamic Enums: Nothing to coerce to.
|
||||||
|
## -> The critical distinction is that dynamic enums can't be coerced beyond str.
|
||||||
|
## -> All dynamic enums have an enum_cb, but this is merely a symptom of ^.
|
||||||
|
case BPT.SingleDynEnum if isinstance(raw_value, str):
|
||||||
|
return raw_value
|
||||||
|
case BPT.SetDynEnum if isinstance(raw_value, set):
|
||||||
|
return raw_value
|
||||||
|
|
||||||
|
# BLPointer
|
||||||
|
## -> None is always valid when it comes to BLPointers.
|
||||||
|
case BPT.BLPointer if raw_value in BLIDStructs or raw_value is None:
|
||||||
|
return raw_value
|
||||||
|
|
||||||
|
# Serialized: Deserialize the Argument
|
||||||
|
case BPT.Serialized:
|
||||||
|
return serialize.decode(obj_type, raw_value)
|
||||||
|
|
||||||
|
msg = f'{self}: No decoder defined for argument {raw_value}'
|
||||||
|
raise NotImplementedError(msg)
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Parse Type
|
||||||
|
####################
|
||||||
|
@staticmethod
|
||||||
|
def from_type(obj_type: type) -> typ.Self: # noqa: PLR0911, PLR0912, C901
|
||||||
|
"""Select an appropriate `BLPropType` to store objects of the given type.
|
||||||
|
|
||||||
|
Use of this method is especially handy when attempting to represent arbitrary, type-annotated objects using Blender properties.
|
||||||
|
For example, the ability of the `BLPropType` to be displayed in a UI is prioritized as much as possible in making this decision.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
obj_type: A type like `bool`, `str`, or custom classes.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A `BLPropType` capable of storing any object of `obj_type`.
|
||||||
|
"""
|
||||||
|
BPT = BLPropType
|
||||||
|
|
||||||
|
# Match Simple
|
||||||
|
match obj_type:
|
||||||
|
case builtins.bool:
|
||||||
|
return BPT.Bool
|
||||||
|
case builtins.int:
|
||||||
|
return BPT.Int
|
||||||
|
case builtins.float:
|
||||||
|
return BPT.Float
|
||||||
|
case builtins.str:
|
||||||
|
return BPT.Str
|
||||||
|
case pathlib.Path:
|
||||||
|
return BPT.Path
|
||||||
|
case enum.StrEnum:
|
||||||
|
return BPT.SingleDynEnum
|
||||||
|
case _:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Match Arrays
|
||||||
|
## -> This deconstructs generic statements like ex. tuple[int, int]
|
||||||
|
typ_origin = typ.get_origin(obj_type)
|
||||||
|
typ_args = typ.get_args(obj_type)
|
||||||
|
if typ_origin is tuple and len(typ_args) > 0:
|
||||||
|
# Match Vectors
|
||||||
|
## -> ONLY respect homogeneous types
|
||||||
|
if all(T is bool for T in typ_args):
|
||||||
|
return BPT.BoolVector
|
||||||
|
if all(T is int for T in typ_args):
|
||||||
|
return BPT.IntVector
|
||||||
|
if all(T is float for T in typ_args):
|
||||||
|
return BPT.FloatVector
|
||||||
|
|
||||||
|
# Match Matrices
|
||||||
|
## -> ONLY respect twice-nested homogeneous types
|
||||||
|
## -> TODO: Explicitly require regularized shape, as in _parse_matrix_size
|
||||||
|
typ_args_args = [typ_arg for T0 in typ_args for typ_arg in typ.get_args(T0)]
|
||||||
|
if typ_args_args:
|
||||||
|
if all(T is bool for T in typ_args_args):
|
||||||
|
return BPT.BoolMatrix
|
||||||
|
if all(T is int for T in typ_args_args):
|
||||||
|
return BPT.IntMatrix
|
||||||
|
if all(T is float for T in typ_args_args):
|
||||||
|
return BPT.FloatMatrix
|
||||||
|
|
||||||
|
# Match SetDynEnum
|
||||||
|
## -> We can't do this in the match statement
|
||||||
|
if obj_type == set[enum.StrEnum]:
|
||||||
|
return BPT.SetDynEnum
|
||||||
|
|
||||||
|
# Match Static Enums
|
||||||
|
## -> Match Single w/Helper Function
|
||||||
|
if _is_strenum(obj_type):
|
||||||
|
return BPT.SingleEnum
|
||||||
|
|
||||||
|
## -> Match Set w/Helper Function
|
||||||
|
if typ_origin is set and len(typ_args) == 1 and _is_strenum(typ_args[0]):
|
||||||
|
return BPT.SetEnum
|
||||||
|
|
||||||
|
# Match BLPointers
|
||||||
|
if obj_type in BLIDStructs:
|
||||||
|
return BPT.BLPointer
|
||||||
|
|
||||||
|
# Fallback: Serializable Object
|
||||||
|
## -> TODO: Check serializability.
|
||||||
|
return BPT.Serialized
|
|
@ -0,0 +1,246 @@
|
||||||
|
# blender_maxwell
|
||||||
|
# Copyright (C) 2024 blender_maxwell Project Contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""Implements various key caches on instances of Blender objects, especially nodes and sockets."""
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
import typing as typ
|
||||||
|
|
||||||
|
from blender_maxwell.utils import bl_instance, logger, serialize
|
||||||
|
|
||||||
|
from .bl_prop import BLProp
|
||||||
|
from .bl_prop_type import BLPropType
|
||||||
|
from .signal import Signal
|
||||||
|
|
||||||
|
log = logger.get(__name__)
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Types
|
||||||
|
####################
|
||||||
|
PropGetMethod: typ.TypeAlias = typ.Callable[
|
||||||
|
[bl_instance.BLInstance], serialize.NaivelyEncodableType
|
||||||
|
]
|
||||||
|
PropSetMethod: typ.TypeAlias = typ.Callable[
|
||||||
|
[bl_instance.BLInstance, serialize.NaivelyEncodableType], None
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - CachedBLProperty
|
||||||
|
####################
|
||||||
|
class CachedBLProperty:
|
||||||
|
"""A descriptor that caches a computed attribute of a Blender node/socket/... instance (`bl_instance`).
|
||||||
|
|
||||||
|
Generally used via the associated decorator, `cached_bl_property`.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
It's like `@cached_property`, but on each Blender Instance ID.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
getter_method: Method of `bl_instance` that computes the value.
|
||||||
|
setter_method: Method of `bl_instance` that sets the value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
getter_method: PropGetMethod,
|
||||||
|
persist: bool = False,
|
||||||
|
depends_on: frozenset[str] = frozenset(),
|
||||||
|
):
|
||||||
|
"""Initialize the getter of the cached property.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
getter_method: Method of `bl_instance` that computes the value.
|
||||||
|
"""
|
||||||
|
self.getter_method: PropGetMethod = getter_method
|
||||||
|
self.setter_method: PropSetMethod | None = None
|
||||||
|
|
||||||
|
self.persist: bool = persist
|
||||||
|
self.depends_on: frozenset[str] = depends_on
|
||||||
|
|
||||||
|
self.bl_prop: BLProp | None = None
|
||||||
|
|
||||||
|
self.decode_type: type = inspect.signature(getter_method).return_annotation
|
||||||
|
|
||||||
|
# Check Non-Empty Type Annotation
|
||||||
|
## For now, just presume that all types can be encoded/decoded.
|
||||||
|
if self.decode_type is inspect.Signature.empty:
|
||||||
|
msg = f'A CachedBLProperty was instantiated, but its getter method "{self.getter_method}" has no return type annotation'
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
def __set_name__(self, owner: type[bl_instance.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.bl_prop = BLProp(
|
||||||
|
name=name,
|
||||||
|
prop_info={'use_prop_update': True},
|
||||||
|
prop_type=self.decode_type,
|
||||||
|
bl_prop_type=BLPropType.Serialized,
|
||||||
|
)
|
||||||
|
self.bl_prop.init_bl_type(owner, depends_on=self.depends_on)
|
||||||
|
|
||||||
|
def __get__(
|
||||||
|
self,
|
||||||
|
bl_instance: bl_instance.BLInstance | None,
|
||||||
|
owner: type[bl_instance.BLInstance],
|
||||||
|
) -> typ.Any:
|
||||||
|
"""Retrieves the property from a cache, or computes it and fills the cache(s).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
bl_instance: The Blender object this prop
|
||||||
|
"""
|
||||||
|
cached_value = self.bl_prop.read_nonpersist(bl_instance)
|
||||||
|
if cached_value is Signal.CacheNotReady or cached_value is Signal.CacheEmpty:
|
||||||
|
if bl_instance is not None:
|
||||||
|
if self.persist:
|
||||||
|
value = self.bl_prop.read(bl_instance)
|
||||||
|
else:
|
||||||
|
value = self.getter_method(bl_instance)
|
||||||
|
|
||||||
|
self.bl_prop.write_nonpersist(bl_instance, value)
|
||||||
|
return value
|
||||||
|
return Signal.CacheNotReady
|
||||||
|
return cached_value
|
||||||
|
|
||||||
|
def __set__(
|
||||||
|
self, bl_instance: bl_instance.BLInstance | None, 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 value is Signal.DoUpdate:
|
||||||
|
bl_instance.on_prop_changed(self.bl_prop.name)
|
||||||
|
|
||||||
|
elif value is Signal.InvalidateCache or value is Signal.InvalidateCacheNoUpdate:
|
||||||
|
# Invalidate Partner Non-Persistent Caches
|
||||||
|
## -> Only for the invalidation case do we also invalidate partners.
|
||||||
|
if bl_instance is not None:
|
||||||
|
# Fill Caches
|
||||||
|
## -> persist: Fill Persist and Non-Persist Cache
|
||||||
|
## -> else: Fill Non-Persist Cache
|
||||||
|
if self.persist:
|
||||||
|
self.bl_prop.write(bl_instance, self.getter_method(bl_instance))
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.bl_prop.write_nonpersist(
|
||||||
|
bl_instance, self.getter_method(bl_instance)
|
||||||
|
)
|
||||||
|
|
||||||
|
if value == Signal.InvalidateCache:
|
||||||
|
bl_instance.on_prop_changed(self.bl_prop.name)
|
||||||
|
|
||||||
|
elif self.setter_method is not None:
|
||||||
|
# Run Setter
|
||||||
|
## -> The user-provided setter should do any updating of partners.
|
||||||
|
if self.setter_method is not None:
|
||||||
|
self.setter_method(bl_instance, value)
|
||||||
|
|
||||||
|
# Fill Non-Persistant (and maybe Persistent) Cache
|
||||||
|
if self.persist:
|
||||||
|
self.bl_prop.write(bl_instance, self.getter_method(bl_instance))
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.bl_prop.write_nonpersist(
|
||||||
|
bl_instance, self.getter_method(bl_instance)
|
||||||
|
)
|
||||||
|
bl_instance.on_prop_changed(self.bl_prop.name)
|
||||||
|
|
||||||
|
else:
|
||||||
|
msg = f'Tried to set "{value}" to "{self.prop_name}" on "{bl_instance.bl_label}", but a setter was not defined'
|
||||||
|
raise NotImplementedError(msg)
|
||||||
|
|
||||||
|
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 decorator, it looks like this:
|
||||||
|
```python
|
||||||
|
class Test(bpy.types.Node):
|
||||||
|
bl_label = 'Default'
|
||||||
|
...
|
||||||
|
def method(self) -> str: return self.bl_label
|
||||||
|
attr = CachedBLProperty(getter_method=method)
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Decorator
|
||||||
|
####################
|
||||||
|
def cached_bl_property(
|
||||||
|
persist: bool = False,
|
||||||
|
depends_on: frozenset[str] = frozenset(),
|
||||||
|
):
|
||||||
|
"""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.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Unfortunately, `functools.cached_property` doesn't work.
|
||||||
|
- Use `cached_attribute` if not using a node/socket.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
```python
|
||||||
|
class CustomNode(bpy.types.Node):
|
||||||
|
@bl_cache.cached()
|
||||||
|
def computed_prop(self) -> ...: return ...
|
||||||
|
|
||||||
|
print(bl_instance.prop) ## Computes first time
|
||||||
|
print(bl_instance.prop) ## Cached (after restart, will recompute)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(getter_method: typ.Callable[[bl_instance.BLInstance], None]) -> type:
|
||||||
|
return CachedBLProperty(
|
||||||
|
getter_method=getter_method, persist=persist, depends_on=depends_on
|
||||||
|
)
|
||||||
|
|
||||||
|
return decorator
|
|
@ -0,0 +1,146 @@
|
||||||
|
# blender_maxwell
|
||||||
|
# Copyright (C) 2024 blender_maxwell Project Contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import inspect
|
||||||
|
import typing as typ
|
||||||
|
|
||||||
|
from blender_maxwell.utils import bl_instance, logger, serialize
|
||||||
|
|
||||||
|
log = logger.get(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class KeyedCache:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
func: typ.Callable,
|
||||||
|
exclude: set[str],
|
||||||
|
encode: set[str],
|
||||||
|
):
|
||||||
|
# Function Information
|
||||||
|
self.func: typ.Callable = func
|
||||||
|
self.func_sig: inspect.Signature = inspect.signature(self.func)
|
||||||
|
|
||||||
|
# Arg -> Key Information
|
||||||
|
self.exclude: set[str] = exclude
|
||||||
|
self.include: set[str] = set(self.func_sig.parameters.keys()) - exclude
|
||||||
|
self.encode: set[str] = encode
|
||||||
|
|
||||||
|
# Cache Information
|
||||||
|
self.key_schema: tuple[str, ...] = tuple(
|
||||||
|
[
|
||||||
|
arg_name
|
||||||
|
for arg_name in self.func_sig.parameters
|
||||||
|
if arg_name not in exclude
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.caches: dict[str | None, dict[tuple[typ.Any, ...], typ.Any]] = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_method(self):
|
||||||
|
return 'self' in self.exclude
|
||||||
|
|
||||||
|
def cache(self, instance_id: str | None) -> dict[tuple[typ.Any, ...], typ.Any]:
|
||||||
|
if self.caches.get(instance_id) is None:
|
||||||
|
self.caches[instance_id] = {}
|
||||||
|
|
||||||
|
return self.caches[instance_id]
|
||||||
|
|
||||||
|
def _encode_key(self, arguments: dict[str, typ.Any]):
|
||||||
|
## WARNING: Order of arguments matters. Arguments may contain 'exclude'd elements.
|
||||||
|
return tuple(
|
||||||
|
[
|
||||||
|
(
|
||||||
|
arg_value
|
||||||
|
if arg_name not in self.encode
|
||||||
|
else serialize.encode(arg_value)
|
||||||
|
)
|
||||||
|
for arg_name, arg_value in arguments.items()
|
||||||
|
if arg_name in self.include
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def __get__(
|
||||||
|
self,
|
||||||
|
bl_instance: bl_instance.BLInstance | None,
|
||||||
|
owner: type[bl_instance.BLInstance],
|
||||||
|
) -> typ.Callable:
|
||||||
|
_func = functools.partial(self, bl_instance)
|
||||||
|
_func.invalidate = functools.partial(
|
||||||
|
self.__class__.invalidate, self, bl_instance
|
||||||
|
)
|
||||||
|
return _func
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
# Test Argument Bindability to Decorated Function
|
||||||
|
try:
|
||||||
|
bound_args = self.func_sig.bind(*args, **kwargs)
|
||||||
|
except TypeError as ex:
|
||||||
|
msg = f'Can\'t bind arguments (args={args}, kwargs={kwargs}) to @keyed_cache-decorated function "{self.func.__name__}" (signature: {self.func_sig})"'
|
||||||
|
raise ValueError(msg) from ex
|
||||||
|
|
||||||
|
# Check that Parameters for Keying the Cache are Available
|
||||||
|
bound_args.apply_defaults()
|
||||||
|
all_arg_keys = set(bound_args.arguments.keys())
|
||||||
|
if not self.include <= (all_arg_keys - self.exclude):
|
||||||
|
msg = f'Arguments spanning the keyed cached ({self.include}) are not available in the non-excluded arguments passed to "{self.func.__name__}": {all_arg_keys - self.exclude}'
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
# Create Keyed Cache Entry
|
||||||
|
key = self._encode_key(bound_args.arguments)
|
||||||
|
cache = self.cache(args[0].instance_id if self.is_method else None)
|
||||||
|
if (value := cache.get(key)) is None:
|
||||||
|
value = self.func(*args, **kwargs)
|
||||||
|
cache[key] = value
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def invalidate(
|
||||||
|
self,
|
||||||
|
bl_instance: bl_instance.BLInstance | None,
|
||||||
|
**arguments: dict[str, typ.Any],
|
||||||
|
) -> dict[str, typ.Any]:
|
||||||
|
# Determine Wildcard Arguments
|
||||||
|
wildcard_arguments = {
|
||||||
|
arg_name for arg_name, arg_value in arguments.items() if arg_value is ...
|
||||||
|
}
|
||||||
|
|
||||||
|
# Compute Keys to Invalidate
|
||||||
|
arguments_hashable = {
|
||||||
|
arg_name: serialize.encode(arg_value)
|
||||||
|
if arg_name in self.encode and arg_name not in wildcard_arguments
|
||||||
|
else arg_value
|
||||||
|
for arg_name, arg_value in arguments.items()
|
||||||
|
}
|
||||||
|
cache = self.cache(bl_instance.instance_id if self.is_method else None)
|
||||||
|
for key in list(cache.keys()):
|
||||||
|
if all(
|
||||||
|
arguments_hashable.get(arg_name) == arg_value
|
||||||
|
for arg_name, arg_value in zip(self.key_schema, key, strict=True)
|
||||||
|
if arg_name not in wildcard_arguments
|
||||||
|
):
|
||||||
|
cache.pop(key)
|
||||||
|
|
||||||
|
|
||||||
|
def keyed_cache(exclude: set[str], encode: set[str] = frozenset()) -> typ.Callable:
|
||||||
|
def decorator(func: typ.Callable) -> typ.Callable:
|
||||||
|
return KeyedCache(
|
||||||
|
func,
|
||||||
|
exclude=exclude,
|
||||||
|
encode=encode,
|
||||||
|
)
|
||||||
|
|
||||||
|
return decorator
|
|
@ -0,0 +1,171 @@
|
||||||
|
# blender_maxwell
|
||||||
|
# Copyright (C) 2024 blender_maxwell Project Contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""Implements various key caches on instances of Blender objects, especially nodes and sockets."""
|
||||||
|
|
||||||
|
## TODO: Note that persist=True on cached_bl_property may cause a draw method to try and write to a Blender property, which Blender disallows.
|
||||||
|
|
||||||
|
import typing as typ
|
||||||
|
|
||||||
|
from blender_maxwell import contracts as ct
|
||||||
|
from blender_maxwell.utils import bl_instance, logger
|
||||||
|
|
||||||
|
from .signal import Signal
|
||||||
|
|
||||||
|
log = logger.get(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Global Variables
|
||||||
|
####################
|
||||||
|
_CACHE_NONPERSIST: dict[bl_instance.InstanceID, dict[typ.Hashable, typ.Any]] = {}
|
||||||
|
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Create/Invalidate
|
||||||
|
####################
|
||||||
|
def bl_instance_nonpersist_cache(
|
||||||
|
bl_instance: bl_instance.BLInstance,
|
||||||
|
) -> dict[typ.Hashable, typ.Any]:
|
||||||
|
"""Retrieve the non-persistent cache of a BLInstance."""
|
||||||
|
# Create Non-Persistent Cache Entry
|
||||||
|
## Prefer explicit cache management to 'defaultdict'
|
||||||
|
if _CACHE_NONPERSIST.get(bl_instance.instance_id) is None:
|
||||||
|
_CACHE_NONPERSIST[bl_instance.instance_id] = {}
|
||||||
|
|
||||||
|
return _CACHE_NONPERSIST[bl_instance.instance_id]
|
||||||
|
|
||||||
|
|
||||||
|
def invalidate_nonpersist_instance_id(instance_id: bl_instance.InstanceID) -> None:
|
||||||
|
"""Invalidate any `instance_id` that might be utilizing cache space in `_CACHE_NONPERSIST`.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
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_NONPERSIST.pop(instance_id, None)
|
||||||
|
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Access
|
||||||
|
####################
|
||||||
|
def read(
|
||||||
|
bl_instance: bl_instance.BLInstance | None,
|
||||||
|
key: typ.Hashable,
|
||||||
|
use_nonpersist: bool = True,
|
||||||
|
use_persist: bool = False,
|
||||||
|
) -> typ.Any | typ.Literal[Signal.CacheNotReady, Signal.CacheEmpty]:
|
||||||
|
"""Read the cache associated with a Blender Instance, without writing to it.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
key: The name to read from the instance-specific cache.
|
||||||
|
use_nonpersist: If true, will always try the non-persistent cache first.
|
||||||
|
use_persist: If true, will always try accessing the attribute `bl_instance,key`, where `key` is the value of the same-named parameter.
|
||||||
|
Generally, such an attribute should be a `bpy.types.Property`.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
The cache hit, if any; else `Signal.CacheEmpty`.
|
||||||
|
"""
|
||||||
|
# Check BLInstance Readiness
|
||||||
|
if bl_instance is None:
|
||||||
|
return Signal.CacheNotReady
|
||||||
|
|
||||||
|
# Try Hit on Persistent Cache
|
||||||
|
if use_persist:
|
||||||
|
value = getattr(bl_instance, key, Signal.CacheEmpty)
|
||||||
|
if value is not Signal.CacheEmpty:
|
||||||
|
return value
|
||||||
|
|
||||||
|
# Check if Instance ID is Available
|
||||||
|
if not bl_instance.instance_id:
|
||||||
|
log.debug(
|
||||||
|
"Can't Get CachedBLProperty: Instance ID not (yet) defined on bl_instance.BLInstance %s",
|
||||||
|
str(bl_instance),
|
||||||
|
)
|
||||||
|
return Signal.CacheNotReady
|
||||||
|
|
||||||
|
# Try Hit on Non-Persistent Cache
|
||||||
|
if use_nonpersist:
|
||||||
|
cache_nonpersist = bl_instance_nonpersist_cache(bl_instance)
|
||||||
|
value = cache_nonpersist.get(key, Signal.CacheEmpty)
|
||||||
|
if value is not Signal.CacheEmpty:
|
||||||
|
return value
|
||||||
|
|
||||||
|
return Signal.CacheEmpty
|
||||||
|
|
||||||
|
|
||||||
|
def write(
|
||||||
|
bl_instance: bl_instance.BLInstance,
|
||||||
|
key: typ.Hashable,
|
||||||
|
value: typ.Any, ## TODO: "Serializable" type
|
||||||
|
use_nonpersist: bool = True,
|
||||||
|
use_persist: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""Write to the cache associated with a Blender Instance.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
key: The name to write to the instance-specific cache.
|
||||||
|
use_nonpersist: If true, will always write to the non-persistent cache first.
|
||||||
|
use_persist: If true, will always write to attribute `bl_instance.key`, where `key` is the value of the same-named parameter.
|
||||||
|
Generally, such an attribute should be a `bpy.types.Property`.
|
||||||
|
call_on_prop_changed: Whether to trigger `bl_instance.on_prop_changed()` with the
|
||||||
|
"""
|
||||||
|
# Check BLInstance Readiness
|
||||||
|
if bl_instance is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Try Write on Persistent Cache
|
||||||
|
if use_persist:
|
||||||
|
# log.critical('%s: Writing %s to %s.', str(bl_instance), str(value), str(key))
|
||||||
|
setattr(bl_instance, key, value)
|
||||||
|
|
||||||
|
if not bl_instance.instance_id:
|
||||||
|
log.debug(
|
||||||
|
"Can't Get CachedBLProperty: Instance ID not (yet) defined on bl_instance.BLInstance %s",
|
||||||
|
str(bl_instance),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Try Write on Non-Persistent Cache
|
||||||
|
if use_nonpersist:
|
||||||
|
cache_nonpersist = bl_instance_nonpersist_cache(bl_instance)
|
||||||
|
cache_nonpersist[key] = value
|
||||||
|
|
||||||
|
|
||||||
|
def invalidate_nonpersist(
|
||||||
|
bl_instance: bl_instance.BLInstance,
|
||||||
|
key: typ.Hashable,
|
||||||
|
) -> None:
|
||||||
|
"""Invalidate a particular key of the non-persistent cache.
|
||||||
|
|
||||||
|
**Persistent caches can't be invalidated without writing to them**.
|
||||||
|
To get the same effect, consider using `write()` to write its default value (which must be manually tracked).
|
||||||
|
"""
|
||||||
|
# Check BLInstance Readiness
|
||||||
|
if bl_instance is None:
|
||||||
|
return
|
||||||
|
if not bl_instance.instance_id:
|
||||||
|
log.debug(
|
||||||
|
"Can't Get CachedBLProperty: Instance ID not (yet) defined on bl_instance.BLInstance %s",
|
||||||
|
str(bl_instance),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Retrieve Non-Persistent Cache
|
||||||
|
cache_nonpersist = bl_instance_nonpersist_cache(bl_instance)
|
||||||
|
cache_nonpersist.pop(key, None)
|
|
@ -0,0 +1,61 @@
|
||||||
|
# blender_maxwell
|
||||||
|
# Copyright (C) 2024 blender_maxwell Project Contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import enum
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Signal(enum.StrEnum):
|
||||||
|
"""A value used to signal the descriptor via its `__set__`.
|
||||||
|
|
||||||
|
Such a signal **must** be entirely unique: Even a well-thought-out string could conceivably produce a very nasty bug, where instead of setting a descriptor-managed attribute, the user would inadvertently signal the descriptor.
|
||||||
|
|
||||||
|
To make it effectively impossible to confuse any other object whatsoever with a signal, the enum values are set to per-session `uuid.uuid4()`.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
**Do not** use this enum for anything other than directly signalling a `bl_cache` descriptor via its setter.
|
||||||
|
|
||||||
|
**Do not** store this enum `Signal` in a variable or method binding that survives longer than the session.
|
||||||
|
|
||||||
|
**Do not** persist this enum; the values will change whenever `bl_cache` is (re)loaded.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
CacheNotReady: The cache isn't yet ready to be used.
|
||||||
|
Generally, this is because the `BLInstance` isn't made yet.
|
||||||
|
CacheEmpty: The cache has no information to offer.
|
||||||
|
|
||||||
|
InvalidateCache: The cache should be invalidated.
|
||||||
|
InvalidateCacheNoUpdate: The cache should be invalidated, but no update method should be run.
|
||||||
|
DoUpdate: Any update method that the cache triggers on change should be run.
|
||||||
|
An update is **not guaranteeed** to be run, merely requested.
|
||||||
|
|
||||||
|
ResetEnumItems: Cached dynamic enum items should be recomputed on next use.
|
||||||
|
ResetStrSearch: Cached string-search items should be recomputed on next use.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Cache Management
|
||||||
|
CacheNotReady: str = str(uuid.uuid4())
|
||||||
|
CacheEmpty: str = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Invalidation
|
||||||
|
InvalidateCache: str = str(uuid.uuid4())
|
||||||
|
InvalidateCacheNoUpdate: str = str(uuid.uuid4())
|
||||||
|
DoUpdate: str = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Reset Signals
|
||||||
|
## -> Invalidates data adjascent to fields.
|
||||||
|
ResetEnumItems: str = str(uuid.uuid4())
|
||||||
|
ResetStrSearch: str = str(uuid.uuid4())
|
|
@ -0,0 +1,299 @@
|
||||||
|
# blender_maxwell
|
||||||
|
# Copyright (C) 2024 blender_maxwell Project Contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import typing as typ
|
||||||
|
import uuid
|
||||||
|
from types import MappingProxyType
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
from blender_maxwell.utils import bl_cache, logger
|
||||||
|
|
||||||
|
InstanceID: typ.TypeAlias = str ## Stringified UUID4
|
||||||
|
|
||||||
|
log = logger.get(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BLInstance:
|
||||||
|
"""An instance of a blender object, ex. nodes/sockets.
|
||||||
|
|
||||||
|
Used as a common base of functionality for nodes/sockets, especially when it comes to the magic introduced by `bl_cache`.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
All the `@classmethod`s are designed to be invoked with `cls` as the subclass of `BLInstance`, not `BLInstance` itself.
|
||||||
|
|
||||||
|
For practical reasons, introducing a metaclass here is not a good idea, and thus `abc.ABC` can't be used.
|
||||||
|
To this end, only `self.on_prop_changed` needs a subclass implementation.
|
||||||
|
It's a little sharp, but managable.
|
||||||
|
|
||||||
|
Inheritance schemes like this are generally not enjoyable.
|
||||||
|
However, the way Blender's node/socket classes are structured makes it the most practical way design for the functionality encapsulated here.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
instance_id: Stringified UUID4 that uniquely identifies an instance, among all active instances on all active classes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Attributes
|
||||||
|
####################
|
||||||
|
instance_id: bpy.props.StringProperty(default='')
|
||||||
|
|
||||||
|
blfields: typ.ClassVar[dict[str, str]] = MappingProxyType({})
|
||||||
|
blfield_deps: typ.ClassVar[dict[str, list[str]]] = MappingProxyType({})
|
||||||
|
|
||||||
|
blfields_dynamic_enum: typ.ClassVar[set[str]] = frozenset()
|
||||||
|
blfield_dynamic_enum_deps: typ.ClassVar[dict[str, list[str]]] = MappingProxyType({})
|
||||||
|
|
||||||
|
blfields_str_search: typ.ClassVar[set[str]] = frozenset()
|
||||||
|
blfield_str_search_deps: typ.ClassVar[dict[str, list[str]]] = MappingProxyType({})
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Runtime Instance Management
|
||||||
|
####################
|
||||||
|
def reset_instance_id(self) -> None:
|
||||||
|
"""Reset the Instance ID of a BLInstance.
|
||||||
|
|
||||||
|
The Instance ID is used to index the instance-specific cache, since Blender doesn't always directly support keeping changing data on node/socket instances.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
Should be run whenever the instance is copied, so that the copy will index its own cache.
|
||||||
|
|
||||||
|
The Instance ID is a `UUID4`, which is globally unique, negating the need for extraneous overlap-checks.
|
||||||
|
"""
|
||||||
|
self.instance_id = str(uuid.uuid4())
|
||||||
|
self.regenerate_dynamic_field_persistance()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def assert_attrs_valid(cls, mandatory_props: set[str]) -> None:
|
||||||
|
"""Asserts that all mandatory attributes are defined on the class.
|
||||||
|
|
||||||
|
The list of mandatory objects is generally sourced from a global variable, `MANDATORY_PROPS`, which should be passed to this function while running `__init_subclass__`.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If a mandatory attribute defined in `base.MANDATORY_PROPS` is not defined on the class.
|
||||||
|
"""
|
||||||
|
for cls_attr in mandatory_props:
|
||||||
|
if not hasattr(cls, cls_attr):
|
||||||
|
msg = f'Node class {cls} does not define mandatory attribute "{cls_attr}".'
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Field Registration
|
||||||
|
####################
|
||||||
|
@classmethod
|
||||||
|
def declare_blfield(
|
||||||
|
cls,
|
||||||
|
attr_name: str,
|
||||||
|
bl_attr_name: str,
|
||||||
|
use_dynamic_enum: bool = False,
|
||||||
|
use_str_search: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""Declare the existance of a (cached) field and any properties affecting its invalidation.
|
||||||
|
|
||||||
|
Primarily, the `attr_name -> bl_attr_name` map will be available via the `cls.blfields` dictionary.
|
||||||
|
Thus, for use in UIs (where `bl_attr_name` must be used), one can use `cls.blfields[attr_name]`.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
attr_name: The name of the attribute accessible via the instance.
|
||||||
|
bl_attr_name: The name of the attribute containing the Blender property.
|
||||||
|
This is used both as a persistant cache for `attr_name`, as well as (possibly) the data altered by the user from the UI.
|
||||||
|
use_dynamic_enum: Will mark `attr_name` as a dynamic enum.
|
||||||
|
Allows `self.regenerate_dynamic_field_persistance` to reset this property, whenever all dynamic `EnumProperty`s are reset at once.
|
||||||
|
use_str_searc: The name of the attribute containing the Blender property.
|
||||||
|
Allows `self.regenerate_dynamic_field_persistance` to reset this property, whenever all searched `StringProperty`s are reset at once.
|
||||||
|
"""
|
||||||
|
cls.blfields = cls.blfields | {attr_name: bl_attr_name}
|
||||||
|
|
||||||
|
if use_dynamic_enum:
|
||||||
|
cls.blfields_dynamic_enum = cls.blfields_dynamic_enum | {attr_name}
|
||||||
|
|
||||||
|
if use_str_search:
|
||||||
|
cls.blfields_str_search = cls.blfields_str_search | {attr_name}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def declare_blfield_dep(
|
||||||
|
cls,
|
||||||
|
src_prop_name: str,
|
||||||
|
dst_prop_name: str,
|
||||||
|
method: typ.Literal[
|
||||||
|
'invalidate', 'reset_enum', 'reset_strsearch'
|
||||||
|
] = 'invalidate',
|
||||||
|
) -> None:
|
||||||
|
"""Declare that `prop_name` relies on another property.
|
||||||
|
|
||||||
|
This is critical for cached, computed properties that must invalidate their cache whenever any of the data they rely on changes.
|
||||||
|
In practice, a chain of invalidation emerges naturally when this is put together, managed internally for performance.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
If the relevant `*_deps` dictionary is not defined on `cls`, we manually create it.
|
||||||
|
This shadows the relevant `BLInstance` attribute, which is an immutable `MappingProxyType` on purpose, precisely to prevent the situation of altering data that shouldn't be common to all classes inheriting from `BLInstance`.
|
||||||
|
|
||||||
|
Not clean, but it works.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
dep_prop_name: The property that should, whenever changed, also invalidate the cache of `prop_name`.
|
||||||
|
prop_name: The property that relies on another property.
|
||||||
|
"""
|
||||||
|
match method:
|
||||||
|
case 'invalidate':
|
||||||
|
if not cls.blfield_deps:
|
||||||
|
cls.blfield_deps = {}
|
||||||
|
deps = cls.blfield_deps
|
||||||
|
case 'reset_enum':
|
||||||
|
if not cls.blfield_dynamic_enum_deps:
|
||||||
|
cls.blfield_dynamic_enum_deps = {}
|
||||||
|
deps = cls.blfield_dynamic_enum_deps
|
||||||
|
case 'reset_strsearch':
|
||||||
|
if not cls.blfield_str_search_deps:
|
||||||
|
cls.blfield_str_search_deps = {}
|
||||||
|
deps = cls.blfield_str_search_deps
|
||||||
|
|
||||||
|
if deps.get(src_prop_name) is None:
|
||||||
|
deps[src_prop_name] = []
|
||||||
|
|
||||||
|
deps[src_prop_name].append(dst_prop_name)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def set_prop(
|
||||||
|
cls,
|
||||||
|
bl_prop_name: str,
|
||||||
|
prop: bpy.types.Property,
|
||||||
|
**kwargs,
|
||||||
|
) -> None:
|
||||||
|
"""Adds a Blender property via `__annotations__`, so that it will be initialized on all subclasses.
|
||||||
|
|
||||||
|
**All Blender properties trigger an update method** when updated from the UI, in order to invalidate the non-persistent cache of the associated `BLField`.
|
||||||
|
Specifically, this behavior happens in `on_bl_prop_changed()`.
|
||||||
|
|
||||||
|
However, whether anything else happens after that invalidation is entirely up to the particular `BLField`.
|
||||||
|
Thus, `BLField` is put in charge of how/when updates occur.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
In general, Blender properties can't be set on classes directly.
|
||||||
|
They must be added as type annotations, which Blender will read and understand.
|
||||||
|
|
||||||
|
This is essentially a convenience method to encapsulate this unexpected behavior, as well as constrain the behavior of the `update` method somewhat.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
bl_prop_name: The name of the property to set, as accessible from Blender.
|
||||||
|
Generally, from code, the user would access the wrapping `BLField` instead of directly accessing the `bl_prop_name` attribute.
|
||||||
|
prop: The `bpy.types.Property` to instantiate and attach..
|
||||||
|
kwargs: Constructor arguments to pass to the Blender property.
|
||||||
|
There are many mostly-documented nuances with these.
|
||||||
|
The methods of `bl_cache.BLPropType` are designed to provide more strict, helpful abstractions for practical use.
|
||||||
|
"""
|
||||||
|
cls.__annotations__[bl_prop_name] = prop(
|
||||||
|
update=lambda self, context: self.on_bl_prop_changed(bl_prop_name, context),
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Runtime Field Management
|
||||||
|
####################
|
||||||
|
def regenerate_dynamic_field_persistance(self):
|
||||||
|
"""Regenerate the persisted data of all dynamic enums and str search BLFields.
|
||||||
|
|
||||||
|
In practice, this sets special "signal" values:
|
||||||
|
- **Dynamic Enums**: The signal value `bl_cache.Signal.ResetEnumItems` will be set, causing `BLField.__set__` to regenerate the enum items using the user-provided callback.
|
||||||
|
- **Searched Strings**: The signal value `bl_cache.Signal.ResetStrSearch` will be set, causing `BLField.__set__` to regenerate the available search strings using the user-provided callback.
|
||||||
|
"""
|
||||||
|
# Generate Enum Items
|
||||||
|
## -> This guarantees that the items are persisted from the start.
|
||||||
|
for dyn_enum_prop_name in self.blfields_dynamic_enum:
|
||||||
|
setattr(self, dyn_enum_prop_name, bl_cache.Signal.ResetEnumItems)
|
||||||
|
|
||||||
|
# Generate Str Search Items
|
||||||
|
## -> Match dynamic enum semantics
|
||||||
|
for str_search_prop_name in self.blfields_str_search:
|
||||||
|
setattr(self, str_search_prop_name, bl_cache.Signal.ResetStrSearch)
|
||||||
|
|
||||||
|
def on_bl_prop_changed(self, bl_prop_name: str, _: bpy.types.Context) -> None:
|
||||||
|
"""Called when a property has been updated via the Blender UI.
|
||||||
|
|
||||||
|
The only effect is to invalidate the non-persistent cache of the associated BLField.
|
||||||
|
The BLField then decides whether to take any other action, ex. calling `self.on_prop_changed()`.
|
||||||
|
"""
|
||||||
|
## TODO: What about non-Blender set properties?
|
||||||
|
|
||||||
|
# Strip the Internal Prefix
|
||||||
|
## -> TODO: This is a bit of a hack. Use a contracts constant.
|
||||||
|
prop_name = bl_prop_name.removeprefix('blfield__')
|
||||||
|
# log.debug(
|
||||||
|
# 'Callback on Property %s (stripped: %s)',
|
||||||
|
# bl_prop_name,
|
||||||
|
# prop_name,
|
||||||
|
# )
|
||||||
|
# log.debug(
|
||||||
|
# 'Dependencies (PROP: %s) (ENUM: %s) (SEAR: %s)',
|
||||||
|
# self.blfield_deps,
|
||||||
|
# self.blfield_dynamic_enum_deps,
|
||||||
|
# self.blfield_str_search_deps,
|
||||||
|
# )
|
||||||
|
|
||||||
|
# Invalidate Property Cache
|
||||||
|
## -> Only the non-persistent cache is regenerated.
|
||||||
|
## -> The BLField decides whether to trigger `on_prop_changed`.
|
||||||
|
if prop_name in self.blfields:
|
||||||
|
# RULE: =1 DataChanged per Dependency Chain
|
||||||
|
## -> We MUST invalidate the cache, but might not want to update.
|
||||||
|
## -> Update should only be triggered when ==0 dependents.
|
||||||
|
setattr(self, prop_name, bl_cache.Signal.InvalidateCacheNoUpdate)
|
||||||
|
|
||||||
|
# Invalidate Dependent Properties (incl. DynEnums and StrSearch)
|
||||||
|
## -> NOTE: Dependent props may also trigger `on_prop_changed`.
|
||||||
|
## -> Meaning, don't use extraneous dependencies (as usual).
|
||||||
|
for deps, invalidate_signal in zip(
|
||||||
|
[
|
||||||
|
self.blfield_deps,
|
||||||
|
self.blfield_dynamic_enum_deps,
|
||||||
|
self.blfield_str_search_deps,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
bl_cache.Signal.InvalidateCache,
|
||||||
|
bl_cache.Signal.ResetEnumItems,
|
||||||
|
bl_cache.Signal.ResetStrSearch,
|
||||||
|
],
|
||||||
|
strict=True,
|
||||||
|
):
|
||||||
|
if prop_name in deps:
|
||||||
|
for dst_prop_name in deps[prop_name]:
|
||||||
|
# log.debug(
|
||||||
|
# 'Property %s is invalidating %s',
|
||||||
|
# prop_name,
|
||||||
|
# dst_prop_name,
|
||||||
|
# )
|
||||||
|
setattr(
|
||||||
|
self,
|
||||||
|
dst_prop_name,
|
||||||
|
invalidate_signal,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Do Update AFTER Dependencies
|
||||||
|
## -> Yes, update will run once per dependency.
|
||||||
|
## -> Don't abuse dependencies :)
|
||||||
|
## -> If no-update is important, use_prop_update is still respected.
|
||||||
|
setattr(self, prop_name, bl_cache.Signal.DoUpdate)
|
||||||
|
|
||||||
|
def on_prop_changed(self, prop_name: str) -> None:
|
||||||
|
"""Triggers changes/an event chain based on a changed property.
|
||||||
|
|
||||||
|
In general, the `BLField` descriptor associated with `prop_name` decides whether this method should be called whenever `__set__` is used.
|
||||||
|
An indirect consequence of this is that `self.on_bl_prop_changed`, which is _always_ triggered, may only _sometimes_ result in `on_prop_changed` being called, at the discretion of the relevant `BLField`.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
**Must** be overridden on all `BLInstance` subclasses.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
|
@ -96,6 +96,26 @@ class MathType(enum.StrEnum):
|
||||||
}[self]
|
}[self]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def coerce_compatible_pyobj(
|
||||||
|
self, pyobj: bool | int | Fraction | float | complex
|
||||||
|
) -> bool | int | Fraction | float | complex:
|
||||||
|
MT = MathType
|
||||||
|
match self:
|
||||||
|
case MT.Bool:
|
||||||
|
return pyobj
|
||||||
|
case MT.Integer:
|
||||||
|
return int(pyobj)
|
||||||
|
case MT.Rational if isinstance(pyobj, int):
|
||||||
|
return Fraction(pyobj, 1)
|
||||||
|
case MT.Rational if isinstance(pyobj, Fraction):
|
||||||
|
return pyobj
|
||||||
|
case MT.Real:
|
||||||
|
return float(pyobj)
|
||||||
|
case MT.Complex if isinstance(pyobj, int | Fraction):
|
||||||
|
return complex(float(pyobj), 0)
|
||||||
|
case MT.Complex if isinstance(pyobj, float):
|
||||||
|
return complex(pyobj, 0)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_expr(sp_obj: SympyType) -> type:
|
def from_expr(sp_obj: SympyType) -> type:
|
||||||
if isinstance(sp_obj, sp.MatrixBase):
|
if isinstance(sp_obj, sp.MatrixBase):
|
||||||
|
@ -124,21 +144,31 @@ class MathType(enum.StrEnum):
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_pytype(dtype) -> type:
|
def from_pytype(dtype: type) -> type:
|
||||||
return {
|
return {
|
||||||
bool: MathType.Bool,
|
bool: MathType.Bool,
|
||||||
int: MathType.Integer,
|
int: MathType.Integer,
|
||||||
|
Fraction: MathType.Rational,
|
||||||
float: MathType.Real,
|
float: MathType.Real,
|
||||||
complex: MathType.Complex,
|
complex: MathType.Complex,
|
||||||
}[dtype]
|
}[dtype]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def has_mathtype(obj: typ.Any) -> typ.Literal['pytype', 'expr'] | None:
|
||||||
|
if isinstance(obj, bool | int | Fraction | float | complex):
|
||||||
|
return 'pytype'
|
||||||
|
if isinstance(obj, sp.Basic | sp.MatrixBase | sp.MutableDenseMatrix):
|
||||||
|
return 'expr'
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pytype(self) -> type:
|
def pytype(self) -> type:
|
||||||
MT = MathType
|
MT = MathType
|
||||||
return {
|
return {
|
||||||
MT.Bool: bool,
|
MT.Bool: bool,
|
||||||
MT.Integer: int,
|
MT.Integer: int,
|
||||||
MT.Rational: float,
|
MT.Rational: Fraction,
|
||||||
MT.Real: float,
|
MT.Real: float,
|
||||||
MT.Complex: complex,
|
MT.Complex: complex,
|
||||||
}[self]
|
}[self]
|
||||||
|
@ -209,8 +239,20 @@ class NumberSize1D(enum.StrEnum):
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def supports_shape(shape: tuple[int, ...] | None):
|
def has_shape(shape: tuple[int, ...] | None):
|
||||||
return shape is None or (len(shape) == 1 and shape[0] in [2, 3])
|
return shape in [None, (2,), (3,), (4,), (2, 1), (3, 1), (4, 1)]
|
||||||
|
|
||||||
|
def supports_shape(self, shape: tuple[int, ...] | None):
|
||||||
|
NS = NumberSize1D
|
||||||
|
match self:
|
||||||
|
case NS.Scalar:
|
||||||
|
return shape is None
|
||||||
|
case NS.Vec2:
|
||||||
|
return shape in ((2,), (2, 1))
|
||||||
|
case NS.Vec3:
|
||||||
|
return shape in ((3,), (3, 1))
|
||||||
|
case NS.Vec4:
|
||||||
|
return shape in ((4,), (4, 1))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_shape(shape: tuple[typ.Literal[2, 3]] | None) -> typ.Self:
|
def from_shape(shape: tuple[typ.Literal[2, 3]] | None) -> typ.Self:
|
||||||
|
@ -220,6 +262,9 @@ class NumberSize1D(enum.StrEnum):
|
||||||
(2,): NS.Vec2,
|
(2,): NS.Vec2,
|
||||||
(3,): NS.Vec3,
|
(3,): NS.Vec3,
|
||||||
(4,): NS.Vec4,
|
(4,): NS.Vec4,
|
||||||
|
(2, 1): NS.Vec2,
|
||||||
|
(3, 1): NS.Vec3,
|
||||||
|
(4, 1): NS.Vec4,
|
||||||
}[shape]
|
}[shape]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -233,6 +278,14 @@ class NumberSize1D(enum.StrEnum):
|
||||||
}[self]
|
}[self]
|
||||||
|
|
||||||
|
|
||||||
|
def symbol_range(sym: sp.Symbol) -> str:
|
||||||
|
return f'{sym.name} ∈ ' + (
|
||||||
|
'ℂ'
|
||||||
|
if sym.is_complex
|
||||||
|
else ('ℝ' if sym.is_real else ('ℤ' if sym.is_integer else '?'))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Unit Dimensions
|
# - Unit Dimensions
|
||||||
####################
|
####################
|
||||||
|
@ -749,7 +802,7 @@ def scale_to_unit(sp_obj: SympyType, unit: spu.Quantity) -> Number:
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If the result of unit-conversion and -stripping still has units, as determined by `uses_units()`.
|
ValueError: If the result of unit-conversion and -stripping still has units, as determined by `uses_units()`.
|
||||||
"""
|
"""
|
||||||
unitless_expr = spu.convert_to(sp_obj, unit) / unit
|
unitless_expr = spu.convert_to(sp_obj, unit) / unit if unit is not None else sp_obj
|
||||||
if not uses_units(unitless_expr):
|
if not uses_units(unitless_expr):
|
||||||
return unitless_expr
|
return unitless_expr
|
||||||
|
|
||||||
|
@ -800,6 +853,9 @@ def unit_str_to_unit(unit_str: str) -> Unit | None:
|
||||||
class PhysicalType(enum.StrEnum):
|
class PhysicalType(enum.StrEnum):
|
||||||
"""Type identifiers for expressions with both `MathType` and a unit, aka a "physical" type."""
|
"""Type identifiers for expressions with both `MathType` and a unit, aka a "physical" type."""
|
||||||
|
|
||||||
|
# Unitless
|
||||||
|
NonPhysical = enum.auto()
|
||||||
|
|
||||||
# Global
|
# Global
|
||||||
Time = enum.auto()
|
Time = enum.auto()
|
||||||
Angle = enum.auto()
|
Angle = enum.auto()
|
||||||
|
@ -845,10 +901,11 @@ class PhysicalType(enum.StrEnum):
|
||||||
AngularWaveVector = enum.auto()
|
AngularWaveVector = enum.auto()
|
||||||
PoyntingVector = enum.auto()
|
PoyntingVector = enum.auto()
|
||||||
|
|
||||||
@property
|
@functools.cached_property
|
||||||
def unit_dim(self):
|
def unit_dim(self):
|
||||||
PT = PhysicalType
|
PT = PhysicalType
|
||||||
return {
|
return {
|
||||||
|
PT.NonPhysical: None,
|
||||||
# Global
|
# Global
|
||||||
PT.Time: Dims.time,
|
PT.Time: Dims.time,
|
||||||
PT.Angle: Dims.angle,
|
PT.Angle: Dims.angle,
|
||||||
|
@ -894,10 +951,11 @@ class PhysicalType(enum.StrEnum):
|
||||||
PT.PoyntingVector: Dims.power / Dims.length**2,
|
PT.PoyntingVector: Dims.power / Dims.length**2,
|
||||||
}[self]
|
}[self]
|
||||||
|
|
||||||
@property
|
@functools.cached_property
|
||||||
def default_unit(self) -> list[Unit]:
|
def default_unit(self) -> list[Unit]:
|
||||||
PT = PhysicalType
|
PT = PhysicalType
|
||||||
return {
|
return {
|
||||||
|
PT.NonPhysical: None,
|
||||||
# Global
|
# Global
|
||||||
PT.Time: spu.picosecond,
|
PT.Time: spu.picosecond,
|
||||||
PT.Angle: spu.radian,
|
PT.Angle: spu.radian,
|
||||||
|
@ -942,10 +1000,11 @@ class PhysicalType(enum.StrEnum):
|
||||||
PT.AngularWaveVector: spu.radian * terahertz,
|
PT.AngularWaveVector: spu.radian * terahertz,
|
||||||
}[self]
|
}[self]
|
||||||
|
|
||||||
@property
|
@functools.cached_property
|
||||||
def valid_units(self) -> list[Unit]:
|
def valid_units(self) -> list[Unit]:
|
||||||
PT = PhysicalType
|
PT = PhysicalType
|
||||||
return {
|
return {
|
||||||
|
PT.NonPhysical: [None],
|
||||||
# Global
|
# Global
|
||||||
PT.Time: [
|
PT.Time: [
|
||||||
femtosecond,
|
femtosecond,
|
||||||
|
@ -1101,12 +1160,13 @@ class PhysicalType(enum.StrEnum):
|
||||||
for physical_type in list(PhysicalType):
|
for physical_type in list(PhysicalType):
|
||||||
if unit in physical_type.valid_units:
|
if unit in physical_type.valid_units:
|
||||||
return physical_type
|
return physical_type
|
||||||
|
## TODO: Optimize
|
||||||
|
|
||||||
msg = f'No PhysicalType found for unit {unit}'
|
msg = f'Could not determine PhysicalType for {unit}'
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
@property
|
@functools.cached_property
|
||||||
def valid_shapes(self):
|
def valid_shapes(self) -> list[typ.Literal[(3,), (2,)] | None]:
|
||||||
PT = PhysicalType
|
PT = PhysicalType
|
||||||
overrides = {
|
overrides = {
|
||||||
# Cartesian
|
# Cartesian
|
||||||
|
@ -1133,7 +1193,7 @@ class PhysicalType(enum.StrEnum):
|
||||||
|
|
||||||
return overrides.get(self, [None])
|
return overrides.get(self, [None])
|
||||||
|
|
||||||
@property
|
@functools.cached_property
|
||||||
def valid_mathtypes(self) -> list[MathType]:
|
def valid_mathtypes(self) -> list[MathType]:
|
||||||
"""Returns a list of valid mathematical types, especially whether it can be real- or complex-valued.
|
"""Returns a list of valid mathematical types, especially whether it can be real- or complex-valued.
|
||||||
|
|
||||||
|
@ -1157,6 +1217,7 @@ class PhysicalType(enum.StrEnum):
|
||||||
MT = MathType
|
MT = MathType
|
||||||
PT = PhysicalType
|
PT = PhysicalType
|
||||||
overrides = {
|
overrides = {
|
||||||
|
PT.NonPhysical: list(MT), ## Support All
|
||||||
# Cartesian
|
# Cartesian
|
||||||
PT.Freq: [MT.Real, MT.Complex], ## Im -> Growth/Damping
|
PT.Freq: [MT.Real, MT.Complex], ## Im -> Growth/Damping
|
||||||
PT.AngFreq: [MT.Real, MT.Complex], ## Im -> Growth/Damping
|
PT.AngFreq: [MT.Real, MT.Complex], ## Im -> Growth/Damping
|
||||||
|
@ -1187,6 +1248,8 @@ class PhysicalType(enum.StrEnum):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def to_name(value: typ.Self) -> str:
|
def to_name(value: typ.Self) -> str:
|
||||||
|
if value is PhysicalType.NonPhysical:
|
||||||
|
return 'Unitless'
|
||||||
return PhysicalType(value).name
|
return PhysicalType(value).name
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
@ -21,7 +21,7 @@ class staticproperty(property): # noqa: N801
|
||||||
The decorated method must take no arguments whatsoever, including `self`/`cls`.
|
The decorated method must take no arguments whatsoever, including `self`/`cls`.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
Use as usual:
|
Exactly as you'd expect.
|
||||||
```python
|
```python
|
||||||
class Spam:
|
class Spam:
|
||||||
@staticproperty
|
@staticproperty
|
||||||
|
|
Loading…
Reference in New Issue