Files
tono/backend/nodes/presentation_ops.py
2026-04-04 00:25:53 -07:00

89 lines
3.2 KiB
Python

"""Presentation operations -- manage presentation overlays on data fields."""
from __future__ import annotations
import numpy as np
from backend.node_registry import register_node
from backend.data_types import DataField
@register_node(display_name="Presentation Ops")
class PresentationOps:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"field": ("DATA_FIELD",),
"operation": (["logscale", "extract_presentation", "attach", "blend"],),
"blend_factor": ("FLOAT", {
"default": 0.5,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"show_when_widget_value": {"operation": ["blend"]},
}),
},
"optional": {
"overlay": ("DATA_FIELD", {
"show_when_widget_value": {"operation": ["attach", "blend"]},
}),
},
}
OUTPUTS = (
('DATA_FIELD', 'result'),
)
FUNCTION = "process"
DESCRIPTION = (
"Manage presentation overlays on data fields. "
"logscale applies logarithmic scaling for visualising data with large dynamic range. "
"extract_presentation normalises the field to [0, 1]. "
"attach replaces the field data with an overlay (resampled if needed). "
"blend linearly mixes the field and overlay by a configurable factor. "
"Equivalent to Gwyddion's presentationops.c module."
)
def process(self, field: DataField, operation: str, blend_factor: float,
overlay: DataField | None = None) -> tuple:
data = np.asarray(field.data, dtype=np.float64)
if operation == "logscale":
data_pos = data - data.min() + 1e-30
result = np.log10(data_pos)
elif operation == "extract_presentation":
dmin, dmax = data.min(), data.max()
if dmax > dmin:
result = (data - dmin) / (dmax - dmin)
else:
result = np.zeros_like(data)
elif operation == "attach":
if overlay is None:
raise ValueError("'attach' operation requires an overlay field.")
overlay_data = np.asarray(overlay.data, dtype=np.float64)
result = self._match_shape(overlay_data, data.shape)
elif operation == "blend":
if overlay is None:
raise ValueError("'blend' operation requires an overlay field.")
overlay_data = np.asarray(overlay.data, dtype=np.float64)
overlay_matched = self._match_shape(overlay_data, data.shape)
result = (1.0 - blend_factor) * data + blend_factor * overlay_matched
else:
raise ValueError(f"Unknown operation: {operation!r}")
return (field.replace(data=result),)
@staticmethod
def _match_shape(source: np.ndarray, target_shape: tuple[int, ...]) -> np.ndarray:
"""Resample *source* to *target_shape* using scipy zoom if shapes differ."""
if source.shape == target_shape:
return source
from scipy.ndimage import zoom
factors = tuple(t / s for t, s in zip(target_shape, source.shape))
return zoom(source, factors, order=3)