refactor: More changes to docs/layout

main
Sofus Albert Høgsbro Rose 2024-04-18 08:42:53 +02:00
parent 8dece384ad
commit ff5d71aeff
Signed by: so-rose
GPG Key ID: AD901CB0F3701434
6 changed files with 287 additions and 153 deletions

View File

@ -108,7 +108,25 @@ class ArrayFlow:
"""
values: jax.Array
unit: spu.Quantity | None
unit: spu.Quantity | None = None
def correct_unit(self, real_unit: spu.Quantity) -> typ.Self:
if self.unit is not None:
return ArrayFlow(values=self.values, unit=real_unit)
msg = f'Tried to correct unit of unitless LazyDataValueRange "{real_unit}"'
raise ValueError(msg)
def rescale_to_unit(self, unit: spu.Quantity) -> typ.Self:
if self.unit is not None:
return ArrayFlow(
values=float(spux.scaling_factor(self.unit, unit)) * self.values,
unit=unit,
)
## TODO: Is this scaling numerically stable?
msg = f'Tried to rescale unitless LazyDataValueRange to unit {unit}'
raise ValueError(msg)
####################
@ -213,14 +231,26 @@ class LazyArrayRangeFlow:
steps: int
scaling: typx.Literal['lin', 'geom', 'log'] = 'lin'
has_unit: bool = False
unit: spu.Quantity = False
unit: spu.Quantity | None = False
def rescale_to_unit(self, unit: spu.Quantity) -> typ.Self:
if self.has_unit:
def correct_unit(self, real_unit: spu.Quantity) -> typ.Self:
if self.unit is not None:
return LazyArrayRangeFlow(
symbols=self.symbols,
unit=real_unit,
start=self.start,
stop=self.stop,
steps=self.steps,
scaling=self.scaling,
)
msg = f'Tried to correct unit of unitless LazyDataValueRange "{real_unit}"'
raise ValueError(msg)
def rescale_to_unit(self, unit: spu.Quantity) -> typ.Self:
if self.unit is not None:
return LazyArrayRangeFlow(
symbols=self.symbols,
has_unit=self.has_unit,
unit=unit,
start=spu.convert_to(self.start, unit),
stop=spu.convert_to(self.stop, unit),
@ -239,7 +269,6 @@ class LazyArrayRangeFlow:
"""Call a function on both bounds (start and stop), creating a new `LazyDataValueRange`."""
return LazyArrayRangeFlow(
symbols=self.symbols,
has_unit=self.has_unit,
unit=self.unit,
start=spu.convert_to(
bound_cb(self.start if not reverse else self.stop), self.unit
@ -255,7 +284,7 @@ class LazyArrayRangeFlow:
self, symbol_values: dict[sp.Symbol, ValueFlow] = MappingProxyType({})
) -> ArrayFlow:
# Realize Symbols
if not self.has_unit:
if self.unit is None:
start = spux.sympy_to_python(self.start.subs(symbol_values))
stop = spux.sympy_to_python(self.stop.subs(symbol_values))
else:

View File

@ -347,6 +347,8 @@ class MaxwellSimTree(bpy.types.NodeTree):
consent_removal = to_socket.allow_remove_link(from_socket)
if not consent_removal:
link_corrections['to_add'].append((from_socket, to_socket))
else:
to_socket.on_link_removed(from_socket)
# Ensure Removal of Socket PTRs, PTRs->REFs
self.node_link_cache.remove_sockets_by_link_ptr(link_ptr)

View File

@ -192,7 +192,6 @@ class MaxwellSimNode(bpy.types.Node):
'active_socket_set',
bpy.props.EnumProperty,
name='Active Socket Set',
description='Selector of active sockets',
items=[
(socket_set_name, socket_set_name, socket_set_name)
for socket_set_name in socket_set_names
@ -742,13 +741,13 @@ class MaxwellSimNode(bpy.types.Node):
) -> None:
"""Draws the UI of the node.
- Locked (`self.locked`): The UI will be unusable.
- Active Preset (`self.active_preset`): The preset selector will display.
- Active Socket Set (`self.active_socket_set`): The socket set selector will display.
- Use Sim Node Name (`self.use_sim_node_name`): The "Sim Node Name will display.
- Properties (`self.draw_props()`): Node properties will display.
- Operators (`self.draw_operators()`): Node operators will display.
- Info (`self.draw_operators()`): Node information will display.
- **Locked** (`self.locked`): The UI will be unusable.
- **Active Preset** (`self.active_preset`): The preset selector will display.
- **Active Socket Set** (`self.active_socket_set`): The socket set selector will display.
- **Use Sim Node Name** (`self.use_sim_node_name`): The `self.sim_node_name` will display.
- **Properties**: Node properties will display, if `self.draw_props()` is overridden.
- **Operators**: Node operators will display, if `self.draw_operators()` is overridden.
- **Info**: Node information will display, if `self.draw_info()` is overridden.
Parameters:
context: The current Blender context.

