refactor: Fixes and movement.

main
Sofus Albert Høgsbro Rose 2024-04-17 18:14:14 +02:00
parent 29cee2e7a2
commit c6e00dcd7b
Signed by: so-rose
GPG Key ID: AD901CB0F3701434
25 changed files with 456 additions and 365 deletions

View File

@ -36,7 +36,7 @@
- Output: Write the input socket value.
- Condition: Input socket is unlinked. (If it's linked, then lock the object's position. Use sync_link_added() for that)
- Node to BL:
- Trigger: "Report" action on an input socket that the managed object declares reliance on.
- Trigger: "Report" event on an input socket that the managed object declares reliance on.
- Input: The input socket value (linked or unlinked)
- Output: The object location (origin), using a unit system.

View File

@ -0,0 +1,77 @@
import typing as typ
import bpy
import pydantic as pyd
import typing_extensions as typx
####################
# - Blender Strings
####################
BLEnumID = typx.Annotated[
str,
pyd.StringConstraints(
pattern=r'^[A-Z_]+$',
),
]
SocketName = typx.Annotated[
str,
pyd.StringConstraints(
pattern=r'^[a-zA-Z0-9_]+$',
),
]
####################
# - Blender Enums
####################
BLModifierType: typ.TypeAlias = typx.Literal['NODES', 'ARRAY']
BLNodeTreeInterfaceID: typ.TypeAlias = str
BLIconSet: frozenset[str] = frozenset(
bpy.types.UILayout.bl_rna.functions['prop'].parameters['icon'].enum_items.keys()
)
####################
# - Blender Structs
####################
BLClass: typ.TypeAlias = (
bpy.types.Panel
| bpy.types.UIList
| bpy.types.Menu
| bpy.types.Header
| bpy.types.Operator
| bpy.types.KeyingSetInfo
| bpy.types.RenderEngine
| bpy.types.AssetShelf
| bpy.types.FileHandler
)
BLKeymapItem: typ.TypeAlias = typ.Any ## TODO: Better Type
BLColorRGBA = tuple[float, float, float, float]
####################
# - Operators
####################
BLOperatorStatus: typ.TypeAlias = set[
typx.Literal['RUNNING_MODAL', 'CANCELLED', 'FINISHED', 'PASS_THROUGH', 'INTERFACE']
]
####################
# - Addon Types
####################
ManagedObjName = typx.Annotated[
str,
pyd.StringConstraints(
pattern=r'^[a-z_]+$',
),
]
KeymapItemDef: typ.TypeAlias = typ.Any ## TODO: Better Type
####################
# - Blender Strings
####################
PresetName = typx.Annotated[
str,
pyd.StringConstraints(
pattern=r'^[a-zA-Z0-9_]+$',
),
]

View File

