ui: Data socket UI is now spiffy.

main
Sofus Albert Høgsbro Rose 2024-04-25 09:56:21 +02:00
parent badadfbfc2
commit 7d944a704e
Signed by: so-rose
GPG Key ID: AD901CB0F3701434
5 changed files with 237 additions and 37 deletions

View File

@ -2,4 +2,9 @@ import enum
class Icon(enum.StrEnum):
# Node Tree
SimNodeEditor = 'MOD_SIMPLEDEFORM'
# Sockets
ToggleSocketInfo = 'TRIA_DOWN'
DataSocketOutput = 'HOLDOUT_OFF'

View File

@ -1,3 +1,5 @@
"""Declares `MapMathNode`."""
import enum
import typing as typ
@ -19,7 +21,80 @@ X_COMPLEX = sp.Symbol('x', complex=True)
class MapMathNode(base.MaxwellSimNode):
"""Applies a function by-structure to the data.
r"""Applies a function by-structure to the data.
The shape, type, and interpretation of the input/output data is dynamically shown.
# Socket Sets
The line between a "map" and a "filter" is generally a matter of taste.
In general, "map" provides "do something to each of x" operations.
While it is often generally assumed that `numpy` broadcasting rules are well-known, dimensional data is inherently complicated.
Therefore, we choose an explicit, opinionated approach to "how things are mapped", prioritizing predictability over flexibility.
## By Element
Applies a function to each scalar number of the array.
:::{.callout-tip title="Example"}
Say we have a standard `(50, 3)` array with a `float32` (`f32`) datatype.
We could interpret such an indexed structure as an **element map**:
$$
A:\,\,\underbrace{(\mathbb{Z}_{50}, \mathbb{Z}_3)}_{\texttt{(50,3)}} \to \underbrace{(\mathbb{R})}_{\texttt{f32}}
$$
`By Element` simply applies a function to each output value, $\mathbb{R}$, producing a new $A$ with the same dimensions.
Note that the datatype might be altered, ex. `\mathbb{C} \to \mathbb{R}`, as part of the function.
:::
## By Vector
Applies a function to each vector, the elements of which span the **last axis**.
This **might** produce a well-known dimensionality change, depending on what each vector maps to.
:::{.callout-tip title="Example"}
Let's build on the `By Element` example, by interpreting it as a list of column vectors, and taking the length of each.
`By Vector` operates on the same data, but interpreted in a slightly deconstructed way:
$$
A:\,\,\underbrace{(\mathbb{Z}_{50})}_{\texttt{(50,)}} \to (\underbrace{(\mathbb{Z}_3)}_{\texttt{(3,)}} \to \underbrace{(\mathbb{R})}_{\texttt{f32}})
$$
`By Vector` applies a function to each $\underbrace{(\mathbb{Z}_3)}_{\texttt{(3,)}} \to \underbrace{(\mathbb{R})}_{\texttt{f32}}$.
Applying a standard 2-norm
$$
||\cdot||_2:\,\,\,\,(\underbrace{(\mathbb{Z}_3)}_{\texttt{(3,)}} \to \underbrace{(\mathbb{R})}_{\texttt{f32}}) \to \underbrace{(\mathbb{R})}_{\texttt{f32}}
$$
to our $A$ results in a new, reduced-dimension array:
$$
A_{||\cdot||_2}:\,\,\underbrace{(\mathbb{Z}_{50})}_{\texttt{(50,)}} \to \underbrace{(\mathbb{R})}_{\texttt{f32}}
$$
:::
## By Matrix
Applies a function to each matrix, the elements of which span the **last two axes**.
This **might** produce a well-known dimensionality change, depending on what each matrix maps to.
:::{.callout-tip title="Just Like Vectors"}
At this point, we reach 3D, and mental models become more difficult.
When dealing with high-dimensional arrays, it is suggested to draw out the math, ex. with the explicit notation introduced earlier.
:::
## Expr
Applies a user-sourced symbolic expression to a single symbol, with the symbol either representing (selectably) a single element, vector, or matrix.
The name and type of the available symbol is clearly shown, and most valid `sympy` expressions that you would expect to work, should work.
Use of expressions generally imposes no performance penalty: Just like the baked-in operations, it is compiled to a high-performance `jax` function.
Thus, it participates in the `ct.FlowKind.LazyValueFunc` composition chain.
Attributes:
operation: Operation to apply to the input.
@ -219,18 +294,16 @@ class MapMathNode(base.MaxwellSimNode):
)
def compute_data_info(self, props: dict, input_sockets: dict) -> ct.InfoFlow:
info = input_sockets['Data']
if ct.FlowSignal.check(info):
return ct.FlowSignal.FlowPending
# Complex -> Real
if (
props['active_socket_set'] == 'By Element'
and props['operation']
in [
if props['active_socket_set'] == 'By Element':
if props['operation'] in [
'REAL',
'IMAG',
'ABS',
]
and not ct.FlowSignal.check(info)
):
]:
return ct.InfoFlow(
dim_names=info.dim_names,
dim_idx=info.dim_idx,
@ -245,6 +318,8 @@ class MapMathNode(base.MaxwellSimNode):
},
output_units=info.output_units,
)
if props['active_socket_set'] == 'By Vector':
pass
return info
@events.computes_output_socket(

View File

@ -838,10 +838,11 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
node: The node within which the socket is embedded.
text: The socket's name in the UI.
"""
col = layout.column(align=False)
col = layout.column()
# Row: Label
row = col.row(align=False)
row = col.row()
row.alignment = 'LEFT'
## Lock Check
if self.locked:
@ -849,7 +850,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
## Link Check
if self.is_linked:
row.label(text=text)
self.draw_input_label_row(row, text)
else:
# User Label Row (incl. Units)
if self.use_units:
@ -912,6 +913,9 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
col = layout.column()
row = col.row()
row.alignment = 'RIGHT'
if self.is_linked:
self.draw_output_label_row(row, text)
else:
row.label(text=text)
# Draw FlowKind.Info related Information
@ -930,6 +934,8 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
) -> None:
"""Draw the label row, which is at the same height as the socket shape.
Will only display if the socket is an **unlinked input socket**.
Notes:
Can be overriden by individual socket classes, if they need to alter the way that the label row is drawn.
@ -939,6 +945,42 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
"""
row.label(text=text)
def draw_input_label_row(
self,
row: bpy.types.UILayout,
text: str,
) -> None:
"""Draw the label row, which is at the same height as the socket shape.
Will only display if the socket is a **linked input socket**.
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)
def draw_output_label_row(
self,
row: bpy.types.UILayout,
text: str,
) -> None:
"""Draw the output label row, which is at the same height as the socket shape.
Will only display if the socket is an **output socket**.
Notes:
Can be overriden by individual socket classes, if they need to alter the way that the output label row is drawn.
Parameters:
row: Target for defining UI elements.
text: The socket's name in the UI.
"""
row.label(text=text)
def draw_value(self, col: bpy.types.UILayout) -> None:
"""Draws the socket value on its own line.

View File

@ -1,13 +1,38 @@
import enum
import typing as typ
import bpy
from blender_maxwell.utils import bl_cache
from blender_maxwell.utils import bl_cache, logger
from blender_maxwell.utils import extra_sympy_units as spux
from ... import contracts as ct
from .. import base
log = logger.get(__name__)
class DataInfoColumn(enum.StrEnum):
Length = enum.auto()
MathType = enum.auto()
Unit = enum.auto()
@staticmethod
def to_name(value: typ.Self) -> str:
return {
DataInfoColumn.Length: 'L',
DataInfoColumn.MathType: '',
DataInfoColumn.Unit: 'U',
}[value]
@staticmethod
def to_icon(value: typ.Self) -> str:
return {
DataInfoColumn.Length: '',
DataInfoColumn.MathType: '',
DataInfoColumn.Unit: '',
}[value]
####################
# - Blender Socket
@ -23,6 +48,14 @@ class DataBLSocket(base.MaxwellSimSocket):
format: str = bl_cache.BLField('')
## TODO: typ.Literal['xarray', 'jax']
show_info_columns: bool = bl_cache.BLField(
False,
prop_ui=True,
)
info_columns: DataInfoColumn = bl_cache.BLField(
{DataInfoColumn.MathType, DataInfoColumn.Length}, prop_ui=True, enum_many=True
)
####################
# - FlowKind
####################
@ -37,30 +70,65 @@ class DataBLSocket(base.MaxwellSimSocket):
####################
# - UI
####################
def draw_input_label_row(self, row: bpy.types.UILayout, text) -> None:
if self.format == 'jax':
row.label(text=text)
row.prop(self, self.blfields['info_columns'])
row.prop(
self,
self.blfields['show_info_columns'],
toggle=True,
text='',
icon=ct.Icon.ToggleSocketInfo,
)
def draw_output_label_row(self, row: bpy.types.UILayout, text) -> None:
if self.format == 'jax':
row.prop(
self,
self.blfields['show_info_columns'],
toggle=True,
text='',
icon=ct.Icon.ToggleSocketInfo,
)
row.prop(self, self.blfields['info_columns'])
row.label(text=text)
def draw_info(self, info: ct.InfoFlow, col: bpy.types.UILayout) -> None:
if self.format == 'jax' and info.dim_names:
if self.format == 'jax' and info.dim_names and self.show_info_columns:
row = col.row()
box = row.box()
grid = box.grid_flow(
columns=3,
columns=len(self.info_columns) + 1,
row_major=True,
even_columns=True,
# even_rows=True,
align=True,
)
# Grid Header
# grid.label(text='Dim')
# grid.label(text='Len')
# grid.label(text='Unit')
# Dimension Names
# Dimensions
for dim_name in info.dim_names:
dim_idx = info.dim_idx[dim_name]
grid.label(text=dim_name)
if DataInfoColumn.Length in self.info_columns:
grid.label(text=str(len(dim_idx)))
if DataInfoColumn.MathType in self.info_columns:
grid.label(text=spux.MathType.to_str(dim_idx.mathtype))
if DataInfoColumn.Unit in self.info_columns:
grid.label(text=spux.sp_to_str(dim_idx.unit))
# Outputs
for output_name in info.output_names:
grid.label(text=output_name)
if DataInfoColumn.Length in self.info_columns:
grid.label(text='', icon=ct.Icon.DataSocketOutput)
if DataInfoColumn.MathType in self.info_columns:
grid.label(
text=spux.MathType.to_str(info.output_mathtypes[output_name])
)
if DataInfoColumn.Unit in self.info_columns:
grid.label(text=spux.sp_to_str(info.output_units[output_name]))
####################
# - Socket Configuration

View File

@ -71,6 +71,16 @@ class MathType(enum.StrEnum):
MathType.Complex: complex,
}[value]
@staticmethod
def to_str(value: typ.Self) -> type:
return {
MathType.Bool: 'T|F',
MathType.Integer: '',
MathType.Rational: '',
MathType.Real: '',
MathType.Complex: '',
}[value]
####################
# - Units