tidy up old code
This commit is contained in:
@@ -8,6 +8,7 @@ tono is a node-based SPM image processing and analysis tool. The main focus is o
|
|||||||
|
|
||||||
It is heavily inspired by [Gwyddion](https://gwyddion.net/), one of my favorite scientific FOSS programs on the web.
|
It is heavily inspired by [Gwyddion](https://gwyddion.net/), one of my favorite scientific FOSS programs on the web.
|
||||||
|
|
||||||
|
<img src="frontend/public/default-workflow.png" width="600">
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
Install a local binary from the Releases section, or run locally:
|
Install a local binary from the Releases section, or run locally:
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from contextvars import ContextVar
|
from contextvars import ContextVar
|
||||||
import inspect
|
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
Callback = Callable[[str, Any], None]
|
Callback = Callable[[str, Any], None]
|
||||||
@@ -13,16 +12,6 @@ _callbacks_var: ContextVar[dict[str, Callback | None]] = ContextVar(
|
|||||||
)
|
)
|
||||||
_node_id_var: ContextVar[str | None] = ContextVar("tono_execution_node_id", default=None)
|
_node_id_var: ContextVar[str | None] = ContextVar("tono_execution_node_id", default=None)
|
||||||
|
|
||||||
_LEGACY_CALLBACK_ATTRS = {
|
|
||||||
"preview": "_broadcast_fn",
|
|
||||||
"table": "_broadcast_table_fn",
|
|
||||||
"mesh": "_broadcast_mesh_fn",
|
|
||||||
"overlay": "_broadcast_overlay_fn",
|
|
||||||
"value": "_broadcast_value_fn",
|
|
||||||
"warning": "_broadcast_warning_fn",
|
|
||||||
"file_download": "_broadcast_file_download_fn",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def execution_callbacks(
|
def execution_callbacks(
|
||||||
@@ -63,42 +52,12 @@ def current_node_id() -> str | None:
|
|||||||
return _node_id_var.get()
|
return _node_id_var.get()
|
||||||
|
|
||||||
|
|
||||||
def _legacy_emit(kind: str, payload: Any) -> bool:
|
|
||||||
callback_attr = _LEGACY_CALLBACK_ATTRS.get(kind)
|
|
||||||
if not callback_attr:
|
|
||||||
return False
|
|
||||||
|
|
||||||
frame = inspect.currentframe()
|
|
||||||
try:
|
|
||||||
frame = frame.f_back
|
|
||||||
while frame is not None:
|
|
||||||
for owner_name in ("self", "cls"):
|
|
||||||
owner = frame.f_locals.get(owner_name)
|
|
||||||
if owner is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
candidate = owner if isinstance(owner, type) else owner.__class__
|
|
||||||
callback = getattr(candidate, callback_attr, None)
|
|
||||||
node_id = getattr(candidate, "_current_node_id", None)
|
|
||||||
if callback is not None and node_id:
|
|
||||||
callback(str(node_id), payload)
|
|
||||||
return True
|
|
||||||
frame = frame.f_back
|
|
||||||
finally:
|
|
||||||
del frame
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _emit(kind: str, payload: Any) -> None:
|
def _emit(kind: str, payload: Any) -> None:
|
||||||
callbacks = _callbacks_var.get()
|
callbacks = _callbacks_var.get()
|
||||||
callback = callbacks.get(kind)
|
callback = callbacks.get(kind)
|
||||||
node_id = current_node_id()
|
node_id = current_node_id()
|
||||||
if callback is not None and node_id:
|
if callback is not None and node_id:
|
||||||
callback(node_id, payload)
|
callback(node_id, payload)
|
||||||
return
|
|
||||||
|
|
||||||
_legacy_emit(kind, payload)
|
|
||||||
|
|
||||||
|
|
||||||
def emit_preview(payload: Any) -> None:
|
def emit_preview(payload: Any) -> None:
|
||||||
|
|||||||
@@ -16,9 +16,6 @@ from backend.data_types import (
|
|||||||
|
|
||||||
@register_node(display_name="Annotations")
|
@register_node(display_name="Annotations")
|
||||||
class Annotations:
|
class Annotations:
|
||||||
_broadcast_warning_fn = None
|
|
||||||
_current_node_id: str = ""
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -37,9 +37,6 @@ class CropResizeField:
|
|||||||
"resizing preserves the cropped physical size."
|
"resizing preserves the cropped physical size."
|
||||||
)
|
)
|
||||||
|
|
||||||
_broadcast_overlay_fn = None
|
|
||||||
_current_node_id: str = ""
|
|
||||||
|
|
||||||
def process(
|
def process(
|
||||||
self,
|
self,
|
||||||
field: DataField,
|
field: DataField,
|
||||||
|
|||||||
@@ -39,9 +39,6 @@ class CrossSection:
|
|||||||
"Equivalent to gwy_data_field_get_profile."
|
"Equivalent to gwy_data_field_get_profile."
|
||||||
)
|
)
|
||||||
|
|
||||||
_broadcast_overlay_fn = None
|
|
||||||
_current_node_id: str = ""
|
|
||||||
|
|
||||||
def process(
|
def process(
|
||||||
self, field: DataField,
|
self, field: DataField,
|
||||||
x1: float, y1: float, x2: float, y2: float,
|
x1: float, y1: float, x2: float, y2: float,
|
||||||
|
|||||||
@@ -39,9 +39,6 @@ class Cursors:
|
|||||||
"On fields it reports x/y/z at both markers plus dx/dy/dz."
|
"On fields it reports x/y/z at both markers plus dx/dy/dz."
|
||||||
)
|
)
|
||||||
|
|
||||||
_broadcast_overlay_fn = None
|
|
||||||
_current_node_id: str = ""
|
|
||||||
|
|
||||||
def process(
|
def process(
|
||||||
self, line, x1: float, y1: float, x2: float, y2: float,
|
self, line, x1: float, y1: float, x2: float, y2: float,
|
||||||
coord_pair=None,
|
coord_pair=None,
|
||||||
|
|||||||
@@ -30,9 +30,6 @@ class FFT1D:
|
|||||||
"Returns the FFT spectrum of the line, and identifies peaks."
|
"Returns the FFT spectrum of the line, and identifies peaks."
|
||||||
)
|
)
|
||||||
|
|
||||||
_broadcast_overlay_fn = None
|
|
||||||
_current_node_id: str = ""
|
|
||||||
|
|
||||||
def process(
|
def process(
|
||||||
self, profile,
|
self, profile,
|
||||||
) -> tuple:
|
) -> tuple:
|
||||||
|
|||||||
@@ -122,84 +122,6 @@ def _measurement_value(table: list, selection: str) -> float:
|
|||||||
raise ValueError(f"Measurement '{row.get('quantity', selection)}' does not have a numeric value.")
|
raise ValueError(f"Measurement '{row.get('quantity', selection)}' does not have a numeric value.")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# SI formatting helpers (from display.py — used by Annotations)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
_SI_PREFIXES = [
|
|
||||||
(1e24, "Y"), (1e21, "Z"), (1e18, "E"), (1e15, "P"), (1e12, "T"),
|
|
||||||
(1e9, "G"), (1e6, "M"), (1e3, "k"), (1.0, ""), (1e-3, "m"),
|
|
||||||
(1e-6, "u"), (1e-9, "n"), (1e-12, "p"), (1e-15, "f"),
|
|
||||||
(1e-18, "a"), (1e-21, "z"), (1e-24, "y"),
|
|
||||||
]
|
|
||||||
_PREFIXABLE_UNITS = {"m", "s", "A", "V", "W", "Hz", "F", "C", "J", "N", "Pa", "T", "H", "S", "g", "K", "Ohm", "ohm", "\u03a9"}
|
|
||||||
|
|
||||||
|
|
||||||
def _format_numeric(value: float) -> str:
|
|
||||||
if not np.isfinite(value):
|
|
||||||
return str(value)
|
|
||||||
abs_value = abs(value)
|
|
||||||
if abs_value == 0:
|
|
||||||
return "0"
|
|
||||||
if abs_value >= 1e4 or abs_value < 1e-3:
|
|
||||||
return f"{value:.3e}"
|
|
||||||
return f"{value:.4g}"
|
|
||||||
|
|
||||||
|
|
||||||
def _format_with_unit(value: float, unit: str) -> str:
|
|
||||||
unit = (unit or "").strip()
|
|
||||||
if not unit:
|
|
||||||
return _format_numeric(value)
|
|
||||||
if unit in _PREFIXABLE_UNITS and np.isfinite(value) and value != 0:
|
|
||||||
abs_value = abs(value)
|
|
||||||
for scale, prefix in _SI_PREFIXES:
|
|
||||||
scaled = abs_value / scale
|
|
||||||
if 1 <= scaled < 1000:
|
|
||||||
signed = value / scale
|
|
||||||
return f"{_format_numeric(signed)} {prefix}{unit}"
|
|
||||||
return f"{_format_numeric(value)} {unit}"
|
|
||||||
|
|
||||||
|
|
||||||
def _nice_length(target: float) -> float:
|
|
||||||
if not np.isfinite(target) or target <= 0:
|
|
||||||
return 0.0
|
|
||||||
exponent = np.floor(np.log10(target))
|
|
||||||
base = 10.0 ** exponent
|
|
||||||
for step in (5.0, 2.0, 1.0):
|
|
||||||
candidate = step * base
|
|
||||||
if candidate <= target:
|
|
||||||
return candidate
|
|
||||||
return base
|
|
||||||
|
|
||||||
|
|
||||||
def _display_value_range(field) -> tuple[float, float, float]:
|
|
||||||
data = np.asarray(field.data, dtype=np.float64)
|
|
||||||
dmin = float(data.min())
|
|
||||||
dmax = float(data.max())
|
|
||||||
if not np.isfinite(dmin) or not np.isfinite(dmax) or dmax <= dmin:
|
|
||||||
return dmin, dmin, dmin
|
|
||||||
|
|
||||||
offset = float(field.display_offset)
|
|
||||||
scale = float(field.display_scale)
|
|
||||||
if not np.isfinite(offset):
|
|
||||||
offset = 0.0
|
|
||||||
if not np.isfinite(scale) or scale <= 0.0:
|
|
||||||
scale = 1.0
|
|
||||||
|
|
||||||
low_norm = float(np.clip(offset, 0.0, 1.0))
|
|
||||||
high_norm = float(np.clip(offset + scale, 0.0, 1.0))
|
|
||||||
if high_norm < low_norm:
|
|
||||||
low_norm, high_norm = high_norm, low_norm
|
|
||||||
mid_norm = 0.5 * (low_norm + high_norm)
|
|
||||||
|
|
||||||
span = dmax - dmin
|
|
||||||
return (
|
|
||||||
dmin + low_norm * span,
|
|
||||||
dmin + mid_norm * span,
|
|
||||||
dmin + high_norm * span,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _render_annotation_text(text: str, size_px: int, color: tuple[int, int, int]):
|
def _render_annotation_text(text: str, size_px: int, color: tuple[int, int, int]):
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
|||||||
@@ -34,9 +34,6 @@ class Histogram:
|
|||||||
"Equivalent to gwy_data_field_dh."
|
"Equivalent to gwy_data_field_dh."
|
||||||
)
|
)
|
||||||
|
|
||||||
_broadcast_overlay_fn = None
|
|
||||||
_current_node_id: str = ""
|
|
||||||
|
|
||||||
def process(
|
def process(
|
||||||
self,
|
self,
|
||||||
field: DataField,
|
field: DataField,
|
||||||
|
|||||||
@@ -36,9 +36,6 @@ class Image:
|
|||||||
"Images (.png, .tiff, .jpg) and arrays (.npy, .npz) are loaded as uncalibrated fields."
|
"Images (.png, .tiff, .jpg) and arrays (.npy, .npz) are loaded as uncalibrated fields."
|
||||||
)
|
)
|
||||||
|
|
||||||
_broadcast_warning_fn = None
|
|
||||||
_current_node_id = None
|
|
||||||
|
|
||||||
def load(self, filename: str = "", colormap: str = "viridis", colormap_map=None, path: str | None = None):
|
def load(self, filename: str = "", colormap: str = "viridis", colormap_map=None, path: str | None = None):
|
||||||
selected_path = str(path).strip() if path is not None else str(filename).strip()
|
selected_path = str(path).strip() if path is not None else str(filename).strip()
|
||||||
if not selected_path:
|
if not selected_path:
|
||||||
|
|||||||
@@ -26,9 +26,6 @@ class ImageDemo:
|
|||||||
|
|
||||||
DESCRIPTION = "Load a bundled demo file so you can try the app without providing your own data."
|
DESCRIPTION = "Load a bundled demo file so you can try the app without providing your own data."
|
||||||
|
|
||||||
_broadcast_warning_fn = None
|
|
||||||
_current_node_id = None
|
|
||||||
|
|
||||||
def load(self, name: str = "", colormap: str = "viridis", colormap_map=None):
|
def load(self, name: str = "", colormap: str = "viridis", colormap_map=None):
|
||||||
from backend.nodes.image import Image
|
from backend.nodes.image import Image
|
||||||
loader = Image()
|
loader = Image()
|
||||||
|
|||||||
@@ -43,9 +43,6 @@ class Markup:
|
|||||||
"or rasterize markup directly onto an IMAGE."
|
"or rasterize markup directly onto an IMAGE."
|
||||||
)
|
)
|
||||||
|
|
||||||
_broadcast_overlay_fn = None
|
|
||||||
_current_node_id: str = ""
|
|
||||||
|
|
||||||
def process(
|
def process(
|
||||||
self,
|
self,
|
||||||
input,
|
input,
|
||||||
|
|||||||
@@ -33,9 +33,6 @@ class DrawMask:
|
|||||||
"and invert flips the final binary output."
|
"and invert flips the final binary output."
|
||||||
)
|
)
|
||||||
|
|
||||||
_broadcast_overlay_fn = None
|
|
||||||
_current_node_id: str = ""
|
|
||||||
|
|
||||||
def process(self, field: DataField, pen_size: int, invert: bool, mask_paths: str) -> tuple:
|
def process(self, field: DataField, pen_size: int, invert: bool, mask_paths: str) -> tuple:
|
||||||
strokes = _parse_mask_strokes(mask_paths)
|
strokes = _parse_mask_strokes(mask_paths)
|
||||||
mask = _rasterize_mask(field.xres, field.yres, strokes, pen_size)
|
mask = _rasterize_mask(field.xres, field.yres, strokes, pen_size)
|
||||||
|
|||||||
@@ -28,9 +28,6 @@ class MaskInvert:
|
|||||||
|
|
||||||
DESCRIPTION = "Invert a binary mask — swap masked and unmasked regions."
|
DESCRIPTION = "Invert a binary mask — swap masked and unmasked regions."
|
||||||
|
|
||||||
_broadcast_fn = None
|
|
||||||
_current_node_id: str = ""
|
|
||||||
|
|
||||||
def process(self, mask: np.ndarray, field: DataField | None = None) -> tuple:
|
def process(self, mask: np.ndarray, field: DataField | None = None) -> tuple:
|
||||||
out = np.where(mask > 127, np.uint8(0), np.uint8(255))
|
out = np.where(mask > 127, np.uint8(0), np.uint8(255))
|
||||||
|
|
||||||
|
|||||||
@@ -41,9 +41,6 @@ class MaskMorphology:
|
|||||||
"Equivalent to Gwyddion mask_morph."
|
"Equivalent to Gwyddion mask_morph."
|
||||||
)
|
)
|
||||||
|
|
||||||
_broadcast_fn = None
|
|
||||||
_current_node_id: str = ""
|
|
||||||
|
|
||||||
def process(self, mask: np.ndarray, operation: str, radius: int, shape: str,
|
def process(self, mask: np.ndarray, operation: str, radius: int, shape: str,
|
||||||
field: DataField | None = None) -> tuple:
|
field: DataField | None = None) -> tuple:
|
||||||
from scipy.ndimage import binary_closing, binary_dilation, binary_erosion, binary_opening
|
from scipy.ndimage import binary_closing, binary_dilation, binary_erosion, binary_opening
|
||||||
|
|||||||
@@ -33,9 +33,6 @@ class ThresholdMask:
|
|||||||
"Equivalent to Gwyddion's threshold and otsu_threshold modules."
|
"Equivalent to Gwyddion's threshold and otsu_threshold modules."
|
||||||
)
|
)
|
||||||
|
|
||||||
_broadcast_fn = None
|
|
||||||
_current_node_id: str = ""
|
|
||||||
|
|
||||||
def process(self, field: DataField, method: str, threshold: float, direction: str) -> tuple:
|
def process(self, field: DataField, method: str, threshold: float, direction: str) -> tuple:
|
||||||
data = field.data
|
data = field.data
|
||||||
|
|
||||||
|
|||||||
@@ -36,9 +36,6 @@ class PreviewImage:
|
|||||||
OUTPUT_NODE = True
|
OUTPUT_NODE = True
|
||||||
DESCRIPTION = "Display an IMAGE or DATA_FIELD as a coloured thumbnail."
|
DESCRIPTION = "Display an IMAGE or DATA_FIELD as a coloured thumbnail."
|
||||||
|
|
||||||
_broadcast_fn = None
|
|
||||||
_current_node_id: str = ""
|
|
||||||
|
|
||||||
def preview(
|
def preview(
|
||||||
self,
|
self,
|
||||||
colormap: str,
|
colormap: str,
|
||||||
|
|||||||
@@ -21,9 +21,6 @@ class PrintTable:
|
|||||||
OUTPUT_NODE = True
|
OUTPUT_NODE = True
|
||||||
DESCRIPTION = "Send a measurement or record table to the browser as a WebSocket message for display."
|
DESCRIPTION = "Send a measurement or record table to the browser as a WebSocket message for display."
|
||||||
|
|
||||||
_broadcast_table_fn = None
|
|
||||||
_current_node_id: str = ""
|
|
||||||
|
|
||||||
def print_table(self, table: list) -> tuple:
|
def print_table(self, table: list) -> tuple:
|
||||||
emit_table(table)
|
emit_table(table)
|
||||||
return ()
|
return ()
|
||||||
|
|||||||
@@ -28,9 +28,6 @@ class RotateField:
|
|||||||
"Optionally expand the canvas to keep the full rotated field while preserving the field center."
|
"Optionally expand the canvas to keep the full rotated field while preserving the field center."
|
||||||
)
|
)
|
||||||
|
|
||||||
_broadcast_warning_fn = None
|
|
||||||
_current_node_id: str = ""
|
|
||||||
|
|
||||||
def process(
|
def process(
|
||||||
self,
|
self,
|
||||||
field: DataField,
|
field: DataField,
|
||||||
|
|||||||
@@ -74,9 +74,6 @@ class Save:
|
|||||||
"Save a single graph value to disk. Supports fields, images, lines, tables, scalars, and 3D meshes."
|
"Save a single graph value to disk. Supports fields, images, lines, tables, scalars, and 3D meshes."
|
||||||
)
|
)
|
||||||
|
|
||||||
_broadcast_warning_fn = None
|
|
||||||
_current_node_id = None
|
|
||||||
|
|
||||||
def save(
|
def save(
|
||||||
self,
|
self,
|
||||||
filename: str,
|
filename: str,
|
||||||
|
|||||||
@@ -63,9 +63,6 @@ class SaveImage:
|
|||||||
"Click Save to write (does not auto-run)."
|
"Click Save to write (does not auto-run)."
|
||||||
)
|
)
|
||||||
|
|
||||||
_broadcast_warning_fn = None
|
|
||||||
_current_node_id = None
|
|
||||||
|
|
||||||
def save(
|
def save(
|
||||||
self,
|
self,
|
||||||
filename: str,
|
filename: str,
|
||||||
@@ -187,5 +184,3 @@ class SaveImage:
|
|||||||
|
|
||||||
def _send_warning(self, message: str):
|
def _send_warning(self, message: str):
|
||||||
emit_warning(message)
|
emit_warning(message)
|
||||||
|
|
||||||
return ()
|
|
||||||
|
|||||||
@@ -19,9 +19,6 @@ from backend.nodes.helpers import (
|
|||||||
class Stats:
|
class Stats:
|
||||||
"""Polymorphic scalar stats node for LINE, DATA_TABLE, DATA_FIELD, or IMAGE inputs."""
|
"""Polymorphic scalar stats node for LINE, DATA_TABLE, DATA_FIELD, or IMAGE inputs."""
|
||||||
|
|
||||||
_broadcast_value_fn = None
|
|
||||||
_current_node_id: str = ""
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -41,9 +41,6 @@ class ValueIO:
|
|||||||
|
|
||||||
DESCRIPTION = "Display a FLOAT, or a selected numeric row from a measurement table, and pass the value through unchanged."
|
DESCRIPTION = "Display a FLOAT, or a selected numeric row from a measurement table, and pass the value through unchanged."
|
||||||
|
|
||||||
_broadcast_value_fn = None
|
|
||||||
_current_node_id: str = ""
|
|
||||||
|
|
||||||
def display_value(self, number_input: str = "0", value=None, measurement: str = "") -> tuple:
|
def display_value(self, number_input: str = "0", value=None, measurement: str = "") -> tuple:
|
||||||
unit = ""
|
unit = ""
|
||||||
if isinstance(value, RecordTable):
|
if isinstance(value, RecordTable):
|
||||||
|
|||||||
@@ -148,9 +148,6 @@ class View3D:
|
|||||||
"Drag to rotate, middle-drag to pan, and right-drag or scroll to zoom. z_scale exaggerates height."
|
"Drag to rotate, middle-drag to pan, and right-drag or scroll to zoom. z_scale exaggerates height."
|
||||||
)
|
)
|
||||||
|
|
||||||
_broadcast_mesh_fn = None
|
|
||||||
_current_node_id: str = ""
|
|
||||||
|
|
||||||
def render(
|
def render(
|
||||||
self, field: DataField,
|
self, field: DataField,
|
||||||
colormap: str, z_scale: float, resolution: int, make_solid: bool = False,
|
colormap: str, z_scale: float, resolution: int, make_solid: bool = False,
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
const EXCLUDED_CANVAS_TARGETS = '.context-menu, .react-flow__node, .react-flow__edge, .react-flow__controls, .react-flow__minimap, .surface-view-container';
|
|
||||||
const CANVAS_AREA_TARGETS = '.react-flow, .react-flow__renderer, .react-flow__viewport, .react-flow__pane, .react-flow__background, .react-flow__selectionpane';
|
|
||||||
|
|
||||||
function getTargetElement(target: EventTarget | null): Element | null {
|
|
||||||
if (!target) return null;
|
|
||||||
if (typeof (target as Element).closest === 'function') return target as Element;
|
|
||||||
const parent = (target as Node).parentElement;
|
|
||||||
if (parent && typeof parent.closest === 'function') {
|
|
||||||
return parent;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function supportsClosest(target: EventTarget | null): boolean {
|
|
||||||
return !!getTargetElement(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
function matchesClosest(target: EventTarget | null, selector: string): boolean {
|
|
||||||
const element = getTargetElement(target);
|
|
||||||
return !!element && element.closest(selector) !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isEditableInteractionTarget(target: EventTarget | null): boolean {
|
|
||||||
if (!supportsClosest(target)) return false;
|
|
||||||
if (matchesClosest(target, 'input, textarea, select')) return true;
|
|
||||||
return matchesClosest(target, '[contenteditable="true"]');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function canStartCanvasRightDragZoomTarget(target: EventTarget | null): boolean {
|
|
||||||
if (!supportsClosest(target)) return false;
|
|
||||||
if (isEditableInteractionTarget(target)) return false;
|
|
||||||
if (matchesClosest(target, EXCLUDED_CANVAS_TARGETS)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return matchesClosest(target, CANVAS_AREA_TARGETS);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function canOpenCanvasContextMenuTarget(target: EventTarget | null): boolean {
|
|
||||||
if (!supportsClosest(target)) return false;
|
|
||||||
if (isEditableInteractionTarget(target)) return false;
|
|
||||||
if (matchesClosest(target, EXCLUDED_CANVAS_TARGETS)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return matchesClosest(target, CANVAS_AREA_TARGETS);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isSecondaryCanvasContextEvent(event: MouseEvent | null): boolean {
|
|
||||||
if (!event || typeof event.button !== 'number') return false;
|
|
||||||
return event.button === 2 || (event.button === 0 && !!event.ctrlKey);
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import test from 'node:test';
|
|
||||||
import assert from 'node:assert/strict';
|
|
||||||
|
|
||||||
import {
|
|
||||||
canOpenCanvasContextMenuTarget,
|
|
||||||
canStartCanvasRightDragZoomTarget,
|
|
||||||
isEditableInteractionTarget,
|
|
||||||
isSecondaryCanvasContextEvent,
|
|
||||||
} from '../src/canvasInteractionTargets.ts';
|
|
||||||
|
|
||||||
function makeTarget(activeSelectors = []) {
|
|
||||||
const selectorSet = new Set(activeSelectors);
|
|
||||||
return {
|
|
||||||
closest(selector) {
|
|
||||||
const parts = String(selector).split(',').map((part) => part.trim());
|
|
||||||
return parts.some((part) => selectorSet.has(part)) ? {} : null;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
test('editable canvas targets stay editable', () => {
|
|
||||||
const inputTarget = makeTarget(['input']);
|
|
||||||
assert.equal(isEditableInteractionTarget(inputTarget), true);
|
|
||||||
assert.equal(canOpenCanvasContextMenuTarget(inputTarget), false);
|
|
||||||
assert.equal(canStartCanvasRightDragZoomTarget(inputTarget), false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('empty pane targets allow the custom canvas context menu', () => {
|
|
||||||
const paneTarget = makeTarget(['.react-flow__pane']);
|
|
||||||
assert.equal(canOpenCanvasContextMenuTarget(paneTarget), true);
|
|
||||||
assert.equal(canStartCanvasRightDragZoomTarget(paneTarget), true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('viewport-level targets also allow the custom canvas context menu', () => {
|
|
||||||
const viewportTarget = makeTarget(['.react-flow__viewport']);
|
|
||||||
assert.equal(canOpenCanvasContextMenuTarget(viewportTarget), true);
|
|
||||||
assert.equal(canStartCanvasRightDragZoomTarget(viewportTarget), true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('node and surface-view targets do not open the canvas context menu', () => {
|
|
||||||
assert.equal(canOpenCanvasContextMenuTarget(makeTarget(['.react-flow__node', '.react-flow__pane'])), false);
|
|
||||||
assert.equal(canOpenCanvasContextMenuTarget(makeTarget(['.surface-view-container', '.react-flow__pane'])), false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('secondary canvas context detection includes macOS ctrl-click', () => {
|
|
||||||
assert.equal(isSecondaryCanvasContextEvent({ button: 2, ctrlKey: false }), true);
|
|
||||||
assert.equal(isSecondaryCanvasContextEvent({ button: 0, ctrlKey: true }), true);
|
|
||||||
assert.equal(isSecondaryCanvasContextEvent({ button: 0, ctrlKey: false }), false);
|
|
||||||
});
|
|
||||||
@@ -5,10 +5,6 @@ export default defineConfig({
|
|||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
host: true,
|
host: true,
|
||||||
allowedHosts: ["bronchial-lorita-gorgeously.ngrok-free.dev"],
|
|
||||||
hmr:{
|
|
||||||
clientPort: 80,
|
|
||||||
},
|
|
||||||
port: 5173,
|
port: 5173,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/nodes': 'http://127.0.0.1:8188',
|
'/nodes': 'http://127.0.0.1:8188',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from backend.data_types import DataField
|
from backend.data_types import DataField
|
||||||
|
from backend.execution_context import execution_callbacks, active_node
|
||||||
|
|
||||||
|
|
||||||
def test_crop_resize_field():
|
def test_crop_resize_field():
|
||||||
@@ -19,42 +20,38 @@ def test_crop_resize_field():
|
|||||||
)
|
)
|
||||||
|
|
||||||
overlays = []
|
overlays = []
|
||||||
CropResizeField._broadcast_overlay_fn = lambda nid, data: overlays.append(data)
|
with execution_callbacks(overlay=lambda nid, data: overlays.append(data)), active_node("test"):
|
||||||
CropResizeField._current_node_id = "test"
|
cropped, = node.process(field, x1=0.25, y1=0.25, x2=0.75, y2=1.0, target_width=0, target_height=0, interpolation="bilinear")
|
||||||
|
assert cropped.data.shape == (3, 4)
|
||||||
|
assert np.array_equal(cropped.data, data[1:4, 2:6])
|
||||||
|
assert cropped.xreal == 4.0
|
||||||
|
assert cropped.yreal == 3.0
|
||||||
|
assert cropped.xoff == 12.0
|
||||||
|
assert cropped.yoff == 21.0
|
||||||
|
assert cropped.si_unit_xy == field.si_unit_xy
|
||||||
|
assert cropped.si_unit_z == field.si_unit_z
|
||||||
|
assert cropped.overlays == []
|
||||||
|
assert len(overlays) == 1
|
||||||
|
assert overlays[0]["kind"] == "crop_box"
|
||||||
|
assert overlays[0]["image"].startswith("data:image/png;base64,")
|
||||||
|
assert overlays[0]["a_locked"] is False
|
||||||
|
assert overlays[0]["b_locked"] is False
|
||||||
|
|
||||||
cropped, = node.process(field, x1=0.25, y1=0.25, x2=0.75, y2=1.0, target_width=0, target_height=0, interpolation="bilinear")
|
resized, = node.process(field, x1=0.0, y1=0.0, x2=1.0, y2=1.0, target_width=8, target_height=0, interpolation="bilinear", corner_a=(0.25, 0.25), corner_b=(0.75, 1.0))
|
||||||
assert cropped.data.shape == (3, 4)
|
assert resized.data.shape == (6, 8)
|
||||||
assert np.array_equal(cropped.data, data[1:4, 2:6])
|
assert resized.xreal == cropped.xreal
|
||||||
assert cropped.xreal == 4.0
|
assert resized.yreal == cropped.yreal
|
||||||
assert cropped.yreal == 3.0
|
assert resized.xoff == cropped.xoff
|
||||||
assert cropped.xoff == 12.0
|
assert resized.yoff == cropped.yoff
|
||||||
assert cropped.yoff == 21.0
|
assert resized.domain == field.domain
|
||||||
assert cropped.si_unit_xy == field.si_unit_xy
|
assert overlays[-1]["a_locked"] is True
|
||||||
assert cropped.si_unit_z == field.si_unit_z
|
assert overlays[-1]["b_locked"] is True
|
||||||
assert cropped.overlays == []
|
|
||||||
assert len(overlays) == 1
|
|
||||||
assert overlays[0]["kind"] == "crop_box"
|
|
||||||
assert overlays[0]["image"].startswith("data:image/png;base64,")
|
|
||||||
assert overlays[0]["a_locked"] is False
|
|
||||||
assert overlays[0]["b_locked"] is False
|
|
||||||
|
|
||||||
resized, = node.process(field, x1=0.0, y1=0.0, x2=1.0, y2=1.0, target_width=8, target_height=0, interpolation="bilinear", corner_a=(0.25, 0.25), corner_b=(0.75, 1.0))
|
reversed_crop, = node.process(field, x1=0.75, y1=1.0, x2=0.25, y2=0.25, target_width=0, target_height=0, interpolation="nearest")
|
||||||
assert resized.data.shape == (6, 8)
|
assert np.array_equal(reversed_crop.data, cropped.data)
|
||||||
assert resized.xreal == cropped.xreal
|
|
||||||
assert resized.yreal == cropped.yreal
|
|
||||||
assert resized.xoff == cropped.xoff
|
|
||||||
assert resized.yoff == cropped.yoff
|
|
||||||
assert resized.domain == field.domain
|
|
||||||
assert overlays[-1]["a_locked"] is True
|
|
||||||
assert overlays[-1]["b_locked"] is True
|
|
||||||
|
|
||||||
reversed_crop, = node.process(field, x1=0.75, y1=1.0, x2=0.25, y2=0.25, target_width=0, target_height=0, interpolation="nearest")
|
try:
|
||||||
assert np.array_equal(reversed_crop.data, cropped.data)
|
node.process(field, x1=0.9, y1=0.0, x2=0.9, y2=1.0, target_width=0, target_height=0, interpolation="nearest")
|
||||||
|
raise AssertionError("Expected invalid crop bounds to raise ValueError")
|
||||||
try:
|
except ValueError:
|
||||||
node.process(field, x1=0.9, y1=0.0, x2=0.9, y2=1.0, target_width=0, target_height=0, interpolation="nearest")
|
pass
|
||||||
raise AssertionError("Expected invalid crop bounds to raise ValueError")
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
CropResizeField._broadcast_overlay_fn = None
|
|
||||||
|
|||||||
@@ -42,11 +42,10 @@ def test_cross_section():
|
|||||||
assert rows["dx"]["unit"] == field.si_unit_xy
|
assert rows["dx"]["unit"] == field.si_unit_xy
|
||||||
assert rows["dy"]["unit"] == field.si_unit_z
|
assert rows["dy"]["unit"] == field.si_unit_z
|
||||||
|
|
||||||
|
from backend.execution_context import execution_callbacks, active_node
|
||||||
captured = []
|
captured = []
|
||||||
Stats._broadcast_value_fn = lambda nid, payload: captured.append(payload)
|
with execution_callbacks(value=lambda nid, payload: captured.append(payload)), active_node("test"):
|
||||||
Stats._current_node_id = "test"
|
stats = Stats()
|
||||||
stats = Stats()
|
mean_value, = stats.process(profile, operation="mean", column="value")
|
||||||
mean_value, = stats.process(profile, operation="mean", column="value")
|
assert mean_value > 0
|
||||||
assert mean_value > 0
|
assert captured[-1]["unit"] == field.si_unit_z
|
||||||
assert captured[-1]["unit"] == field.si_unit_z
|
|
||||||
Stats._broadcast_value_fn = None
|
|
||||||
|
|||||||
@@ -12,53 +12,50 @@ def test_line_cursors():
|
|||||||
|
|
||||||
line = np.linspace(0, 10, 100).astype(np.float64)
|
line = np.linspace(0, 10, 100).astype(np.float64)
|
||||||
|
|
||||||
|
from backend.execution_context import execution_callbacks, active_node
|
||||||
overlays = []
|
overlays = []
|
||||||
Cursors._broadcast_overlay_fn = lambda nid, data: overlays.append(data)
|
with execution_callbacks(overlay=lambda nid, data: overlays.append(data)), active_node("test"):
|
||||||
Cursors._current_node_id = "test"
|
table, coord_pair = node.process(line, x1=0.25, y1=0.5, x2=0.75, y2=0.5)
|
||||||
|
assert isinstance(coord_pair, tuple) and len(coord_pair) == 2
|
||||||
|
assert len(table) == 7
|
||||||
|
quantities = {row["quantity"] for row in table}
|
||||||
|
assert "Length" in quantities
|
||||||
|
assert "A x" in quantities
|
||||||
|
assert "B x" in quantities
|
||||||
|
assert "dx" in quantities
|
||||||
|
assert "dy" in quantities
|
||||||
|
|
||||||
table, coord_pair = node.process(line, x1=0.25, y1=0.5, x2=0.75, y2=0.5)
|
a_pos = next(r["value"] for r in table if r["quantity"] == "A x")
|
||||||
assert isinstance(coord_pair, tuple) and len(coord_pair) == 2
|
b_pos = next(r["value"] for r in table if r["quantity"] == "B x")
|
||||||
assert len(table) == 7
|
assert b_pos > a_pos
|
||||||
quantities = {row["quantity"] for row in table}
|
|
||||||
assert "Length" in quantities
|
|
||||||
assert "A x" in quantities
|
|
||||||
assert "B x" in quantities
|
|
||||||
assert "dx" in quantities
|
|
||||||
assert "dy" in quantities
|
|
||||||
|
|
||||||
a_pos = next(r["value"] for r in table if r["quantity"] == "A x")
|
dy = next(r["value"] for r in table if r["quantity"] == "dy")
|
||||||
b_pos = next(r["value"] for r in table if r["quantity"] == "B x")
|
assert dy > 0
|
||||||
assert b_pos > a_pos
|
|
||||||
|
|
||||||
dy = next(r["value"] for r in table if r["quantity"] == "dy")
|
assert len(overlays) == 1
|
||||||
assert dy > 0
|
assert overlays[0]["kind"] == "line_plot"
|
||||||
|
assert len(overlays[0]["line"]) == len(line)
|
||||||
|
assert len(overlays[0]["x_axis"]) == len(line)
|
||||||
|
assert 0.0 <= overlays[0]["x1"] <= 1.0
|
||||||
|
assert 0.0 <= overlays[0]["x2"] <= 1.0
|
||||||
|
|
||||||
assert len(overlays) == 1
|
line_data = LineData(data=line, x_axis=np.linspace(0, 1, 100))
|
||||||
assert overlays[0]["kind"] == "line_plot"
|
table2, _ = node.process(line_data, x1=0.25, y1=0.5, x2=0.75, y2=0.5)
|
||||||
assert len(overlays[0]["line"]) == len(line)
|
assert len(table2) == 7
|
||||||
assert len(overlays[0]["x_axis"]) == len(line)
|
|
||||||
assert 0.0 <= overlays[0]["x1"] <= 1.0
|
|
||||||
assert 0.0 <= overlays[0]["x2"] <= 1.0
|
|
||||||
|
|
||||||
line_data = LineData(data=line, x_axis=np.linspace(0, 1, 100))
|
field = DataField(
|
||||||
table2, _ = node.process(line_data, x1=0.25, y1=0.5, x2=0.75, y2=0.5)
|
data=np.arange(100, dtype=np.float64).reshape(10, 10),
|
||||||
assert len(table2) == 7
|
xreal=2.0, yreal=4.0, si_unit_xy="um", si_unit_z="nm",
|
||||||
|
)
|
||||||
field = DataField(
|
overlays.clear()
|
||||||
data=np.arange(100, dtype=np.float64).reshape(10, 10),
|
table3, _ = node.process(field, x1=0.2, y1=0.25, x2=0.7, y2=0.75)
|
||||||
xreal=2.0, yreal=4.0, si_unit_xy="um", si_unit_z="nm",
|
assert len(table3) == 9
|
||||||
)
|
field_rows = {row["quantity"]: row for row in table3}
|
||||||
overlays.clear()
|
assert field_rows["dx"]["unit"] == "um"
|
||||||
table3, _ = node.process(field, x1=0.2, y1=0.25, x2=0.7, y2=0.75)
|
assert field_rows["dy"]["unit"] == "um"
|
||||||
assert len(table3) == 9
|
assert field_rows["dz"]["unit"] == "nm"
|
||||||
field_rows = {row["quantity"]: row for row in table3}
|
assert np.isclose(field_rows["dx"]["value"], 1.0)
|
||||||
assert field_rows["dx"]["unit"] == "um"
|
assert np.isclose(field_rows["dy"]["value"], 2.0)
|
||||||
assert field_rows["dy"]["unit"] == "um"
|
assert len(overlays) == 1
|
||||||
assert field_rows["dz"]["unit"] == "nm"
|
assert overlays[0]["kind"] == "cursor_points"
|
||||||
assert np.isclose(field_rows["dx"]["value"], 1.0)
|
assert overlays[0]["image"].startswith("data:image/png;base64,")
|
||||||
assert np.isclose(field_rows["dy"]["value"], 2.0)
|
|
||||||
assert len(overlays) == 1
|
|
||||||
assert overlays[0]["kind"] == "cursor_points"
|
|
||||||
assert overlays[0]["image"].startswith("data:image/png;base64,")
|
|
||||||
|
|
||||||
Cursors._broadcast_overlay_fn = None
|
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ def test_measurement_value_errors():
|
|||||||
|
|
||||||
|
|
||||||
def test_format_with_unit():
|
def test_format_with_unit():
|
||||||
from backend.nodes.helpers import _format_with_unit, _format_numeric
|
from backend.data_types import _format_with_unit, _format_numeric
|
||||||
|
|
||||||
assert _format_numeric(0.0) == "0"
|
assert _format_numeric(0.0) == "0"
|
||||||
assert not np.isfinite(float('inf')) or _format_numeric(float('inf')) is not None
|
assert not np.isfinite(float('inf')) or _format_numeric(float('inf')) is not None
|
||||||
@@ -182,7 +182,7 @@ def test_square_unit_and_apply():
|
|||||||
|
|
||||||
|
|
||||||
def test_nice_length():
|
def test_nice_length():
|
||||||
from backend.nodes.helpers import _nice_length
|
from backend.data_types import _nice_length
|
||||||
|
|
||||||
assert _nice_length(0.0) == 0.0
|
assert _nice_length(0.0) == 0.0
|
||||||
assert _nice_length(float('inf')) == 0.0
|
assert _nice_length(float('inf')) == 0.0
|
||||||
|
|||||||
@@ -9,32 +9,29 @@ def test_height_histogram():
|
|||||||
data = np.linspace(0, 1, 1000).reshape(25, 40)
|
data = np.linspace(0, 1, 1000).reshape(25, 40)
|
||||||
field = make_field(data=data)
|
field = make_field(data=data)
|
||||||
|
|
||||||
|
from backend.execution_context import execution_callbacks, active_node
|
||||||
overlays = []
|
overlays = []
|
||||||
Histogram._broadcast_overlay_fn = lambda nid, data: overlays.append(data)
|
with execution_callbacks(overlay=lambda nid, data: overlays.append(data)), active_node("test"):
|
||||||
Histogram._current_node_id = "test"
|
table, coord_pair = node.process(field, n_bins=10, y_scale="linear", x1=0.2, y1=0.5, x2=0.8, y2=0.5)
|
||||||
|
assert isinstance(coord_pair, tuple) and len(coord_pair) == 2
|
||||||
table, coord_pair = node.process(field, n_bins=10, y_scale="linear", x1=0.2, y1=0.5, x2=0.8, y2=0.5)
|
measurements = {row["quantity"]: row for row in table}
|
||||||
assert isinstance(coord_pair, tuple) and len(coord_pair) == 2
|
assert "A position" in measurements
|
||||||
measurements = {row["quantity"]: row for row in table}
|
assert "A count" in measurements
|
||||||
assert "A position" in measurements
|
assert "B position" in measurements
|
||||||
assert "A count" in measurements
|
assert "B count" in measurements
|
||||||
assert "B position" in measurements
|
assert "delta X" in measurements
|
||||||
assert "B count" in measurements
|
assert "delta Y" in measurements
|
||||||
assert "delta X" in measurements
|
assert measurements["A count"]["unit"] == "count"
|
||||||
assert "delta Y" in measurements
|
assert measurements["B count"]["unit"] == "count"
|
||||||
assert measurements["A count"]["unit"] == "count"
|
assert measurements["B position"]["value"] > measurements["A position"]["value"]
|
||||||
assert measurements["B count"]["unit"] == "count"
|
assert len(overlays) == 1
|
||||||
assert measurements["B position"]["value"] > measurements["A position"]["value"]
|
assert overlays[0]["kind"] == "line_plot"
|
||||||
assert len(overlays) == 1
|
assert overlays[0]["section_title"] == "Histogram"
|
||||||
assert overlays[0]["kind"] == "line_plot"
|
assert len(overlays[0]["line"]) == 10
|
||||||
assert overlays[0]["section_title"] == "Histogram"
|
assert len(overlays[0]["x_axis"]) == 10
|
||||||
assert len(overlays[0]["line"]) == 10
|
assert np.isclose(overlays[0]["x1"], 0.2)
|
||||||
assert len(overlays[0]["x_axis"]) == 10
|
assert np.isclose(overlays[0]["x2"], 0.8)
|
||||||
assert np.isclose(overlays[0]["x1"], 0.2)
|
assert np.isclose(
|
||||||
assert np.isclose(overlays[0]["x2"], 0.8)
|
measurements["delta Y"]["value"],
|
||||||
assert np.isclose(
|
measurements["B count"]["value"] - measurements["A count"]["value"],
|
||||||
measurements["delta Y"]["value"],
|
)
|
||||||
measurements["B count"]["value"] - measurements["A count"]["value"],
|
|
||||||
)
|
|
||||||
|
|
||||||
Histogram._broadcast_overlay_fn = None
|
|
||||||
|
|||||||
@@ -126,12 +126,13 @@ def test_load_file_unsupported():
|
|||||||
|
|
||||||
def test_load_file_warning():
|
def test_load_file_warning():
|
||||||
from backend.nodes.image import Image as ImageNode
|
from backend.nodes.image import Image as ImageNode
|
||||||
|
from backend.execution_context import execution_callbacks, active_node
|
||||||
node = ImageNode()
|
node = ImageNode()
|
||||||
warnings = []
|
warnings = []
|
||||||
ImageNode._broadcast_warning_fn = lambda nid, msg: warnings.append(msg)
|
|
||||||
ImageNode._current_node_id = "test"
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir, \
|
||||||
|
execution_callbacks(warning=lambda nid, msg: warnings.append(msg)), \
|
||||||
|
active_node("test"):
|
||||||
arr = np.random.default_rng(10).integers(0, 256, (16, 16), dtype=np.uint8)
|
arr = np.random.default_rng(10).integers(0, 256, (16, 16), dtype=np.uint8)
|
||||||
img = PILImage.fromarray(arr)
|
img = PILImage.fromarray(arr)
|
||||||
path = os.path.join(tmpdir, "test.png")
|
path = os.path.join(tmpdir, "test.png")
|
||||||
@@ -142,8 +143,6 @@ def test_load_file_warning():
|
|||||||
assert len(warnings) == 1
|
assert len(warnings) == 1
|
||||||
assert "Uncalibrated" in warnings[0]
|
assert "Uncalibrated" in warnings[0]
|
||||||
|
|
||||||
ImageNode._broadcast_warning_fn = None
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_file_ibw():
|
def test_load_file_ibw():
|
||||||
from backend.nodes.image import Image
|
from backend.nodes.image import Image
|
||||||
|
|||||||
@@ -8,33 +8,30 @@ def test_draw_mask():
|
|||||||
node = DrawMask()
|
node = DrawMask()
|
||||||
|
|
||||||
field = make_field(data=np.zeros((32, 32), dtype=np.float64))
|
field = make_field(data=np.zeros((32, 32), dtype=np.float64))
|
||||||
|
from backend.execution_context import execution_callbacks, active_node
|
||||||
overlays = []
|
overlays = []
|
||||||
DrawMask._broadcast_overlay_fn = lambda nid, data: overlays.append(data)
|
with execution_callbacks(overlay=lambda nid, data: overlays.append(data)), active_node("test"):
|
||||||
DrawMask._current_node_id = "test"
|
mask_paths = [{"size": 5, "points": [{"x": 0.2, "y": 0.5}, {"x": 0.8, "y": 0.5}]}]
|
||||||
|
|
||||||
mask_paths = [{"size": 5, "points": [{"x": 0.2, "y": 0.5}, {"x": 0.8, "y": 0.5}]}]
|
mask, = node.process(field, pen_size=2, invert=False, mask_paths=json.dumps(mask_paths))
|
||||||
|
assert mask.dtype == np.uint8
|
||||||
|
assert mask.shape == (32, 32)
|
||||||
|
assert mask[16, 16] == 255
|
||||||
|
assert mask[14, 16] == 255
|
||||||
|
assert mask[0, 0] == 0
|
||||||
|
|
||||||
mask, = node.process(field, pen_size=2, invert=False, mask_paths=json.dumps(mask_paths))
|
assert len(overlays) == 1
|
||||||
assert mask.dtype == np.uint8
|
assert overlays[0]["kind"] == "mask_paint"
|
||||||
assert mask.shape == (32, 32)
|
assert overlays[0]["section_title"] == "Mask"
|
||||||
assert mask[16, 16] == 255
|
assert overlays[0]["image"].startswith("data:image/png;base64,")
|
||||||
assert mask[14, 16] == 255
|
assert overlays[0]["image_width"] == field.xres
|
||||||
assert mask[0, 0] == 0
|
assert overlays[0]["image_height"] == field.yres
|
||||||
|
assert overlays[0]["invert"] is False
|
||||||
|
|
||||||
assert len(overlays) == 1
|
inverted, = node.process(field, pen_size=2, invert=True, mask_paths=json.dumps(mask_paths))
|
||||||
assert overlays[0]["kind"] == "mask_paint"
|
assert inverted[16, 16] == 0
|
||||||
assert overlays[0]["section_title"] == "Mask"
|
assert inverted[0, 0] == 255
|
||||||
assert overlays[0]["image"].startswith("data:image/png;base64,")
|
assert overlays[-1]["invert"] is True
|
||||||
assert overlays[0]["image_width"] == field.xres
|
|
||||||
assert overlays[0]["image_height"] == field.yres
|
|
||||||
assert overlays[0]["invert"] is False
|
|
||||||
|
|
||||||
inverted, = node.process(field, pen_size=2, invert=True, mask_paths=json.dumps(mask_paths))
|
cleared, = node.process(field, pen_size=12, invert=False, mask_paths="[]")
|
||||||
assert inverted[16, 16] == 0
|
assert np.count_nonzero(cleared) == 0
|
||||||
assert inverted[0, 0] == 255
|
|
||||||
assert overlays[-1]["invert"] is True
|
|
||||||
|
|
||||||
cleared, = node.process(field, pen_size=12, invert=False, mask_paths="[]")
|
|
||||||
assert np.count_nonzero(cleared) == 0
|
|
||||||
|
|
||||||
DrawMask._broadcast_overlay_fn = None
|
|
||||||
|
|||||||
@@ -10,30 +10,27 @@ def test_threshold_mask():
|
|||||||
data[:, 32:] = 1.0
|
data[:, 32:] = 1.0
|
||||||
field = make_field(data=data)
|
field = make_field(data=data)
|
||||||
|
|
||||||
|
from backend.execution_context import execution_callbacks, active_node
|
||||||
previews = []
|
previews = []
|
||||||
ThresholdMask._broadcast_fn = lambda nid, uri: previews.append(uri)
|
with execution_callbacks(preview=lambda nid, uri: previews.append(uri)), active_node("test"):
|
||||||
ThresholdMask._current_node_id = "test"
|
mask, table = node.process(field, method="absolute", threshold=0.5, direction="above")
|
||||||
|
assert mask.dtype == np.uint8
|
||||||
|
assert mask.shape == (64, 64)
|
||||||
|
assert np.all(mask[:, :32] == 0)
|
||||||
|
assert np.all(mask[:, 32:] == 255)
|
||||||
|
|
||||||
mask, table = node.process(field, method="absolute", threshold=0.5, direction="above")
|
assert len(previews) == 1
|
||||||
assert mask.dtype == np.uint8
|
assert previews[0].startswith("data:image/png;base64,")
|
||||||
assert mask.shape == (64, 64)
|
|
||||||
assert np.all(mask[:, :32] == 0)
|
|
||||||
assert np.all(mask[:, 32:] == 255)
|
|
||||||
|
|
||||||
assert len(previews) == 1
|
mask_below, _ = node.process(field, method="absolute", threshold=0.5, direction="below")
|
||||||
assert previews[0].startswith("data:image/png;base64,")
|
assert np.all(mask_below[:, :32] == 255)
|
||||||
|
assert np.all(mask_below[:, 32:] == 0)
|
||||||
|
|
||||||
mask_below, _ = node.process(field, method="absolute", threshold=0.5, direction="below")
|
mask_rel, _ = node.process(field, method="relative", threshold=0.5, direction="above")
|
||||||
assert np.all(mask_below[:, :32] == 255)
|
assert np.all(mask_rel[:, 32:] == 255)
|
||||||
assert np.all(mask_below[:, 32:] == 0)
|
|
||||||
|
|
||||||
mask_rel, _ = node.process(field, method="relative", threshold=0.5, direction="above")
|
mask_otsu, _ = node.process(field, method="otsu", threshold=0.0, direction="above")
|
||||||
assert np.all(mask_rel[:, 32:] == 255)
|
assert mask_otsu[:, 32:].sum() > mask_otsu[:, :32].sum()
|
||||||
|
|
||||||
mask_otsu, _ = node.process(field, method="otsu", threshold=0.0, direction="above")
|
|
||||||
assert mask_otsu[:, 32:].sum() > mask_otsu[:, :32].sum()
|
|
||||||
|
|
||||||
ThresholdMask._broadcast_fn = None
|
|
||||||
|
|
||||||
|
|
||||||
def test_threshold_mask_unknown_method():
|
def test_threshold_mask_unknown_method():
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
def test_print_table():
|
def test_print_table():
|
||||||
from backend.nodes.print_table import PrintTable
|
from backend.nodes.print_table import PrintTable
|
||||||
|
from backend.execution_context import execution_callbacks, active_node
|
||||||
node = PrintTable()
|
node = PrintTable()
|
||||||
|
|
||||||
table_spec = PrintTable.INPUT_TYPES()["required"]["table"]
|
table_spec = PrintTable.INPUT_TYPES()["required"]["table"]
|
||||||
@@ -7,12 +8,8 @@ def test_print_table():
|
|||||||
assert table_spec[1]["accepted_types"] == ["DATA_TABLE"]
|
assert table_spec[1]["accepted_types"] == ["DATA_TABLE"]
|
||||||
|
|
||||||
captured = []
|
captured = []
|
||||||
PrintTable._broadcast_table_fn = lambda node_id, rows: captured.append(rows)
|
with execution_callbacks(table=lambda nid, rows: captured.append(rows)), active_node("test"):
|
||||||
PrintTable._current_node_id = "test"
|
table = [{"quantity": "test", "value": 42.0, "unit": "m"}]
|
||||||
|
node.print_table(table=table)
|
||||||
table = [{"quantity": "test", "value": 42.0, "unit": "m"}]
|
assert len(captured) == 1
|
||||||
node.print_table(table=table)
|
assert captured[0] == table
|
||||||
assert len(captured) == 1
|
|
||||||
assert captured[0] == table
|
|
||||||
|
|
||||||
PrintTable._broadcast_table_fn = None
|
|
||||||
|
|||||||
@@ -42,22 +42,20 @@ def test_rotate_field():
|
|||||||
def test_rotate_field_overlay_warning():
|
def test_rotate_field_overlay_warning():
|
||||||
from backend.nodes.rotate import RotateField
|
from backend.nodes.rotate import RotateField
|
||||||
|
|
||||||
|
from backend.execution_context import execution_callbacks, active_node
|
||||||
node = RotateField()
|
node = RotateField()
|
||||||
warnings = []
|
warnings = []
|
||||||
RotateField._broadcast_warning_fn = lambda nid, msg: warnings.append(msg)
|
|
||||||
RotateField._current_node_id = "test"
|
|
||||||
|
|
||||||
field = DataField(
|
field = DataField(
|
||||||
data=np.arange(16, dtype=np.float64).reshape(4, 4),
|
data=np.arange(16, dtype=np.float64).reshape(4, 4),
|
||||||
overlays=[{"kind": "markup", "shapes": [{"kind": "line", "x1": 0.1, "y1": 0.1, "x2": 0.9, "y2": 0.9, "width": 2, "color": "#ffffff"}]}],
|
overlays=[{"kind": "markup", "shapes": [{"kind": "line", "x1": 0.1, "y1": 0.1, "x2": 0.9, "y2": 0.9, "width": 2, "color": "#ffffff"}]}],
|
||||||
)
|
)
|
||||||
|
|
||||||
rotated, = node.process(field, angle=30.0, interpolation="bilinear", expand_canvas=True)
|
with execution_callbacks(warning=lambda nid, msg: warnings.append(msg)), active_node("test"):
|
||||||
assert rotated.overlays == []
|
rotated, = node.process(field, angle=30.0, interpolation="bilinear", expand_canvas=True)
|
||||||
assert len(warnings) == 1
|
assert rotated.overlays == []
|
||||||
assert "clears annotation/markup overlays" in warnings[0]
|
assert len(warnings) == 1
|
||||||
|
assert "clears annotation/markup overlays" in warnings[0]
|
||||||
RotateField._broadcast_warning_fn = None
|
|
||||||
|
|
||||||
|
|
||||||
def test_rotate_unknown_interpolation():
|
def test_rotate_unknown_interpolation():
|
||||||
|
|||||||
@@ -11,61 +11,58 @@ def test_stats():
|
|||||||
assert input_spec[0] == "DATA_FIELD"
|
assert input_spec[0] == "DATA_FIELD"
|
||||||
assert input_spec[1]["accepted_types"] == ["IMAGE", "LINE", "DATA_TABLE"]
|
assert input_spec[1]["accepted_types"] == ["IMAGE", "LINE", "DATA_TABLE"]
|
||||||
|
|
||||||
|
from backend.execution_context import execution_callbacks, active_node
|
||||||
captured = []
|
captured = []
|
||||||
Stats._broadcast_value_fn = lambda node_id, payload: captured.append((node_id, payload))
|
with execution_callbacks(value=lambda nid, payload: captured.append((nid, payload))), active_node("test"):
|
||||||
Stats._current_node_id = "test"
|
line = np.array([1.0, 2.0, 3.0, 4.0], dtype=np.float64)
|
||||||
|
result, = node.process(line, operation="mean", column="value")
|
||||||
|
assert np.isclose(result, 2.5)
|
||||||
|
assert captured[-1] == ("test", {"value": result})
|
||||||
|
roughness, = node.process(line, operation="Rq", column="value")
|
||||||
|
assert np.isclose(roughness, np.sqrt(np.mean((line - line.mean()) ** 2)))
|
||||||
|
|
||||||
line = np.array([1.0, 2.0, 3.0, 4.0], dtype=np.float64)
|
table = DataTable([
|
||||||
result, = node.process(line, operation="mean", column="value")
|
{"name": "a", "value": 3.0, "unit": "m", "other": 10.0},
|
||||||
assert np.isclose(result, 2.5)
|
{"name": "b", "value": 7.0, "unit": "m", "other": 20.0},
|
||||||
assert captured[-1] == ("test", {"value": result})
|
])
|
||||||
roughness, = node.process(line, operation="Rq", column="value")
|
result, = node.process(table, operation="max", column="value")
|
||||||
assert np.isclose(roughness, np.sqrt(np.mean((line - line.mean()) ** 2)))
|
assert result == 7.0
|
||||||
|
assert captured[-1] == ("test", {"value": 7.0, "unit": "m"})
|
||||||
|
count, = node.process(table, operation="count", column="other")
|
||||||
|
assert count == 2.0
|
||||||
|
auto_column_range, = node.process(table, operation="range", column="")
|
||||||
|
assert auto_column_range == 4.0
|
||||||
|
|
||||||
table = DataTable([
|
field = make_field(data=np.array([[1.0, 5.0], [2.0, 4.0]], dtype=np.float64))
|
||||||
{"name": "a", "value": 3.0, "unit": "m", "other": 10.0},
|
result, = node.process(field, operation="range", column="value")
|
||||||
{"name": "b", "value": 7.0, "unit": "m", "other": 20.0},
|
assert result == 4.0
|
||||||
])
|
assert captured[-1] == ("test", {"value": 4.0, "unit": "m"})
|
||||||
result, = node.process(table, operation="max", column="value")
|
|
||||||
assert result == 7.0
|
|
||||||
assert captured[-1] == ("test", {"value": 7.0, "unit": "m"})
|
|
||||||
count, = node.process(table, operation="count", column="other")
|
|
||||||
assert count == 2.0
|
|
||||||
auto_column_range, = node.process(table, operation="range", column="")
|
|
||||||
assert auto_column_range == 4.0
|
|
||||||
|
|
||||||
field = make_field(data=np.array([[1.0, 5.0], [2.0, 4.0]], dtype=np.float64))
|
image = np.array([[0, 10], [20, 30]], dtype=np.uint8)
|
||||||
result, = node.process(field, operation="range", column="value")
|
result, = node.process(image, operation="avg", column="value")
|
||||||
assert result == 4.0
|
assert np.isclose(result, 15.0)
|
||||||
assert captured[-1] == ("test", {"value": 4.0, "unit": "m"})
|
assert captured[-1] == ("test", {"value": 15.0})
|
||||||
|
|
||||||
image = np.array([[0, 10], [20, 30]], dtype=np.uint8)
|
try:
|
||||||
result, = node.process(image, operation="avg", column="value")
|
node.process(table, operation="Rq", column="value")
|
||||||
assert np.isclose(result, 15.0)
|
raise AssertionError("Expected invalid TABLE operation to raise ValueError")
|
||||||
assert captured[-1] == ("test", {"value": 15.0})
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
node.process(table, operation="Rq", column="value")
|
node.process([{"label": "only text"}], operation="max", column="label")
|
||||||
raise AssertionError("Expected invalid TABLE operation to raise ValueError")
|
raise AssertionError("Expected non-numeric record-table input to raise ValueError")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
node.process([{"label": "only text"}], operation="max", column="label")
|
node.process(
|
||||||
raise AssertionError("Expected non-numeric record-table input to raise ValueError")
|
RecordTable([{"quantity": "min", "value": 1.0, "unit": "m"}]),
|
||||||
except ValueError:
|
operation="max", column="value",
|
||||||
pass
|
)
|
||||||
|
raise AssertionError("Expected measurement table input to raise ValueError")
|
||||||
try:
|
except ValueError:
|
||||||
node.process(
|
pass
|
||||||
RecordTable([{"quantity": "min", "value": 1.0, "unit": "m"}]),
|
|
||||||
operation="max", column="value",
|
|
||||||
)
|
|
||||||
raise AssertionError("Expected measurement table input to raise ValueError")
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
Stats._broadcast_value_fn = None
|
|
||||||
|
|
||||||
|
|
||||||
def test_stats_empty_inputs():
|
def test_stats_empty_inputs():
|
||||||
|
|||||||
@@ -11,23 +11,20 @@ def test_value_display():
|
|||||||
assert value_spec[0] == "FLOAT"
|
assert value_spec[0] == "FLOAT"
|
||||||
assert value_spec[1]["accepted_types"] == ["RECORD_TABLE"]
|
assert value_spec[1]["accepted_types"] == ["RECORD_TABLE"]
|
||||||
|
|
||||||
|
from backend.execution_context import execution_callbacks, active_node
|
||||||
captured = []
|
captured = []
|
||||||
ValueIO._broadcast_value_fn = lambda node_id, payload: captured.append((node_id, payload))
|
with execution_callbacks(value=lambda nid, payload: captured.append((nid, payload))), active_node("test"):
|
||||||
ValueIO._current_node_id = "test"
|
result = node.display_value(value=3.25)
|
||||||
|
assert result == (3.25,)
|
||||||
|
assert captured == [("test", {"value": 3.25})]
|
||||||
|
|
||||||
result = node.display_value(value=3.25)
|
measurements = RecordTable([
|
||||||
assert result == (3.25,)
|
{"quantity": "delta X", "value": 1.7e-7, "unit": "m"},
|
||||||
assert captured == [("test", {"value": 3.25})]
|
{"quantity": "delta Y", "value": 463, "unit": "count"},
|
||||||
|
])
|
||||||
measurements = RecordTable([
|
result = node.display_value(value=measurements, measurement="delta X")
|
||||||
{"quantity": "delta X", "value": 1.7e-7, "unit": "m"},
|
assert result == (1.7e-7,)
|
||||||
{"quantity": "delta Y", "value": 463, "unit": "count"},
|
assert captured[-1] == ("test", {"value": 1.7e-7, "unit": "m"})
|
||||||
])
|
|
||||||
result = node.display_value(value=measurements, measurement="delta X")
|
|
||||||
assert result == (1.7e-7,)
|
|
||||||
assert captured[-1] == ("test", {"value": 1.7e-7, "unit": "m"})
|
|
||||||
|
|
||||||
ValueIO._broadcast_value_fn = None
|
|
||||||
|
|
||||||
|
|
||||||
def test_value_display_string_input():
|
def test_value_display_string_input():
|
||||||
|
|||||||
Reference in New Issue
Block a user