@ -1,79 +1,48 @@
# ruff: noqa: I001
from blender_maxwell.contracts import (
BLColorRGBA,
BLEnumID,
BLIconSet,
BLModifierType,
BLNodeTreeInterfaceID,
ManagedObjName,
PresetName,
SocketName,
)
####################
# - String Types
####################
from .bl import SocketName
from .bl import PresetName
from .bl import ManagedObjName
from .bl import BLEnumID
from .bl import BLColorRGBA
####################
# - Icon Types
####################
from .bl_socket_desc_map import BL_SOCKET_DESCR_TYPE_MAP
from .bl_socket_types import BL_SOCKET_DESCR_ANNOT_STRING, BL_SOCKET_DIRECT_TYPE_MAP
from .category_labels import NODE_CAT_LABELS
from .category_types import NodeCategory
from .flow_events import FlowEvent
from .flow_kinds import (
ArrayFlow,
CapabilitiesFlow,
FlowKind,
InfoFlow,
LazyArrayRangeFlow,
LazyValueFlow,
ParamsFlow,
ValueFlow,
)
from .icons import Icon
####################
# - Tree Types
####################
from .trees import TreeType
####################
# - Socket Types
####################
from .socket_types import SocketType
from .socket_units import SOCKET_UNITS
from .mobj_types import ManagedObjType
from .node_types import NodeType
from .socket_colors import SOCKET_COLORS
from .socket_shapes import SOCKET_SHAPES
from .socket_types import SocketType
from .socket_units import SOCKET_UNITS
from .tree_types import TreeType
from .unit_systems import UNITS_BLENDER, UNITS_TIDY3D
from .socket_from_bl_desc import BL_SOCKET_DESCR_TYPE_MAP
from .socket_from_bl_direct import BL_SOCKET_DIRECT_TYPE_MAP
from .socket_from_bl_desc import BL_SOCKET_DESCR_ANNOT_STRING
####################
# - Node Types
####################
from .node_types import NodeType
from .node_cats import NodeCategory
from .node_cat_labels import NODE_CAT_LABELS
####################
# - Managed Obj Type
####################
from .managed_obj_type import ManagedObjType
####################
# - Data Flows
####################
from .data_flows import (
FlowKind,
CapabilitiesFlow,
ValueFlow,
ArrayFlow,
LazyValueFlow,
LazyArrayRangeFlow,
ParamsFlow,
InfoFlow,
)
from .data_flow_actions import DataFlowAction
####################
# - Export
####################
__all__ = [
'SocketName',
'PresetName',
'ManagedObjName',
'BLEnumID',
'BLColorRGBA',
'BLEnumID',
'BLIconSet',
'BLModifierType',
'BLNodeTreeInterfaceID',
'ManagedObjName',
'PresetName',
'SocketName',
'Icon',
'TreeType',
'SocketType',
@ -97,5 +66,5 @@ __all__ = [
'LazyArrayRangeFlow',
'ParamsFlow',
'InfoFlow',
'DataFlowAction',
'FlowEvent',
]

View File

@ -1,35 +0,0 @@
import pydantic as pyd
import typing_extensions as pytypes_ext
####################
# - Pure BL Types
####################
BLEnumID = pytypes_ext.Annotated[
str,
pyd.StringConstraints(
pattern=r'^[A-Z_]+$',
),
]
SocketName = pytypes_ext.Annotated[
str,
pyd.StringConstraints(
pattern=r'^[a-zA-Z0-9_]+$',
),
]
PresetName = pytypes_ext.Annotated[
str,
pyd.StringConstraints(
pattern=r'^[a-zA-Z0-9_]+$',
),
]
BLColorRGBA = tuple[float, float, float, float]
####################
# - Shared-With-BL Types
####################
ManagedObjName = pytypes_ext.Annotated[
str,
pyd.StringConstraints(
pattern=r'^[a-z_]+$',
),
]

View File

@ -1,48 +0,0 @@
import enum
import typing as typ
import typing_extensions as typx
class DataFlowAction(enum.StrEnum):
# Locking
EnableLock = 'enable_lock'
DisableLock = 'disable_lock'
# Value
OutputRequested = 'output_requested'
DataChanged = 'value_changed'
# Previewing
ShowPreview = 'show_preview'
ShowPlot = 'show_plot'
@staticmethod
def trigger_direction(action: typ.Self) -> typx.Literal['input', 'output']:
"""When a given action is triggered, all sockets/nodes/... in this direction should be recursively triggered.
Parameters:
action: The action for which to retrieve the trigger direction.
Returns:
The trigger direction, which can be used ex. in nodes to select `node.inputs` or `node.outputs`.
"""
return {
DataFlowAction.EnableLock: 'input',
DataFlowAction.DisableLock: 'input',
DataFlowAction.DataChanged: 'output',
DataFlowAction.OutputRequested: 'input',
DataFlowAction.ShowPreview: 'input',
DataFlowAction.ShowPlot: 'input',
}[action]
@staticmethod
def stop_if_no_event_methods(action: typ.Self) -> bool:
return {
DataFlowAction.EnableLock: False,
DataFlowAction.DisableLock: False,
DataFlowAction.DataChanged: True,
DataFlowAction.OutputRequested: True,
DataFlowAction.ShowPreview: False,
DataFlowAction.ShowPlot: False,
}[action]

View File

@ -0,0 +1,74 @@
import enum
import typing as typ
import typing_extensions as typx
from blender_maxwell.utils.staticproperty import staticproperty
class FlowEvent(enum.StrEnum):
"""Defines an event that can propagate through the graph (node-socket-node-...).
Contrary to `FlowKind`, a `FlowEvent` doesn't propagate any data.
Instead, it allows for dead-simple communication across direct graph connections.
The entire system is built around user-defined event handlers, which are also used internally.
See `events`.
Attributes:
EnableLock: Indicates that the node/socket should enable locking.
Locking prevents the use of the UI, including adding/removing links.
This event can lock a subset of the node tree graph.
DisableLock: Indicates that the node/socket should disable locking.
This event can unlock part of a locked subgraph.
ShowPreview: Indicates that the node/socket should enable its primary preview.
This should be used if a more specific preview-esque event doesn't apply.
ShowPlot: Indicates that the node/socket should enable its plotted preview.
This should generally be used if the node is rendering to an image, for viewing through the Blender image editor.
LinkChanged: Indicates that a link to a node/socket was added/removed.
In nodes, this is accompanied by a `socket_name` to indicate which socket it is that had its links altered.
DataChanged: Indicates that data flowing through a node/socket was altered.
In nodes, this event is accompanied by a `socket_name` or `prop_name`, to indicate which socket/property it is that was changed.
**This event is essential**, as it invalidates all input/output socket caches along its path.
"""
# Lock Events
EnableLock = enum.auto()
DisableLock = enum.auto()
# Preview Events
ShowPreview = enum.auto()
ShowPlot = enum.auto()
# Data Events
LinkChanged = enum.auto()
DataChanged = enum.auto()
# Non-Triggered Events
OutputRequested = enum.auto()
# Properties
@staticproperty
def flow_direction() -> typx.Literal['input', 'output']:
"""Describes the direction in which the event should flow.
Doesn't include `FlowEvent`s that aren't meant to be triggered:
- `OutputRequested`.
Parameters:
event: The event for which to retrieve the trigger direction.
Returns:
The trigger direction, which can be used ex. in nodes to select `node.inputs` or `node.outputs`.
"""
return {
# Lock Events
FlowEvent.EnableLock: 'input',
FlowEvent.DisableLock: 'input',
# Preview Events
FlowEvent.ShowPreview: 'input',
FlowEvent.ShowPlot: 'input',
# Data Events
FlowEvent.LinkChanged: 'output',
FlowEvent.DataChanged: 'output',
}

View File

@ -12,7 +12,6 @@ import sympy.physics.units as spu
import typing_extensions as typx
from ....utils import extra_sympy_units as spux
from ....utils import sci_constants as constants
from .socket_types import SocketType
@ -37,10 +36,8 @@ class FlowKind(enum.StrEnum):
Can be used to represent computations for which all data is not yet known, or for which just-in-time compilation can drastically increase performance.
LazyArrayRange: An object that generates an `Array` from range information (start/stop/step/spacing).
This should be used instead of `Array` whenever possible.
Param: An object providing data to complete `Lazy` data.
For example,
Info: An object providing context about other flows.
For example,
Param: A dictionary providing particular parameters for a lazy value.
Info: An dictionary providing extra context about any aspect of flow.
"""
Capabilities = enum.auto()

View File

@ -1,6 +1,6 @@
import enum
from ....utils.blender_type_enum import BlenderTypeEnum
from blender_maxwell.blender_type_enum import BlenderTypeEnum
class ManagedObjType(BlenderTypeEnum):

View File

@ -1,64 +0,0 @@
from .socket_types import SocketType as ST
SOCKET_SHAPES = {
# Basic
ST.Any: 'CIRCLE',
ST.Bool: 'CIRCLE',
ST.String: 'CIRCLE',
ST.FilePath: 'CIRCLE',
ST.Expr: 'CIRCLE',
# Number
ST.IntegerNumber: 'CIRCLE',
ST.RationalNumber: 'CIRCLE',
ST.RealNumber: 'CIRCLE',
ST.ComplexNumber: 'CIRCLE',
# Vector
ST.Integer2DVector: 'CIRCLE',
ST.Real2DVector: 'CIRCLE',
ST.Complex2DVector: 'CIRCLE',
ST.Integer3DVector: 'CIRCLE',
ST.Real3DVector: 'CIRCLE',
ST.Complex3DVector: 'CIRCLE',
# Physical
ST.PhysicalUnitSystem: 'CIRCLE',
ST.PhysicalTime: 'CIRCLE',
ST.PhysicalAngle: 'CIRCLE',
ST.PhysicalLength: 'CIRCLE',
ST.PhysicalArea: 'CIRCLE',
ST.PhysicalVolume: 'CIRCLE',
ST.PhysicalPoint2D: 'CIRCLE',
ST.PhysicalPoint3D: 'CIRCLE',
ST.PhysicalSize2D: 'CIRCLE',
ST.PhysicalSize3D: 'CIRCLE',
ST.PhysicalMass: 'CIRCLE',
ST.PhysicalSpeed: 'CIRCLE',
ST.PhysicalAccelScalar: 'CIRCLE',
ST.PhysicalForceScalar: 'CIRCLE',
ST.PhysicalAccel3D: 'CIRCLE',
ST.PhysicalForce3D: 'CIRCLE',
ST.PhysicalPol: 'CIRCLE',
ST.PhysicalFreq: 'CIRCLE',
# Blender
ST.BlenderMaterial: 'DIAMOND',
ST.BlenderObject: 'DIAMOND',
ST.BlenderCollection: 'DIAMOND',
ST.BlenderImage: 'DIAMOND',
ST.BlenderGeoNodes: 'DIAMOND',
ST.BlenderText: 'DIAMOND',
# Maxwell
ST.MaxwellSource: 'CIRCLE',
ST.MaxwellTemporalShape: 'CIRCLE',
ST.MaxwellMedium: 'CIRCLE',
ST.MaxwellMediumNonLinearity: 'CIRCLE',
ST.MaxwellStructure: 'CIRCLE',
ST.MaxwellBoundConds: 'CIRCLE',
ST.MaxwellBoundCond: 'CIRCLE',
ST.MaxwellMonitor: 'CIRCLE',
ST.MaxwellFDTDSim: 'CIRCLE',
ST.MaxwellFDTDSimData: 'CIRCLE',
ST.MaxwellSimGrid: 'CIRCLE',
ST.MaxwellSimGridAxis: 'CIRCLE',
ST.MaxwellSimDomain: 'CIRCLE',
# Tidy3D
ST.Tidy3DCloudTask: 'DIAMOND',
}

View File

@ -23,6 +23,9 @@ _MPL_CM = matplotlib.cm.get_cmap('viridis', 512)
VIRIDIS_COLORMAP = jnp.array([_MPL_CM(i)[:3] for i in range(512)])
####################
# - Image Functions
####################
def apply_colormap(normalized_data, colormap):
# Linear interpolation between colormap points
n_colors = colormap.shape[0]
@ -74,6 +77,9 @@ def rgba_image_from_2d_map(map_2d, colormap: str | None = None):
return rgba_image_from_2d_map__grayscale(map_2d)
####################
# - Managed BL Image
####################
class ManagedBLImage(base.ManagedObj):
managed_obj_type = ct.ManagedObjType.ManagedBLImage
_bl_image_name: str
@ -170,7 +176,7 @@ class ManagedBLImage(base.ManagedObj):
)
####################
# - Actions
# - Methods
####################
def bl_select(self) -> None:
"""Synchronizes the managed object to the preview, by manipulating

View File

@ -98,7 +98,7 @@ class ManagedBLMesh(base.ManagedObj):
bpy.data.meshes.remove(bl_object.data)
####################
# - Actions
# - Methods
####################
def show_preview(self) -> None:
"""Moves the managed Blender object to the preview collection.

View File

@ -12,8 +12,6 @@ from . import base
log = logger.get(__name__)
ModifierType: typ.TypeAlias = typx.Literal['NODES', 'ARRAY']
NodeTreeInterfaceID: typ.TypeAlias = str
UnitSystem: typ.TypeAlias = typ.Any
@ -33,7 +31,7 @@ class ModifierAttrsNODES(typ.TypedDict):
node_group: bpy.types.GeometryNodeTree
unit_system: UnitSystem
inputs: dict[NodeTreeInterfaceID, typ.Any]
inputs: dict[ct.BLNodeTreeInterfaceID, typ.Any]
class ModifierAttrsARRAY(typ.TypedDict):
@ -222,7 +220,7 @@ class ManagedBLModifier(base.ManagedObj):
def bl_modifier(
self,
bl_object: bpy.types.Object,
modifier_type: ModifierType,
modifier_type: ct.BLModifierType,
modifier_attrs: ModifierAttrs,
):
"""Creates a new modifier for the current `bl_object`.

View File

@ -343,7 +343,7 @@ class MaxwellSimTree(bpy.types.NodeTree):
## The link has already been removed, but we can fix that.
## If NO: Queue re-adding the link (safe since the sockets exist)
## TODO: Crash if deleting removing linked loose sockets.
consent_removal = to_socket.sync_link_removed(from_socket)
consent_removal = to_socket.allow_remove_link(from_socket)
if not consent_removal:
link_corrections['to_add'].append((from_socket, to_socket))
@ -354,12 +354,14 @@ class MaxwellSimTree(bpy.types.NodeTree):
# Retrieve Link Reference
link = self.node_link_cache.link_ptrs_as_links[link_ptr]
# Ask 'to_socket' for Consent to Remove Link
# Ask 'to_socket' for Consent to Add Link
## The link has already been added, but we can fix that.
## If NO: Queue re-adding the link (safe since the sockets exist)
consent_added = link.to_socket.sync_link_added(link)
consent_added = link.to_socket.allow_add_link(link)
if not consent_added:
link_corrections['to_remove'].append(link)
else:
link.to_socket.on_link_added(link)
# Link Corrections
## ADD: Links that 'to_socket' don't want removed.

View File

@ -121,28 +121,26 @@ class MaxwellSimNode(bpy.types.Node):
@classmethod
def _gather_event_methods(cls) -> dict[str, typ.Callable[[], None]]:
"""Gathers all methods called in response to actions/events observed by the node.
"""Gathers all methods called in response to events observed by the node.
Notes:
- 'Event methods' must have an attribute 'action_type' in order to be picked up.
- 'Event methods' must have an attribute 'action_type'.
- 'Event methods' must have an attribute 'event' in order to be picked up.
- 'Event methods' must have an attribute 'event'.
Returns:
Event methods, indexed by the action that (maybe) triggers them.
Event methods, indexed by the event that (maybe) triggers them.
"""
event_methods = [
method
for attr_name in dir(cls)
if hasattr(method := getattr(cls, attr_name), 'action_type')
and method.action_type in set(ct.DataFlowAction)
if hasattr(method := getattr(cls, attr_name), 'event')
and method.event in set(ct.FlowEvent)
]
event_methods_by_action = {
action_type: [] for action_type in set(ct.DataFlowAction)
}
event_methods_by_event = {event: [] for event in set(ct.FlowEvent)}
for method in event_methods:
event_methods_by_action[method.action_type].append(method)
event_methods_by_event[method.event].append(method)
return event_methods_by_action
return event_methods_by_event
@classmethod
def socket_set_names(cls) -> list[str]:
@ -185,7 +183,7 @@ class MaxwellSimNode(bpy.types.Node):
cls.set_prop('locked', bpy.props.BoolProperty, no_update=True, default=False)
## Event Method Callbacks
cls.event_methods_by_action = 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:
@ -483,25 +481,22 @@ class MaxwellSimNode(bpy.types.Node):
# - Event Methods
####################
@property
def _event_method_filter_by_action(self) -> dict[ct.DataFlowAction, typ.Callable]:
"""Compute a map of DataFlowActions, to a function that filters its event methods.
def _event_method_filter_by_event(self) -> dict[ct.FlowEvent, typ.Callable]:
"""Compute a map of FlowEvents, to a function that filters its event methods.
The returned filter functions are hard-coded, and must always return a `bool`.
They may use attributes of `self`, always return `True` or `False`, or something different.
Notes:
This is an internal method; you probably want `self.filtered_event_methods_by_action`.
This is an internal method; you probably want `self.filtered_event_methods_by_event`.
Returns:
The map of `ct.DataFlowAction` to a function that can determine whether any `event_method` should be run.
The map of `ct.FlowEvent` to a function that can determine whether any `event_method` should be run.
"""
return {
ct.DataFlowAction.EnableLock: lambda *_: True,
ct.DataFlowAction.DisableLock: lambda *_: True,
ct.DataFlowAction.DataChanged: lambda event_method,
socket_name,
prop_name,
_: (
ct.FlowEvent.EnableLock: lambda *_: True,
ct.FlowEvent.DisableLock: lambda *_: True,
ct.FlowEvent.DataChanged: lambda event_method, socket_name, prop_name, _: (
(
socket_name
and socket_name in event_method.callback_info.on_changed_sockets
@ -516,7 +511,7 @@ class MaxwellSimNode(bpy.types.Node):
and socket_name in self.loose_input_sockets
)
),
ct.DataFlowAction.OutputRequested: lambda output_socket_method,
ct.FlowEvent.OutputRequested: lambda output_socket_method,
output_socket_name,
_,
kind: (
@ -526,26 +521,26 @@ class MaxwellSimNode(bpy.types.Node):
== output_socket_method.callback_info.output_socket_name
)
),
ct.DataFlowAction.ShowPreview: lambda *_: True,
ct.DataFlowAction.ShowPlot: lambda *_: True,
ct.FlowEvent.ShowPreview: lambda *_: True,
ct.FlowEvent.ShowPlot: lambda *_: True,
}
def filtered_event_methods_by_action(
def filtered_event_methods_by_event(
self,
action: ct.DataFlowAction,
event: ct.FlowEvent,
_filter: tuple[ct.SocketName, str],
) -> list[typ.Callable]:
"""Return all event methods that should run, given the context provided by `_filter`.
The inclusion decision is made by the internal property `self._event_method_filter_by_action`.
The inclusion decision is made by the internal property `self._event_method_filter_by_event`.
Returns:
All `event_method`s that should run, as callable objects (they can be run using `event_method(self)`).
"""
return [
event_method
for event_method in self.event_methods_by_action[action]
if self._event_method_filter_by_action[action](event_method, *_filter)
for event_method in self.event_methods_by_event[event]
if self._event_method_filter_by_event[event](event_method, *_filter)
]
####################
@ -591,7 +586,7 @@ class MaxwellSimNode(bpy.types.Node):
raise ValueError(msg)
####################
# - Compute Action: Output Socket
# - Compute Event: Output Socket
####################
@bl_cache.keyed_cache(
exclude={'self', 'optional'},
@ -619,8 +614,8 @@ class MaxwellSimNode(bpy.types.Node):
msg = f"Can't compute nonexistent output socket name {output_socket_name}, as it's not currently active"
raise RuntimeError(msg)
output_socket_methods = self.filtered_event_methods_by_action(
ct.DataFlowAction.OutputRequested,
output_socket_methods = self.filtered_event_methods_by_event(
ct.FlowEvent.OutputRequested,
(output_socket_name, None, kind),
)
@ -636,7 +631,7 @@ class MaxwellSimNode(bpy.types.Node):
raise ValueError(msg)
####################
# - Action Trigger
# - Event Trigger
####################
def _should_recompute_output_socket(
self,
@ -657,25 +652,25 @@ class MaxwellSimNode(bpy.types.Node):
)
)
def trigger_action(
def trigger_event(
self,
action: ct.DataFlowAction,
event: ct.FlowEvent,
socket_name: ct.SocketName | None = None,
prop_name: ct.SocketName | None = None,
) -> None:
"""Recursively triggers actions/events forwards or backwards along the node tree, allowing nodes in the update path to react.
"""Recursively triggers events forwards or backwards along the node tree, allowing nodes in the update path to react.
Use `events` decorators to define methods that react to particular `ct.DataFlowAction`s.
Use `events` decorators to define methods that react to particular `ct.FlowEvent`s.
Notes:
This can be an unpredictably heavy function, depending on the node graph topology.
Parameters:
action: The action/event to report forwards/backwards along the node tree.
event: The event to report forwards/backwards along the node tree.
socket_name: The input socket that was altered, if any, in order to trigger this event.
pop_name: The property that was altered, if any, in order to trigger this event.
"""
if action == ct.DataFlowAction.DataChanged:
if event == ct.FlowEvent.DataChanged:
input_socket_name = socket_name ## Trigger direction is forwards
# Invalidate Input Socket Cache
@ -687,8 +682,8 @@ class MaxwellSimNode(bpy.types.Node):
)
# Invalidate Output Socket Cache
for output_socket_method in self.event_methods_by_action[
ct.DataFlowAction.OutputRequested
for output_socket_method in self.event_methods_by_event[
ct.FlowEvent.OutputRequested
]:
method_info = output_socket_method.callback_info
if self._should_recompute_output_socket(
@ -701,24 +696,24 @@ class MaxwellSimNode(bpy.types.Node):
# Run Triggered Event Methods
stop_propagation = False
triggered_event_methods = self.filtered_event_methods_by_action(
action, (socket_name, prop_name, None)
triggered_event_methods = self.filtered_event_methods_by_event(
event, (socket_name, prop_name, None)
)
for event_method in triggered_event_methods:
stop_propagation |= event_method.stop_propagation
event_method(self)
# Propagate Action to All Sockets in "Trigger Direction"
# Propagate Event to All Sockets in "Trigger Direction"
## The trigger chain goes node/socket/node/socket/...
if not stop_propagation:
triggered_sockets = self._bl_sockets(
direc=ct.DataFlowAction.trigger_direction(action)
direc=ct.FlowEvent.flow_direction[event]
)
for bl_socket in triggered_sockets:
bl_socket.trigger_action(action)
bl_socket.trigger_event(event)
####################
# - Property Action: On Update
# - Property Event: On Update
####################
def sync_prop(self, prop_name: str, _: bpy.types.Context) -> None:
"""Report that a particular property has changed, which may cause certain caches to regenerate.
@ -732,7 +727,7 @@ class MaxwellSimNode(bpy.types.Node):
prop_name: The name of the property that changed.
"""
if hasattr(self, prop_name):
self.trigger_action(ct.DataFlowAction.DataChanged, prop_name=prop_name)
self.trigger_event(ct.FlowEvent.DataChanged, prop_name=prop_name)
else:
msg = f'Property {prop_name} not defined on node {self}'
raise RuntimeError(msg)
@ -864,9 +859,7 @@ class MaxwellSimNode(bpy.types.Node):
## -> Compromise: Users explicitly say 'run_on_init' in @on_value_changed
for event_method in [
event_method
for event_method in self.event_methods_by_action[
ct.DataFlowAction.DataChanged
]
for event_method in self.event_methods_by_event[ct.FlowEvent.DataChanged]
if event_method.callback_info.run_on_init
]:
event_method(self)
@ -915,7 +908,7 @@ class MaxwellSimNode(bpy.types.Node):
bl_socket.is_linked and bl_socket.locked
for bl_socket in self.inputs.values()
):
self.trigger_action(ct.DataFlowAction.DisableLock)
self.trigger_event(ct.FlowEvent.DisableLock)
# Free Managed Objects
for managed_obj in self.managed_objs.values():

View File

@ -50,7 +50,7 @@ PropName: typ.TypeAlias = str
def event_decorator(
action_type: ct.DataFlowAction,
event: ct.FlowEvent,
callback_info: EventCallbackInfo | None,
stop_propagation: bool = False,
# Request Data for Callback
@ -72,10 +72,10 @@ def event_decorator(
"""Returns a decorator for a method of `MaxwellSimNode`, declaring it as able respond to events passing through a node.
Parameters:
action_type: A name describing which event the decorator should respond to.
Set to `return_method.action_type`
callback_info: A dictionary that provides the caller with additional per-`action_type` information.
This might include parameters to help select the most appropriate method(s) to respond to an event with, or actions to take after running the callback.
event: A name describing which event the decorator should respond to.
Set to `return_method.event`
callback_info: A dictionary that provides the caller with additional per-`event` information.
This might include parameters to help select the most appropriate method(s) to respond to an event with, or events to take after running the callback.
props: Set of `props` to compute, then pass to the decorated method.
stop_propagation: Whether or stop propagating the event through the graph after encountering this method.
Other methods defined on the same node will still run.
@ -93,7 +93,7 @@ def event_decorator(
A decorator, which can be applied to a method of `MaxwellSimNode`.
When a `MaxwellSimNode` subclass initializes, such a decorated method will be picked up on.
When the `action_type` action passes through the node, then `callback_info` is used to determine
When `event` passes through the node, then `callback_info` is used to determine
"""
req_params = (
{'self'}
@ -252,14 +252,14 @@ def event_decorator(
)
# Set Decorated Attributes and Return
## Fix Introspection + Documentation
## TODO: Fix Introspection + Documentation
# decorated.__name__ = method.__name__
# decorated.__module__ = method.__module__
# decorated.__qualname__ = method.__qualname__
# decorated.__doc__ = method.__doc__
decorated.__doc__ = method.__doc__
## Add Spice
decorated.action_type = action_type
decorated.event = event
decorated.callback_info = callback_info
decorated.stop_propagation = stop_propagation
@ -275,7 +275,7 @@ def on_enable_lock(
**kwargs,
):
return event_decorator(
action_type=ct.DataFlowAction.EnableLock,
event=ct.FlowEvent.EnableLock,
callback_info=None,
**kwargs,
)
@ -285,7 +285,7 @@ def on_disable_lock(
**kwargs,
):
return event_decorator(
action_type=ct.DataFlowAction.DisableLock,
event=ct.FlowEvent.DisableLock,
callback_info=None,
**kwargs,
)
@ -300,7 +300,7 @@ def on_value_changed(
**kwargs,
):
return event_decorator(
action_type=ct.DataFlowAction.DataChanged,
event=ct.FlowEvent.DataChanged,
callback_info=InfoDataChanged(
run_on_init=run_on_init,
on_changed_sockets=(
@ -320,7 +320,7 @@ def computes_output_socket(
**kwargs,
):
return event_decorator(
action_type=ct.DataFlowAction.OutputRequested,
event=ct.FlowEvent.OutputRequested,
callback_info=InfoOutputRequested(
output_socket_name=output_socket_name,
kind=kind,
@ -342,7 +342,7 @@ def on_show_preview(
**kwargs,
):
return event_decorator(
action_type=ct.DataFlowAction.ShowPreview,
event=ct.FlowEvent.ShowPreview,
callback_info={},
**kwargs,
)
@ -353,7 +353,7 @@ def on_show_plot(
**kwargs,
):
return event_decorator(
action_type=ct.DataFlowAction.ShowPlot,
event=ct.FlowEvent.ShowPlot,
callback_info={},
stop_propagation=stop_propagation,
**kwargs,

View File

@ -124,7 +124,7 @@ class ViewerNode(base.MaxwellSimNode):
)
def on_changed_plot_preview(self, props):
if self.inputs['Data'].is_linked and props['auto_plot']:
self.trigger_action(ct.DataFlowAction.ShowPlot)
self.trigger_event(ct.FlowEvent.ShowPlot)
@events.on_value_changed(
socket_name='Data',
@ -137,7 +137,7 @@ class ViewerNode(base.MaxwellSimNode):
# Remove Non-Repreviewed Previews on Close
with node_tree.repreview_all():
if self.inputs['Data'].is_linked and props['auto_3d_preview']:
self.trigger_action(ct.DataFlowAction.ShowPreview)
self.trigger_event(ct.FlowEvent.ShowPreview)
####################

View File

@ -180,7 +180,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
####################
def sync_lock_tree(self, context):
if self.lock_tree:
self.trigger_action(ct.DataFlowAction.EnableLock)
self.trigger_event(ct.FlowEvent.EnableLock)
self.locked = False
for bl_socket in self.inputs:
if bl_socket.name == 'FDTD Sim':
@ -188,7 +188,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
bl_socket.locked = False
else:
self.trigger_action(ct.DataFlowAction.DisableLock)
self.trigger_event(ct.FlowEvent.DisableLock)
self.sync_prop('lock_tree', context)

View File

@ -17,25 +17,68 @@ log = logger.get(__name__)
# - SocketDef
####################
class SocketDef(pyd.BaseModel, abc.ABC):
"""Defines everything needed to initialize a `MaxwellSimSocket`.
Used by nodes to specify which sockets to use as inputs/outputs.
Notes:
Not instantiated directly - rather, individual sockets each define a SocketDef subclass tailored to its own needs.
Attributes:
socket_type: The socket type to initialize.
"""
socket_type: ct.SocketType
@abc.abstractmethod
def init(self, bl_socket: bpy.types.NodeSocket) -> None:
"""Initializes a real Blender node socket from this socket definition."""
"""Initializes a real Blender node socket from this socket definition.
Parameters:
bl_socket: The Blender node socket to alter using data from this SocketDef.
"""
####################
# - Serialization
####################
def dump_as_msgspec(self) -> serialize.NaiveRepresentation:
"""Transforms this `SocketDef` into an object that can be natively serialized by `msgspec`.
Notes:
Makes use of `pydantic.BaseModel.model_dump()` to cast any special fields into a serializable format.
If this method is failing, check that `pydantic` can actually cast all the fields in your model.
Returns:
A particular `list`, with three elements:
1. The `serialize`-provided "Type Identifier", to differentiate this list from generic list.
2. The name of this subclass, so that the correct `SocketDef` can be reconstructed on deserialization.
3. A dictionary containing simple Python types, as cast by `pydantic`.
"""
return [serialize.TypeID.SocketDef, self.__class__.__name__, self.model_dump()]
@staticmethod
def parse_as_msgspec(obj: serialize.NaiveRepresentation) -> typ.Self:
return next(
"""Transforms an object made by `self.dump_as_msgspec()` into a subclass of `SocketDef`.
Notes:
The method presumes that the deserialized object produced by `msgspec` perfectly matches the object originally created by `self.dump_as_msgspec()`.
This is a **mostly robust** presumption, as `pydantic` attempts to be quite consistent in how to interpret types with almost identical semantics.
Still, yet-unknown edge cases may challenge these presumptions.
Returns:
A new subclass of `SocketDef`, initialized using the `model_dump()` dictionary.
"""
initialized_classes = [
subclass(**obj[2])
for subclass in SocketDef.__subclasses__()
if subclass.__name__ == obj[1]
)
]
if not initialized_classes:
msg = f'No "SocketDef" subclass found for name {obj[1]}. Please report this error'
RuntimeError(msg)
return next(initialized_classes)
####################
@ -72,7 +115,6 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
socket_color: tuple
# Options
# link_limit: int = 0
use_units: bool = False
use_prelock: bool = False
@ -132,14 +174,10 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
# Setup Style
cls.socket_color = ct.SOCKET_COLORS[cls.socket_type]
cls.socket_shape = ct.SOCKET_SHAPES[cls.socket_type]
# Setup List
cls.__annotations__['active_kind'] = bpy.props.StringProperty(
name='Active Kind',
description='The active Data Flow Kind',
default=str(ct.FlowKind.Value),
update=lambda self, _: self.sync_active_kind(),
cls.set_prop(
'active_kind', bpy.props.StringProperty, default=str(ct.FlowKind.Value)
)
# Configure Use of Units
@ -169,86 +207,116 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
)
####################
# - Action Chain
# - Event Chain
####################
def trigger_action(
def trigger_event(
self,
action: ct.DataFlowAction,
event: ct.FlowEvent,
) -> None:
"""Called whenever the socket's output value has changed.
This also invalidates any of the socket's caches.
When called on an input node, the containing node's
`trigger_action` method will be called with this socket.
`trigger_event` method will be called with this socket.
When called on a linked output node, the linked socket's
`trigger_action` method will be called.
`trigger_event` method will be called.
"""
# Forwards Chains
if action in {ct.DataFlowAction.DataChanged}:
if event in {ct.FlowEvent.DataChanged}:
## Input Socket
if not self.is_output:
self.node.trigger_action(action, socket_name=self.name)
self.node.trigger_event(event, socket_name=self.name)
## Linked Output Socket
elif self.is_output and self.is_linked:
for link in self.links:
link.to_socket.trigger_action(action)
link.to_socket.trigger_event(event)
# Backwards Chains
elif action in {
ct.DataFlowAction.EnableLock,
ct.DataFlowAction.DisableLock,
ct.DataFlowAction.OutputRequested,
ct.DataFlowAction.DataChanged,
ct.DataFlowAction.ShowPreview,
ct.DataFlowAction.ShowPlot,
elif event in {
ct.FlowEvent.EnableLock,
ct.FlowEvent.DisableLock,
ct.FlowEvent.OutputRequested,
ct.FlowEvent.DataChanged,
ct.FlowEvent.ShowPreview,
ct.FlowEvent.ShowPlot,
}:
if action == ct.DataFlowAction.EnableLock:
if event == ct.FlowEvent.EnableLock:
self.locked = True
if action == ct.DataFlowAction.DisableLock:
if event == ct.FlowEvent.DisableLock:
self.locked = False
## Output Socket
if self.is_output:
self.node.trigger_action(action, socket_name=self.name)
self.node.trigger_event(event, socket_name=self.name)
## Linked Input Socket
elif not self.is_output and self.is_linked:
for link in self.links:
link.from_socket.trigger_action(action)
link.from_socket.trigger_event(event)
####################
# - Action Chain: Event Handlers
# - Event Chain: Event Handlers
####################
def sync_active_kind(self):
"""Called when the active data flow kind of the socket changes.
def sync_prop(self, prop_name: str, _: bpy.types.Context) -> None:
"""Called when a property has been updated.
Alters the shape of the socket to match the active FlowKind, then triggers `ct.DataFlowAction.DataChanged` on the current socket.
Contrary to `node.on_prop_changed()`, socket-specific callbacks are baked into this function:
- **Active Kind** (`active_kind`): Sets the socket shape to reflect the active `FlowKind`.
Attributes:
prop_name: The name of the property that was changed.
"""
self.display_shape = {
ct.FlowKind.Value: ct.SOCKET_SHAPES[self.socket_type],
ct.FlowKind.ValueArray: 'SQUARE',
ct.FlowKind.ValueSpectrum: 'SQUARE',
ct.FlowKind.LazyValue: ct.SOCKET_SHAPES[self.socket_type],
ct.FlowKind.LazyValueRange: 'SQUARE',
ct.FlowKind.LazyValueSpectrum: 'SQUARE',
}[self.active_kind] + ('_DOT' if self.use_units else '')
# Property: Active Kind
if prop_name == 'active_kind':
self.display_shape(
'SQUARE'
if self.active_kind
in {ct.FlowKind.LazyValue, ct.FlowKind.LazyValueRange}
else 'CIRCLE'
) + ('_DOT' if self.use_units else '')
self.trigger_action(ct.DataFlowAction.DataChanged)
# Valid Properties
elif hasattr(self, prop_name):
self.trigger_event(ct.FlowEvent.DataChanged)
def sync_prop(self, prop_name: str, _: bpy.types.Context):
"""Called when a property has been updated."""
if hasattr(self, prop_name):
self.trigger_action(ct.DataFlowAction.DataChanged)
# Undefined Properties
else:
msg = f'Property {prop_name} not defined on socket {self}'
raise RuntimeError(msg)
def sync_link_added(self, link) -> bool:
"""Called when a link has been added to this (input) socket."""
def allow_add_link(self, link: bpy.types.NodeLink) -> bool:
"""Called to ask whether a link may be added to this (input) socket.
- **Locked**: Locked sockets may not have links added.
- **Capabilities**: Capabilities of both sockets participating in the link must be compatible.
Notes:
In practice, the link in question has already been added.
This function determines **whether the new link should be instantly removed** - if so, the removal producing the _practical effect_ of the link "not being added" at all.
Attributes:
link: The node link that was already added, whose continued existance is in question.
Returns:
Whether or not consent is given to add the link.
In practice, the link will simply remain if consent is given.
If consent is not given, the new link will be removed.
Raises:
RuntimeError: If this socket is an output socket.
"""
# Output Socket Check
if self.is_output:
msg = 'Tried to ask output socket for consent to add link'
raise RuntimeError(msg)
# Lock Check
if self.locked:
log.error(
'Attempted to link output socket "%s" (%s) to input socket "%s" (%s), but input socket is locked',
@ -258,36 +326,66 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
self.capabilities,
)
return False
# Capability Check
if not link.from_socket.capabilities.is_compatible_with(self.capabilities):
log.error(
'Attempted to link output socket "%s" (%s) to input socket "%s" (%s), but capabilities are invalid',
'Attempted to link output socket "%s" (%s) to input socket "%s" (%s), but capabilities are incompatible',
link.from_socket.bl_label,
link.from_socket.capabilities,
self.bl_label,
self.capabilities,
)
return False
if self.is_output:
msg = "Tried to sync 'link add' on output socket"
raise RuntimeError(msg)
self.trigger_action(ct.DataFlowAction.DataChanged)
return True
def sync_link_removed(self, from_socket) -> bool:
"""Called when a link has been removed from this (input) socket.
def on_link_added(self, link: bpy.types.NodeLink) -> None:
"""Triggers a `ct.FlowEvent.LinkChanged` event on link add.
Returns a bool, whether or not the socket consents to the link change.
Attributes:
link: The node link that was added.
Currently unused.
Returns:
Whether or not consent is given to add the link.
In practice, the link will simply remain if consent is given.
If consent is not given, the new link will be removed.
"""
if self.locked:
return False
self.trigger_event(ct.FlowEvent.DataChanged)
def allow_remove_link(self, from_socket: bpy.types.NodeSocket) -> bool:
"""Called to ask whether a link may be removed from this `to_socket`.
- **Locked**: Locked sockets may not have links removed.
- **Capabilities**: Capabilities of both sockets participating in the link must be compatible.
Notes:
In practice, the link in question has already been removed.
Therefore, only the `from_socket` that the link _was_ attached to is provided.
Attributes:
from_socket: The node socket that was attached to before link removal.
Currently unused.
Returns:
Whether or not consent is given to remove the link.
If so, nothing will happen.
If consent is not given, a new link will be added that is identical to the old one.
Raises:
RuntimeError: If this socket is an output socket.
"""
# Output Socket Check
if self.is_output:
msg = "Tried to sync 'link add' on output socket"
raise RuntimeError(msg)
self.trigger_action(ct.DataFlowAction.DataChanged)
# Lock Check
if self.locked:
return False
self.trigger_event(ct.FlowEvent.DataChanged)
return True
####################

View File

@ -18,7 +18,7 @@ class MaxwellMonitorSocketDef(base.SocketDef):
def init(self, bl_socket: MaxwellMonitorBLSocket) -> None:
if self.is_list:
bl_socket.active_kind = ct.FlowKind.ValueArray
bl_socket.active_kind = ct.FlowKind.Array
####################

View File

@ -0,0 +1,24 @@
class staticproperty(property): # noqa: N801
"""A read-only variant of `@property` that is entirely static, for use in specific situations.
The decorated method must take no arguments whatsoever, including `self`/`cls`.
Examples:
Use as usual:
```python
class Spam:
@staticproperty
def eggs():
return 10
assert Spam.eggs == 10
```
"""
def __get__(self, *_):
"""Overridden getter that ignores instance and owner, and just returns the value of the evaluated (static) method.
Returns:
The evaluated value of the static method that was decorated.
"""
return self.fget()