From c6e00dcd7b4dd805140f1737fe4e135f8ee45dbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofus=20Albert=20H=C3=B8gsbro=20Rose?= Date: Wed, 17 Apr 2024 18:14:14 +0200 Subject: [PATCH] refactor: Fixes and movement. --- FUTURE.md | 2 +- src/blender_maxwell/contracts.py | 77 +++++++ .../maxwell_sim_nodes/contracts/__init__.py | 107 +++------ .../maxwell_sim_nodes/contracts/bl.py | 35 --- ..._from_bl_desc.py => bl_socket_desc_map.py} | 0 ...t_from_bl_direct.py => bl_socket_types.py} | 0 ...{node_cat_labels.py => category_labels.py} | 0 .../{node_cats.py => category_types.py} | 0 .../contracts/data_flow_actions.py | 48 ---- .../contracts/flow_events.py | 74 ++++++ .../{data_flows.py => flow_kinds.py} | 7 +- .../{managed_obj_type.py => mobj_types.py} | 2 +- .../contracts/socket_shapes.py | 64 ----- .../contracts/{trees.py => tree_types.py} | 0 .../managed_objs/managed_bl_image.py | 8 +- .../managed_objs/managed_bl_mesh.py | 2 +- .../managed_objs/managed_bl_modifier.py | 6 +- .../node_trees/maxwell_sim_nodes/node_tree.py | 8 +- .../maxwell_sim_nodes/nodes/base.py | 99 ++++---- .../maxwell_sim_nodes/nodes/events.py | 30 +-- .../maxwell_sim_nodes/nodes/outputs/viewer.py | 4 +- .../web_exporters/tidy3d_web_exporter.py | 4 +- .../maxwell_sim_nodes/sockets/base.py | 218 +++++++++++++----- .../sockets/maxwell/monitor.py | 2 +- src/blender_maxwell/utils/staticproperty.py | 24 ++ 25 files changed, 456 insertions(+), 365 deletions(-) create mode 100644 src/blender_maxwell/contracts.py delete mode 100644 src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl.py rename src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/{socket_from_bl_desc.py => bl_socket_desc_map.py} (100%) rename src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/{socket_from_bl_direct.py => bl_socket_types.py} (100%) rename src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/{node_cat_labels.py => category_labels.py} (100%) rename src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/{node_cats.py => category_types.py} (100%) delete mode 100644 src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/data_flow_actions.py create mode 100644 src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_events.py rename src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/{data_flows.py => flow_kinds.py} (97%) rename src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/{managed_obj_type.py => mobj_types.py} (79%) delete mode 100644 src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_shapes.py rename src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/{trees.py => tree_types.py} (100%) create mode 100644 src/blender_maxwell/utils/staticproperty.py diff --git a/FUTURE.md b/FUTURE.md index 855b826..aaf7834 100644 --- a/FUTURE.md +++ b/FUTURE.md @@ -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. diff --git a/src/blender_maxwell/contracts.py b/src/blender_maxwell/contracts.py new file mode 100644 index 0000000..e8140f6 --- /dev/null +++ b/src/blender_maxwell/contracts.py @@ -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_]+$', + ), +] diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/__init__.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/__init__.py index 6e2aefd..ded208b 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/__init__.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/__init__.py @@ -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', ] diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl.py deleted file mode 100644 index 66a5cae..0000000 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl.py +++ /dev/null @@ -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_]+$', - ), -] diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_from_bl_desc.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl_socket_desc_map.py similarity index 100% rename from src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_from_bl_desc.py rename to src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl_socket_desc_map.py diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_from_bl_direct.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl_socket_types.py similarity index 100% rename from src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_from_bl_direct.py rename to src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl_socket_types.py diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_cat_labels.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/category_labels.py similarity index 100% rename from src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_cat_labels.py rename to src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/category_labels.py diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_cats.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/category_types.py similarity index 100% rename from src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_cats.py rename to src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/category_types.py diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/data_flow_actions.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/data_flow_actions.py deleted file mode 100644 index 0413c9e..0000000 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/data_flow_actions.py +++ /dev/null @@ -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] diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_events.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_events.py new file mode 100644 index 0000000..88194dc --- /dev/null +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_events.py @@ -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', + } diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/data_flows.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds.py similarity index 97% rename from src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/data_flows.py rename to src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds.py index 016c60b..6cd4757 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/data_flows.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds.py @@ -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() diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/managed_obj_type.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/mobj_types.py similarity index 79% rename from src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/managed_obj_type.py rename to src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/mobj_types.py index 9f303cc..5fc3cd9 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/managed_obj_type.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/mobj_types.py @@ -1,6 +1,6 @@ import enum -from ....utils.blender_type_enum import BlenderTypeEnum +from blender_maxwell.blender_type_enum import BlenderTypeEnum class ManagedObjType(BlenderTypeEnum): diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_shapes.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_shapes.py deleted file mode 100644 index a2f10a0..0000000 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_shapes.py +++ /dev/null @@ -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', -} diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/trees.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/tree_types.py similarity index 100% rename from src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/trees.py rename to src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/tree_types.py diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_image.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_image.py index 2bea3fd..bed4e6e 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_image.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_image.py @@ -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 diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_mesh.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_mesh.py index 5244295..09bb147 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_mesh.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_mesh.py @@ -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. diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_modifier.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_modifier.py index e5a4edd..dbadd97 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_modifier.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_modifier.py @@ -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`. diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/node_tree.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/node_tree.py index 0750346..04d3df7 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/node_tree.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/node_tree.py @@ -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. diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py index 18193fc..81243da 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py @@ -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(): diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/events.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/events.py index b047f78..bbea4f8 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/events.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/events.py @@ -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, diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py index 807ac66..b3d1c4c 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py @@ -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) #################### diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/web_exporters/tidy3d_web_exporter.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/web_exporters/tidy3d_web_exporter.py index cd0b2cc..eb1ecb4 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/web_exporters/tidy3d_web_exporter.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/web_exporters/tidy3d_web_exporter.py @@ -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) diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/base.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/base.py index b4b6e36..85013f8 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/base.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/base.py @@ -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 #################### diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/monitor.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/monitor.py index 751fee5..c35e1a9 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/monitor.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/monitor.py @@ -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 #################### diff --git a/src/blender_maxwell/utils/staticproperty.py b/src/blender_maxwell/utils/staticproperty.py new file mode 100644 index 0000000..9eafbc6 --- /dev/null +++ b/src/blender_maxwell/utils/staticproperty.py @@ -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()