View File

@ -125,6 +125,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
####################
# - Initialization
####################
## TODO: Common implementation of this for both sockets and nodes - perhaps a BLInstance base class?
@classmethod
def set_prop(
cls,
@ -183,103 +184,84 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
# Configure Use of Units
if cls.use_units:
# Set Shape :)
cls.socket_shape += '_DOT'
if not (socket_units := ct.SOCKET_UNITS.get(cls.socket_type)):
msg = 'Tried to `use_units` on {cls.bl_idname} socket, but `SocketType` has no units defined in `contracts.SOCKET_UNITS`'
msg = f'Tried to define "use_units" on socket {cls.bl_label} socket, but there is no unit for {cls.socket_type} defined in "contracts.SOCKET_UNITS"'
raise RuntimeError(msg)
# Current Unit
cls.__annotations__['active_unit'] = bpy.props.EnumProperty(
cls.set_prop(
'active_unit',
bpy.props.EnumProperty,
name='Unit',
description='Choose a unit',
items=[
(unit_name, str(unit_value), str(unit_value))
for unit_name, unit_value in socket_units['values'].items()
],
default=socket_units['default'],
update=lambda self, _: self.sync_unit_change(),
)
# Previous Unit (for conversion)
cls.__annotations__['prev_active_unit'] = bpy.props.StringProperty(
cls.set_prop(
'prev_active_unit',
bpy.props.StringProperty,
default=socket_units['default'],
)
####################
# - Event Chain
# - Property Event: On Update
####################
def trigger_event(
self,
event: ct.FlowEvent,
) -> None:
"""Called whenever the socket's output value has changed.
def _on_active_kind_changed(self) -> None:
"""Matches the display shape to the active `FlowKind`.
This also invalidates any of the socket's caches.
When called on an input node, the containing node's
`trigger_event` method will be called with this socket.
When called on a linked output node, the linked socket's
`trigger_event` method will be called.
Notes:
Called by `self.on_prop_changed()` when `self.active_kind` was changed.
"""
# Forwards Chains
if event in {ct.FlowEvent.DataChanged}:
## Input Socket
if not self.is_output:
self.node.trigger_event(event, socket_name=self.name)
self.display_shape = (
'SQUARE'
if self.active_kind in {ct.FlowKind.LazyValue, ct.FlowKind.LazyValueRange}
else 'CIRCLE'
) + ('_DOT' if self.use_units else '')
## Linked Output Socket
elif self.is_output and self.is_linked:
for link in self.links:
link.to_socket.trigger_event(event)
def _on_unit_changed(self) -> None:
"""Synchronizes the `FlowKind` data to the newly set unit.
# Backwards Chains
elif event in {
ct.FlowEvent.EnableLock,
ct.FlowEvent.DisableLock,
ct.FlowEvent.OutputRequested,
ct.FlowEvent.DataChanged,
ct.FlowEvent.ShowPreview,
ct.FlowEvent.ShowPlot,
}:
if event == ct.FlowEvent.EnableLock:
self.locked = True
When a new unit is set, the internal ex. floating point properties become out of sync.
This function applies a rescaling operation based on the factor between the previous unit (`self.prev_unit`) and the new unit `(self.unit)`.
if event == ct.FlowEvent.DisableLock:
self.locked = False
- **Value**: Retrieve the value (with incorrect new unit), exchange the new unit for the old unit, and assign it back.
- **Array**: Replace the internal unit with the old (correct) unit, and rescale all values in the array to the new unit.
## Output Socket
if self.is_output:
self.node.trigger_event(event, socket_name=self.name)
Notes:
Called by `self.on_prop_changed()` when `self.active_unit` is changed.
## Linked Input Socket
elif not self.is_output and self.is_linked:
for link in self.links:
link.from_socket.trigger_event(event)
This allows for a unit-scaling operation **without needing to know anything about the data representation** (at the cost of performance).
"""
if self.active_kind == ct.FlowKind.Value:
self.value = self.value / self.unit * self.prev_unit
elif self.active_kind in [ct.FlowKind.Array, ct.FlowKind.LazyArrayRange]:
self.lazy_value_range = self.lazy_value_range.correct_unit(
self.prev_unit
).rescale_to_unit(self.unit)
else:
msg = f'Active kind {self.active_kind} has no way of scaling units (from {self.prev_active_unit} to {self.active_unit}). Please check the node definition'
raise RuntimeError(msg)
self.prev_active_unit = self.active_unit
####################
# - Event Chain: Event Handlers
####################
def sync_prop(self, prop_name: str, _: bpy.types.Context) -> None:
"""Called when a property has been updated.
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`.
- **Active Kind** (`self.active_kind`): Sets the socket shape to reflect the active `FlowKind`.
- **Unit** (`self.unit`): Corrects the internal `FlowKind` representation to match the new unit.
Attributes:
prop_name: The name of the property that was changed.
"""
# 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._on_active_kind_changed()
elif prop_name == 'unit':
self._on_unit_changed()
# Valid Properties
elif hasattr(self, prop_name):
@ -290,6 +272,9 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
msg = f'Property {prop_name} not defined on socket {self}'
raise RuntimeError(msg)
####################
# - Link Event: Consent / On Change
####################
def allow_add_link(self, link: bpy.types.NodeLink) -> bool:
"""Called to ask whether a link may be added to this (input) socket.
@ -300,7 +285,6 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
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.
@ -341,21 +325,19 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
return True
def on_link_added(self, link: bpy.types.NodeLink) -> None:
"""Triggers a `ct.FlowEvent.LinkChanged` event on link add.
def on_link_added(self, link: bpy.types.NodeLink) -> None: # noqa: ARG002
"""Triggers a `ct.FlowEvent.LinkChanged` event when a link is added.
Notes:
Called by the node tree, generally (but not guaranteed) after `self.allow_add_link()` has given consent to add the link.
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.
"""
self.trigger_event(ct.FlowEvent.DataChanged)
self.trigger_event(ct.FlowEvent.LinkChanged)
def allow_remove_link(self, from_socket: bpy.types.NodeSocket) -> bool:
def allow_remove_link(self, from_socket: bpy.types.NodeSocket) -> bool: # noqa: ARG002
"""Called to ask whether a link may be removed from this `to_socket`.
- **Locked**: Locked sockets may not have links removed.
@ -386,9 +368,67 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
if self.locked:
return False
self.trigger_event(ct.FlowEvent.DataChanged)
return True
def on_link_removed(self, from_socket: bpy.types.NodeSocket) -> None: # noqa: ARG002
"""Triggers a `ct.FlowEvent.LinkChanged` event when a link is removed.
Notes:
Called by the node tree, generally (but not guaranteed) after `self.allow_remove_link()` has given consent to remove the link.
Attributes:
from_socket: The node socket that was attached to before link removal.
Currently unused.
"""
self.trigger_event(ct.FlowEvent.LinkChanged)
####################
# - Event Chain
####################
def trigger_event(
self,
event: ct.FlowEvent,
) -> None:
"""Recursively triggers an event along the node tree, depending on whether the socket is an input or output socket.
Notes:
This can be an unpredictably heavy function, depending on the node graph topology.
Parameters:
event: The event to report along the node tree.
The value of `ct.FlowEvent.flow_direction[event]` must match either `input` or `output`, depending on whether the socket is input/output.
"""
flow_direction = ct.FlowEvent.flow_direction[event]
# Input Socket | Input Flow
if not self.is_output and flow_direction == 'input':
if event in [ct.FlowEvent.EnableLock, ct.FlowEvent.DisableLock]:
self.locked = event == ct.FlowEvent.EnableLock
for link in self.links:
link.from_socket.trigger_event(event)
# Input Socket | Output Flow
if not self.is_output and flow_direction == 'output':
## THIS IS A WORKAROUND (bc Node only understands DataChanged)
## TODO: Handle LinkChanged on the node.
if event == ct.FlowEvent.LinkChanged:
self.node.trigger_event(ct.FlowEvent.DataChanged, socket_name=self.name)
self.node.trigger_event(event, socket_name=self.name)
# Output Socket | Input Flow
if self.is_output and flow_direction == 'input':
if event in [ct.FlowEvent.EnableLock, ct.FlowEvent.DisableLock]:
self.locked = event == ct.FlowEvent.EnableLock
self.node.trigger_event(event, socket_name=self.name)
# Output Socket | Output Flow
if self.is_output and flow_direction == 'output':
for link in self.links:
link.to_socket.trigger_event(event)
####################
# - Data Chain
####################
@ -437,7 +477,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
def lazy_array_range(self, value: tuple[ct.DataValue, ct.DataValue, int]) -> None:
raise NotImplementedError
# LazyArrayRange
# Param
@property
def param(self) -> ct.ParamsFlow:
raise NotImplementedError
@ -446,6 +486,15 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
def param(self, value: tuple[ct.DataValue, ct.DataValue, int]) -> None:
raise NotImplementedError
# Info
@property
def info(self) -> ct.ParamsFlow:
raise NotImplementedError
@info.setter
def info(self, value: tuple[ct.DataValue, ct.DataValue, int]) -> None:
raise NotImplementedError
####################
# - Data Chain Computation
####################
@ -546,41 +595,19 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
self.active_unit = matching_unit_names[0]
def sync_unit_change(self) -> None:
"""In unit-aware sockets, the internal `value()` property multiplies the Blender property value by the current active unit.
When the unit is changed, `value()` will display the old scalar with the new unit.
To fix this, we need to update the scalar to use the new unit.
Can be overridden if more specific logic is required.
"""
if self.active_kind == ct.FlowKind.Value:
self.value = self.value / self.unit * self.prev_unit
elif self.active_kind == ct.FlowKind.LazyValueRange:
lazy_value_range = self.lazy_value_range
self.lazy_value_range = (
lazy_value_range.start / self.unit * self.prev_unit,
lazy_value_range.stop / self.unit * self.prev_unit,
lazy_value_range.steps,
)
self.prev_active_unit = self.active_unit
####################
# - Style
# - Theme
####################
def draw_color(
self,
context: bpy.types.Context,
node: bpy.types.Node,
) -> ct.BLColorRGBA:
"""Color of the socket icon, when embedded in a node."""
return self.socket_color
@classmethod
def draw_color_simple(cls) -> ct.BLColorRGBA:
"""Fallback color of the socket icon (ex.when not embedded in a node)."""
"""Sets the socket's color to `cls.socket_color`.
Notes:
Blender calls this method to determine the socket color.
Returns:
A Blender-compatible RGBA value, with no explicit color space.
"""
return cls.socket_color
####################
@ -593,7 +620,17 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
node: bpy.types.Node,
text: str,
) -> None:
"""Called by Blender to draw the socket UI."""
"""Draw the socket UI.
- **Input Socket**: Will use `self.draw_input()`.
- **Output Socket**: Will use `self.draw_output()`.
Parameters:
context: The current Blender context.
layout: Target for defining UI elements.
node: The node within which the socket is embedded.
text: The socket's name in the UI.
"""
if self.is_output:
self.draw_output(context, layout, node, text)
else:
@ -606,8 +643,21 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
node: bpy.types.Node,
text: str,
) -> None:
pass
"""Draw the "prelock" UI, which is usable regardless of the `self.locked` state.
Notes:
If a "prelock" UI is needed by a socket, it should set `self.use_prelock` and override this method.
Parameters:
context: The current Blender context.
col: Target for defining UI elements.
node: The node within which the socket is embedded.
text: The socket's name in the UI.
"""
####################
# - UI: Input / Output Socket
####################
def draw_input(
self,
context: bpy.types.Context,
@ -615,7 +665,23 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
node: bpy.types.Node,
text: str,
) -> None:
"""Draws the socket UI, when the socket is an input socket."""
"""Draw the UI of the input socket.
- **Locked** (`self.locked`): The UI will be unusable.
- **Linked** (`self.is_linked`): Only the socket label will display.
- **Use Units** (`self.use_units`): The currently active unit will display as a dropdown menu.
- **Use Prelock** (`self.use_prelock`): The "prelock" UI drawn with `self.draw_prelock()`, which shows **regardless of `self.locked`**.
- **FlowKind**: The `FlowKind`-specific UI corresponding to the current `self.active_kind`.
Notes:
Shouldn't be overridden.
Parameters:
context: The current Blender context.
layout: Target for defining UI elements.
node: The node within which the socket is embedded.
text: The socket's name in the UI.
"""
col = layout.column(align=False)
# Label Row
@ -653,25 +719,33 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
elif self.locked:
row.enabled = False
# Data Column(s)
# FlowKind Column(s)
col = row.column(align=True)
{
ct.FlowKind.Value: self.draw_value,
ct.FlowKind.ValueArray: self.draw_value_array,
ct.FlowKind.ValueSpectrum: self.draw_value_spectrum,
ct.FlowKind.Array: self.draw_value_array,
ct.FlowKind.LazyValue: self.draw_lazy_value,
ct.FlowKind.LazyValueRange: self.draw_lazy_value_range,
ct.FlowKind.LazyValueSpectrum: self.draw_lazy_value_spectrum,
}[self.active_kind](col)
def draw_output(
self,
context: bpy.types.Context,
context: bpy.types.Context, # noqa: ARG002
layout: bpy.types.UILayout,
node: bpy.types.Node,
node: bpy.types.Node, # noqa: ARG002
text: str,
) -> None:
"""Draws the socket UI, when the socket is an output socket."""
"""Draw the label text on the output socket.
Notes:
Shouldn't be overridden.
Parameters:
context: The current Blender context.
layout: Target for defining UI elements.
node: The node within which the socket is embedded.
text: The socket's name in the UI.
"""
layout.label(text=text)
####################
@ -682,29 +756,53 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
row: bpy.types.UILayout,
text: str,
) -> None:
"""Called to draw the label row (same height as socket shape).
"""Draw the label row, which is at the same height as the socket shape.
Can be overridden.
Notes:
Can be overriden by individual socket classes, if they need to alter the way that the label row is drawn.
Parameters:
row: Target for defining UI elements.
text: The socket's name in the UI.
"""
row.label(text=text)
####################
# - FlowKind draw() Methods
####################
def draw_value(self, col: bpy.types.UILayout) -> None:
pass
"""Draws the socket value on its own line.
def draw_value_array(self, col: bpy.types.UILayout) -> None:
pass
Notes:
Should be overriden by individual socket classes, if they have an editable `FlowKind.Value`.
def draw_value_spectrum(self, col: bpy.types.UILayout) -> None:
pass
Parameters:
col: Target for defining UI elements.
"""
def draw_array(self, col: bpy.types.UILayout) -> None:
"""Draws the socket array on its own line.
Notes:
Should be overriden by individual socket classes, if they have an editable `FlowKind.Array`.
Parameters:
col: Target for defining UI elements.
"""
def draw_lazy_value(self, col: bpy.types.UILayout) -> None:
pass
"""Draws the socket lazy value on its own line.
def draw_lazy_value_range(self, col: bpy.types.UILayout) -> None:
pass
Notes:
Should be overriden by individual socket classes, if they have an editable `FlowKind.LazyValue`.
def draw_lazy_value_spectrum(self, col: bpy.types.UILayout) -> None:
pass
Parameters:
col: Target for defining UI elements.
"""
def draw_lazy_array_range(self, col: bpy.types.UILayout) -> None:
"""Draws the socket lazy array range on its own line.
Notes:
Should be overriden by individual socket classes, if they have an editable `FlowKind.LazyArrayRange`.
Parameters:
col: Target for defining UI elements.
"""

View File

@ -4,7 +4,8 @@ import os
import sys
from pathlib import Path
from ... import info
import blender_maxwell.contracts as ct
from . import simple_logger
log = simple_logger.get(__name__)
@ -30,8 +31,8 @@ def importable_addon_deps(path_deps: Path):
yield
finally:
pass
#log.info('Removing Path from sys.path: %s', str(os_path))
#sys.path.remove(os_path)
# log.info('Removing Path from sys.path: %s', str(os_path))
# sys.path.remove(os_path)
else:
try:
yield
@ -43,7 +44,7 @@ def importable_addon_deps(path_deps: Path):
def syspath_from_bpy_prefs() -> bool:
import bpy
addon_prefs = bpy.context.preferences.addons[info.ADDON_NAME].preferences
addon_prefs = bpy.context.preferences.addons[ct.addon.NAME].preferences
if hasattr(addon_prefs, 'path_addon_pydeps'):
log.info('Retrieved PyDeps Path from Addon Prefs')
path_pydeps = addon_prefs.path_addon_pydeps
@ -67,7 +68,7 @@ def _check_pydeps(
"""
def conform_pypi_package_deplock(deplock: str):
"""Conforms a <package>==<version> de-lock to match if pypi considers them the same (PyPi is case-insensitive and considers -/_ to be the same)
"""Conforms a <package>==<version> de-lock to match if pypi considers them the same (PyPi is case-insensitive and considers -/_ to be the same).
See <https://peps.python.org/pep-0426/#name>
"""
@ -127,7 +128,7 @@ def check_pydeps(path_deps: Path):
global DEPS_OK # noqa: PLW0603
global DEPS_ISSUES # noqa: PLW0603
if len(issues := _check_pydeps(info.PATH_REQS, path_deps)) > 0:
if len(issues := _check_pydeps(ct.addon.PATH_REQS, path_deps)) > 0:
log.info('PyDeps Check Failed')
log.debug('%s', ', '.join(issues))

View File

@ -105,6 +105,11 @@ def parse_abbrev_symbols_to_units(expr: sp.Basic) -> sp.Basic:
####################
# - Units <-> Scalars
####################
def scaling_factor(unit_from: spu.Quantity, unit_to: spu.Quantity) -> sp.Basic:
if unit_from.dimension == unit_to.dimension:
return spu.convert_to(unit_from, unit_to) / unit_to
def scale_to_unit(expr: sp.Expr, unit: spu.Quantity) -> typ.Any:
## TODO: An LFU cache could do better than an LRU.
unitless_expr = spu.convert_to(expr, unit) / unit