modularize style and add propagating widgets
This commit is contained in:
@@ -39,6 +39,34 @@ class MeasureTable(list):
|
|||||||
"""Named scalar measurements, typically rows of quantity/value/unit."""
|
"""Named scalar measurements, typically rows of quantity/value/unit."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LineData:
|
||||||
|
data: np.ndarray
|
||||||
|
x_axis: np.ndarray | None = None
|
||||||
|
x_unit: str = ""
|
||||||
|
y_unit: str = ""
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
self.data = np.asarray(self.data, dtype=np.float64).ravel()
|
||||||
|
if self.x_axis is not None:
|
||||||
|
axis = np.asarray(self.x_axis, dtype=np.float64).ravel()
|
||||||
|
self.x_axis = axis[: len(self.data)]
|
||||||
|
else:
|
||||||
|
self.x_axis = None
|
||||||
|
|
||||||
|
def __array__(self, dtype=None):
|
||||||
|
return np.asarray(self.data, dtype=dtype) if dtype is not None else self.data
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return len(self.data)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self.data)
|
||||||
|
|
||||||
|
def __getitem__(self, item):
|
||||||
|
return self.data[item]
|
||||||
|
|
||||||
|
|
||||||
def _normalize_hex_color(color: Any, default: str = "#000000") -> str:
|
def _normalize_hex_color(color: Any, default: str = "#000000") -> str:
|
||||||
if isinstance(color, str):
|
if isinstance(color, str):
|
||||||
text = color.strip()
|
text = color.strip()
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ class ExecutionEngine:
|
|||||||
"""
|
"""
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from backend.data_types import (
|
from backend.data_types import (
|
||||||
DataField, image_to_uint8, encode_preview, render_datafield_preview,
|
DataField, LineData, image_to_uint8, encode_preview, render_datafield_preview,
|
||||||
)
|
)
|
||||||
from backend.nodes.io import Image, ImageDemo
|
from backend.nodes.io import Image, ImageDemo
|
||||||
|
|
||||||
@@ -302,7 +302,7 @@ class ExecutionEngine:
|
|||||||
on_preview(node_id, encode_preview(arr))
|
on_preview(node_id, encode_preview(arr))
|
||||||
return
|
return
|
||||||
|
|
||||||
if type_name == "LINE" and isinstance(value, np.ndarray) and on_preview:
|
if type_name == "LINE" and isinstance(value, (np.ndarray, LineData)) and on_preview:
|
||||||
preview = self._render_line_preview(cls, slot, result)
|
preview = self._render_line_preview(cls, slot, result)
|
||||||
if preview:
|
if preview:
|
||||||
on_preview(node_id, preview)
|
on_preview(node_id, preview)
|
||||||
@@ -354,6 +354,7 @@ class ExecutionEngine:
|
|||||||
) -> dict | None:
|
) -> dict | None:
|
||||||
"""Return structured LINE preview data for responsive frontend rendering."""
|
"""Return structured LINE preview data for responsive frontend rendering."""
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from backend.data_types import LineData
|
||||||
|
|
||||||
return_types = getattr(cls, "RETURN_TYPES", ())
|
return_types = getattr(cls, "RETURN_TYPES", ())
|
||||||
|
|
||||||
@@ -374,7 +375,10 @@ class ExecutionEngine:
|
|||||||
matplotlib.use("Agg")
|
matplotlib.use("Agg")
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
y_meta = y if isinstance(y, LineData) else None
|
||||||
y = np.asarray(y, dtype=np.float64).ravel()
|
y = np.asarray(y, dtype=np.float64).ravel()
|
||||||
|
if x is None and y_meta is not None and y_meta.x_axis is not None:
|
||||||
|
x = y_meta.x_axis
|
||||||
if x is None:
|
if x is None:
|
||||||
x = np.arange(len(y), dtype=np.float64)
|
x = np.arange(len(y), dtype=np.float64)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ MENU_LAYOUT: dict[str, list[str]] = {
|
|||||||
"Number",
|
"Number",
|
||||||
"RangeSlider",
|
"RangeSlider",
|
||||||
"Coordinate",
|
"Coordinate",
|
||||||
|
"CoordinatePair",
|
||||||
"Font",
|
"Font",
|
||||||
],
|
],
|
||||||
"Output": [
|
"Output": [
|
||||||
@@ -55,10 +56,10 @@ MENU_LAYOUT: dict[str, list[str]] = {
|
|||||||
"FixZero",
|
"FixZero",
|
||||||
],
|
],
|
||||||
"Measure": [
|
"Measure": [
|
||||||
"Statistics",
|
|
||||||
"Histogram",
|
|
||||||
"CrossSection",
|
"CrossSection",
|
||||||
|
"Histogram",
|
||||||
"Cursors",
|
"Cursors",
|
||||||
|
"Statistics",
|
||||||
"Stats",
|
"Stats",
|
||||||
],
|
],
|
||||||
"Mask": [
|
"Mask": [
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ from __future__ import annotations
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from backend.node_registry import register_node
|
from backend.node_registry import register_node
|
||||||
from backend.data_types import DataField, MeasureTable, RecordTable, datafield_to_uint8, encode_preview, render_datafield_preview
|
from backend.data_types import DataField, LineData, MeasureTable, RecordTable, datafield_to_uint8, encode_preview, render_datafield_preview
|
||||||
|
from backend.nodes.io import Coordinate, CoordinatePair
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -62,7 +63,7 @@ class Statistics:
|
|||||||
# Histogram
|
# Histogram
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@register_node(display_name="Height Histogram")
|
@register_node(display_name="Histogram")
|
||||||
class Histogram:
|
class Histogram:
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
@@ -78,8 +79,8 @@ class Histogram:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RETURN_TYPES = ("MEASURE_TABLE",)
|
RETURN_TYPES = ("MEASURE_TABLE", "COORDPAIR",)
|
||||||
RETURN_NAMES = ("measurements",)
|
RETURN_NAMES = ("measurements", "marker pair",)
|
||||||
FUNCTION = "process"
|
FUNCTION = "process"
|
||||||
CATEGORY = "analysis"
|
CATEGORY = "analysis"
|
||||||
DESCRIPTION = (
|
DESCRIPTION = (
|
||||||
@@ -155,7 +156,7 @@ class Histogram:
|
|||||||
{"quantity": "delta X", "value": xb - xa, "unit": field.si_unit_z},
|
{"quantity": "delta X", "value": xb - xa, "unit": field.si_unit_z},
|
||||||
{"quantity": "delta Y", "value": yb - ya, "unit": count_unit},
|
{"quantity": "delta Y", "value": yb - ya, "unit": count_unit},
|
||||||
])
|
])
|
||||||
return (table,)
|
return (table, ((x1, y1), (x2, y2)))
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -177,12 +178,12 @@ class Cursors:
|
|||||||
"y2": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01, "hidden": True}),
|
"y2": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01, "hidden": True}),
|
||||||
},
|
},
|
||||||
"optional": {
|
"optional": {
|
||||||
"x_axis": ("LINE",),
|
"coord_pair": ("COORDPAIR", {"label": "coord pair"}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
RETURN_TYPES = ("MEASURE_TABLE",)
|
RETURN_TYPES = ("MEASURE_TABLE","COORDPAIR",)
|
||||||
RETURN_NAMES = ("measurement",)
|
RETURN_NAMES = ("measurement","coord pair",)
|
||||||
FUNCTION = "process"
|
FUNCTION = "process"
|
||||||
CATEGORY = "analysis"
|
CATEGORY = "analysis"
|
||||||
DESCRIPTION = (
|
DESCRIPTION = (
|
||||||
@@ -196,12 +197,17 @@ class Cursors:
|
|||||||
|
|
||||||
def process(
|
def process(
|
||||||
self, line, x1: float, y1: float, x2: float, y2: float,
|
self, line, x1: float, y1: float, x2: float, y2: float,
|
||||||
x_axis=None,
|
coord_pair=None,
|
||||||
) -> tuple:
|
) -> tuple:
|
||||||
if isinstance(line, DataField):
|
if coord_pair is not None:
|
||||||
return self._process_field(line, x1=x1, y1=y1, x2=x2, y2=y2)
|
(x1, y1), (x2, y2) = coord_pair
|
||||||
|
|
||||||
return self._process_line(line, x1=x1, y1=y1, x2=x2, y2=y2, x_axis=x_axis)
|
locked = coord_pair is not None
|
||||||
|
|
||||||
|
if isinstance(line, DataField):
|
||||||
|
return self._process_field(line, x1=x1, y1=y1, x2=x2, y2=y2, locked=locked)
|
||||||
|
|
||||||
|
return self._process_line(line, x1=x1, y1=y1, x2=x2, y2=y2, locked=locked)
|
||||||
|
|
||||||
def _process_line(
|
def _process_line(
|
||||||
self,
|
self,
|
||||||
@@ -210,12 +216,14 @@ class Cursors:
|
|||||||
y1: float,
|
y1: float,
|
||||||
x2: float,
|
x2: float,
|
||||||
y2: float,
|
y2: float,
|
||||||
x_axis=None,
|
locked: bool = False,
|
||||||
) -> tuple:
|
) -> tuple:
|
||||||
y = np.asarray(line, dtype=np.float64).ravel()
|
y = np.asarray(line, dtype=np.float64).ravel()
|
||||||
|
x_unit = line.x_unit if isinstance(line, LineData) else ""
|
||||||
|
y_unit = line.y_unit if isinstance(line, LineData) else ""
|
||||||
n = len(y)
|
n = len(y)
|
||||||
if x_axis is not None:
|
if isinstance(line, LineData) and line.x_axis is not None:
|
||||||
x = np.asarray(x_axis, dtype=np.float64).ravel()[:n]
|
x = np.asarray(line.x_axis, dtype=np.float64).ravel()[:n]
|
||||||
else:
|
else:
|
||||||
x = np.arange(n, dtype=np.float64)
|
x = np.arange(n, dtype=np.float64)
|
||||||
x1 = float(np.clip(x1, 0.0, 1.0))
|
x1 = float(np.clip(x1, 0.0, 1.0))
|
||||||
@@ -251,21 +259,21 @@ class Cursors:
|
|||||||
"x2": x2,
|
"x2": x2,
|
||||||
"y1": float(y1),
|
"y1": float(y1),
|
||||||
"y2": float(y2),
|
"y2": float(y2),
|
||||||
"a_locked": False,
|
"a_locked": locked,
|
||||||
"b_locked": False,
|
"b_locked": locked,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- Output table ---
|
# --- Output table ---
|
||||||
table = MeasureTable([
|
table = MeasureTable([
|
||||||
{"quantity": "A x", "value": xa, "unit": ""},
|
{"quantity": "A x", "value": xa, "unit": x_unit},
|
||||||
{"quantity": "A y", "value": ya, "unit": ""},
|
{"quantity": "A y", "value": ya, "unit": y_unit},
|
||||||
{"quantity": "B x", "value": xb, "unit": ""},
|
{"quantity": "B x", "value": xb, "unit": x_unit},
|
||||||
{"quantity": "B y", "value": yb, "unit": ""},
|
{"quantity": "B y", "value": yb, "unit": y_unit},
|
||||||
{"quantity": "dx", "value": xb - xa, "unit": ""},
|
{"quantity": "dx", "value": xb - xa, "unit": x_unit},
|
||||||
{"quantity": "dy", "value": yb - ya, "unit": ""},
|
{"quantity": "dy", "value": yb - ya, "unit": y_unit},
|
||||||
])
|
])
|
||||||
return (table,)
|
return (table, ((x1, y1), (x2, y2)))
|
||||||
|
|
||||||
def _process_field(
|
def _process_field(
|
||||||
self,
|
self,
|
||||||
@@ -274,6 +282,7 @@ class Cursors:
|
|||||||
y1: float,
|
y1: float,
|
||||||
x2: float,
|
x2: float,
|
||||||
y2: float,
|
y2: float,
|
||||||
|
locked: bool = False,
|
||||||
) -> tuple:
|
) -> tuple:
|
||||||
from scipy.ndimage import map_coordinates
|
from scipy.ndimage import map_coordinates
|
||||||
|
|
||||||
@@ -306,8 +315,8 @@ class Cursors:
|
|||||||
"y1": y1,
|
"y1": y1,
|
||||||
"x2": x2,
|
"x2": x2,
|
||||||
"y2": y2,
|
"y2": y2,
|
||||||
"a_locked": False,
|
"a_locked": locked,
|
||||||
"b_locked": False,
|
"b_locked": locked,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -322,7 +331,7 @@ class Cursors:
|
|||||||
{"quantity": "dy", "value": by - ay, "unit": field.si_unit_xy},
|
{"quantity": "dy", "value": by - ay, "unit": field.si_unit_xy},
|
||||||
{"quantity": "dz", "value": z2 - z1, "unit": field.si_unit_z},
|
{"quantity": "dz", "value": z2 - z1, "unit": field.si_unit_z},
|
||||||
])
|
])
|
||||||
return (table,)
|
return (table, ((x1, y1), (x2, y2)))
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -642,13 +651,12 @@ class CrossSection:
|
|||||||
"n_samples": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 1}),
|
"n_samples": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 1}),
|
||||||
},
|
},
|
||||||
"optional": {
|
"optional": {
|
||||||
"marker_A": ("COORD",),
|
"marker_pair": ("COORDPAIR", {"label": "marker pair"}),
|
||||||
"marker_B": ("COORD",),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
RETURN_TYPES = ("LINE",)
|
RETURN_TYPES = ("LINE", "COORDPAIR",)
|
||||||
RETURN_NAMES = ("profile",)
|
RETURN_NAMES = ("profile", "marker pair",)
|
||||||
FUNCTION = "process"
|
FUNCTION = "process"
|
||||||
CATEGORY = "analysis"
|
CATEGORY = "analysis"
|
||||||
DESCRIPTION = (
|
DESCRIPTION = (
|
||||||
@@ -664,15 +672,13 @@ class CrossSection:
|
|||||||
self, field: DataField,
|
self, field: DataField,
|
||||||
x1: float, y1: float, x2: float, y2: float,
|
x1: float, y1: float, x2: float, y2: float,
|
||||||
extend: str, n_samples: int,
|
extend: str, n_samples: int,
|
||||||
marker_A=None, marker_B=None,
|
marker_pair=None,
|
||||||
) -> tuple:
|
) -> tuple:
|
||||||
from scipy.ndimage import map_coordinates
|
from scipy.ndimage import map_coordinates
|
||||||
|
|
||||||
# COORD inputs override widget values
|
# COORDPAIR input overrides widget values
|
||||||
if marker_A is not None:
|
if marker_pair is not None:
|
||||||
x1, y1 = float(marker_A[0]), float(marker_A[1])
|
(x1, y1), (x2, y2) = marker_pair
|
||||||
if marker_B is not None:
|
|
||||||
x2, y2 = float(marker_B[0]), float(marker_B[1])
|
|
||||||
|
|
||||||
# Remember marker positions (before extend)
|
# Remember marker positions (before extend)
|
||||||
marker_x1, marker_y1 = float(x1), float(y1)
|
marker_x1, marker_y1 = float(x1), float(y1)
|
||||||
@@ -714,12 +720,24 @@ class CrossSection:
|
|||||||
"image": image_uri,
|
"image": image_uri,
|
||||||
"x1": marker_x1, "y1": marker_y1,
|
"x1": marker_x1, "y1": marker_y1,
|
||||||
"x2": marker_x2, "y2": marker_y2,
|
"x2": marker_x2, "y2": marker_y2,
|
||||||
"a_locked": marker_A is not None,
|
"a_locked": marker_pair is not None,
|
||||||
"b_locked": marker_B is not None,
|
"b_locked": marker_pair is not None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return (profile.astype(np.float64),)
|
dx_real = (x2 - x1) * field.xreal
|
||||||
|
dy_real = (y2 - y1) * field.yreal
|
||||||
|
distance_axis = np.linspace(0.0, float(np.hypot(dx_real, dy_real)), n_samples, dtype=np.float64)
|
||||||
|
|
||||||
|
return (
|
||||||
|
LineData(
|
||||||
|
data=profile.astype(np.float64),
|
||||||
|
x_axis=distance_axis,
|
||||||
|
x_unit=field.si_unit_xy,
|
||||||
|
y_unit=field.si_unit_z,
|
||||||
|
),
|
||||||
|
((marker_x1, marker_y1), (marker_x2, marker_y2)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -1028,7 +1046,11 @@ class Stats:
|
|||||||
if source_type == "LINE":
|
if source_type == "LINE":
|
||||||
line_entry = LINE_OPS.get(operation)
|
line_entry = LINE_OPS.get(operation)
|
||||||
explicit_unit = line_entry[1] if isinstance(line_entry, tuple) and len(line_entry) > 1 else ""
|
explicit_unit = line_entry[1] if isinstance(line_entry, tuple) and len(line_entry) > 1 else ""
|
||||||
return _apply_scalar_unit(explicit_unit, operation)
|
if explicit_unit:
|
||||||
|
return _apply_scalar_unit(explicit_unit, operation)
|
||||||
|
if isinstance(input_value, LineData):
|
||||||
|
return _apply_scalar_unit(input_value.y_unit, operation)
|
||||||
|
return ""
|
||||||
|
|
||||||
if source_type == "RECORD_TABLE" and isinstance(input_value, list) and column:
|
if source_type == "RECORD_TABLE" and isinstance(input_value, list) and column:
|
||||||
return _apply_scalar_unit(_common_table_unit(input_value, column), operation)
|
return _apply_scalar_unit(_common_table_unit(input_value, column), operation)
|
||||||
@@ -1052,6 +1074,12 @@ class Stats:
|
|||||||
raise ValueError(f"Column '{column_name}' has no numeric values.")
|
raise ValueError(f"Column '{column_name}' has no numeric values.")
|
||||||
return ("RECORD_TABLE", np.asarray(values, dtype=np.float64), column_name)
|
return ("RECORD_TABLE", np.asarray(values, dtype=np.float64), column_name)
|
||||||
|
|
||||||
|
if isinstance(input_value, LineData):
|
||||||
|
values = np.asarray(input_value.data, dtype=np.float64)
|
||||||
|
if values.size == 0:
|
||||||
|
raise ValueError("Stats requires a non-empty input.")
|
||||||
|
return ("LINE", values.ravel(), None)
|
||||||
|
|
||||||
if isinstance(input_value, np.ndarray):
|
if isinstance(input_value, np.ndarray):
|
||||||
values = np.asarray(input_value, dtype=np.float64)
|
values = np.asarray(input_value, dtype=np.float64)
|
||||||
if values.size == 0:
|
if values.size == 0:
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from __future__ import annotations
|
|||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from backend.node_registry import register_node
|
from backend.node_registry import register_node
|
||||||
from backend.data_types import DataField
|
from backend.data_types import DataField, LineData
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -251,6 +251,15 @@ class FFTFilter1D:
|
|||||||
|
|
||||||
# Inverse FFT
|
# Inverse FFT
|
||||||
filtered = np.fft.irfft(Z, n=n)
|
filtered = np.fft.irfft(Z, n=n)
|
||||||
|
if isinstance(line, LineData):
|
||||||
|
return (
|
||||||
|
LineData(
|
||||||
|
data=filtered,
|
||||||
|
x_axis=line.x_axis.copy() if line.x_axis is not None else None,
|
||||||
|
x_unit=line.x_unit,
|
||||||
|
y_unit=line.y_unit,
|
||||||
|
),
|
||||||
|
)
|
||||||
return (filtered,)
|
return (filtered,)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -446,7 +446,30 @@ class Coordinate:
|
|||||||
|
|
||||||
def process(self, x: float, y: float) -> tuple:
|
def process(self, x: float, y: float) -> tuple:
|
||||||
return ((float(x), float(y)),)
|
return ((float(x), float(y)),)
|
||||||
|
|
||||||
|
|
||||||
|
@register_node(display_name="Coordinate Pair")
|
||||||
|
class CoordinatePair:
|
||||||
|
"""Provide a pair of Coordinates, for drawing lines between markers, etc."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"a": ("COORD",),
|
||||||
|
"b": ("COORD",),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("COORDPAIR",)
|
||||||
|
RETURN_NAMES = ("coord pair",)
|
||||||
|
FUNCTION = "process"
|
||||||
|
CATEGORY = "io"
|
||||||
|
DESCRIPTION = "Output a pair of coordinates."
|
||||||
|
|
||||||
|
def process(self, a: tuple, b: tuple) -> tuple:
|
||||||
|
return ((a, b),)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Number
|
# Number
|
||||||
|
|||||||
@@ -23,42 +23,9 @@ import {
|
|||||||
hasBlockingAutoRunInput,
|
hasBlockingAutoRunInput,
|
||||||
} from './executionGraph';
|
} from './executionGraph';
|
||||||
|
|
||||||
// ── Constants ─────────────────────────────────────────────────────────
|
import {
|
||||||
|
DATA_TYPES, SOCKET_COMPATIBILITY, TYPE_COLORS, CAT_COLORS, CANVAS_COLORS,
|
||||||
const DATA_TYPES = new Set([
|
} from './constants';
|
||||||
'DATA_FIELD', 'IMAGE', 'LINE', 'MEASURE_TABLE', 'RECORD_TABLE', 'ANY_TABLE',
|
|
||||||
'COORD', 'STATS_SOURCE', 'CURSOR_SOURCE', 'VALUE_SOURCE', 'COLORMAP', 'SAVE_LAYER', 'FONT', 'FILE_PATH', 'DIRECTORY',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const SOCKET_COMPATIBILITY = {
|
|
||||||
STATS_SOURCE: new Set(['DATA_FIELD', 'IMAGE', 'LINE', 'RECORD_TABLE']),
|
|
||||||
CURSOR_SOURCE: new Set(['DATA_FIELD', 'LINE']),
|
|
||||||
ANY_TABLE: new Set(['MEASURE_TABLE', 'RECORD_TABLE']),
|
|
||||||
VALUE_SOURCE: new Set(['FLOAT', 'MEASURE_TABLE']),
|
|
||||||
SAVE_LAYER: new Set(['DATA_FIELD', 'IMAGE']),
|
|
||||||
FLOAT: new Set(['INT']),
|
|
||||||
INT: new Set(['FLOAT']),
|
|
||||||
};
|
|
||||||
|
|
||||||
const TYPE_COLORS = {
|
|
||||||
DATA_FIELD: '#ff002f',
|
|
||||||
IMAGE: '#00ff08a0',
|
|
||||||
LINE: '#ffbe5c',
|
|
||||||
MEASURE_TABLE:'#35e2fd',
|
|
||||||
RECORD_TABLE:'#fbbf24',
|
|
||||||
ANY_TABLE: '#67e8f9',
|
|
||||||
COORD: '#e91ed1',
|
|
||||||
FLOAT: '#7dd3fc',
|
|
||||||
INT: '#38bdf8',
|
|
||||||
STATS_SOURCE:'#c084fc',
|
|
||||||
CURSOR_SOURCE:'#a78bfa',
|
|
||||||
VALUE_SOURCE:'#60a5fa',
|
|
||||||
COLORMAP: '#f472b6',
|
|
||||||
SAVE_LAYER: '#22c55e',
|
|
||||||
FONT: '#fb7185',
|
|
||||||
FILE_PATH: '#f59e0b',
|
|
||||||
DIRECTORY: '#f97316',
|
|
||||||
};
|
|
||||||
|
|
||||||
const NODE_TYPES = { custom: CustomNode };
|
const NODE_TYPES = { custom: CustomNode };
|
||||||
|
|
||||||
@@ -378,7 +345,7 @@ function ContextMenu({ x, y, nodeDefs, onAdd, onClose, filterType, filterDirecti
|
|||||||
if (categories.length === 0) {
|
if (categories.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="context-menu" ref={menuRef} style={{ left: menuPos.x, top: menuPos.y }} onClick={(e) => e.stopPropagation()}>
|
<div className="context-menu" ref={menuRef} style={{ left: menuPos.x, top: menuPos.y }} onClick={(e) => e.stopPropagation()}>
|
||||||
<div className="context-item" style={{ color: '#64748b' }}>No compatible nodes</div>
|
<div className="context-item" style={{ color: 'var(--text-muted)' }}>No compatible nodes</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -415,7 +382,7 @@ function ContextMenu({ x, y, nodeDefs, onAdd, onClose, filterType, filterDirecti
|
|||||||
{searchResults ? (
|
{searchResults ? (
|
||||||
<div className="ctx-list">
|
<div className="ctx-list">
|
||||||
{searchResults.length === 0 ? (
|
{searchResults.length === 0 ? (
|
||||||
<div className="context-item" style={{ color: '#64748b' }}>No matches</div>
|
<div className="context-item" style={{ color: 'var(--text-muted)' }}>No matches</div>
|
||||||
) : (
|
) : (
|
||||||
searchResults.map(({ className, def }) => (
|
searchResults.map(({ className, def }) => (
|
||||||
<div
|
<div
|
||||||
@@ -655,7 +622,7 @@ function Flow() {
|
|||||||
|
|
||||||
const onConnect = useCallback((params) => {
|
const onConnect = useCallback((params) => {
|
||||||
const type = getHandleType(params.sourceHandle);
|
const type = getHandleType(params.sourceHandle);
|
||||||
const color = TYPE_COLORS[type] || '#999';
|
const color = TYPE_COLORS[type] || 'var(--fallback-type)';
|
||||||
|
|
||||||
setEdges((eds) => {
|
setEdges((eds) => {
|
||||||
// Enforce single connection per input handle
|
// Enforce single connection per input handle
|
||||||
@@ -864,7 +831,7 @@ function Flow() {
|
|||||||
return type;
|
return type;
|
||||||
})();
|
})();
|
||||||
const targetHandle = `input::${inputName}::${targetType}`;
|
const targetHandle = `input::${inputName}::${targetType}`;
|
||||||
const color = TYPE_COLORS[filterType] || '#999';
|
const color = TYPE_COLORS[filterType] || 'var(--fallback-type)';
|
||||||
setEdges((eds) => addEdge({
|
setEdges((eds) => addEdge({
|
||||||
source: contextMenu.pendingNodeId,
|
source: contextMenu.pendingNodeId,
|
||||||
sourceHandle: contextMenu.pendingHandleId,
|
sourceHandle: contextMenu.pendingHandleId,
|
||||||
@@ -879,7 +846,7 @@ function Flow() {
|
|||||||
if (outputIdx !== -1) {
|
if (outputIdx !== -1) {
|
||||||
const outputType = def.output[outputIdx];
|
const outputType = def.output[outputIdx];
|
||||||
const sourceHandle = `output::${outputIdx}::${outputType}`;
|
const sourceHandle = `output::${outputIdx}::${outputType}`;
|
||||||
const color = TYPE_COLORS[outputType] || '#999';
|
const color = TYPE_COLORS[outputType] || 'var(--fallback-type)';
|
||||||
setEdges((eds) => addEdge({
|
setEdges((eds) => addEdge({
|
||||||
source: newNodeId,
|
source: newNodeId,
|
||||||
sourceHandle,
|
sourceHandle,
|
||||||
@@ -1021,7 +988,7 @@ function Flow() {
|
|||||||
const vp = getViewportForBounds(bounds, imageWidth, imageHeight, 0.5, 1, pad);
|
const vp = getViewportForBounds(bounds, imageWidth, imageHeight, 0.5, 1, pad);
|
||||||
|
|
||||||
const blob = await captureWorkflowViewportBlob(viewportEl, {
|
const blob = await captureWorkflowViewportBlob(viewportEl, {
|
||||||
backgroundColor: '#1a1a1a',
|
backgroundColor: CANVAS_COLORS.bgDeep,
|
||||||
width: imageWidth,
|
width: imageWidth,
|
||||||
height: imageHeight,
|
height: imageHeight,
|
||||||
style: {
|
style: {
|
||||||
@@ -1274,11 +1241,7 @@ function Flow() {
|
|||||||
<MiniMap
|
<MiniMap
|
||||||
nodeColor={(n) => {
|
nodeColor={(n) => {
|
||||||
const cat = n.data?.definition?.category;
|
const cat = n.data?.definition?.category;
|
||||||
const colors = {
|
return CAT_COLORS[cat] || 'var(--fallback-cat)';
|
||||||
io: '#37474f', filters: '#1a237e', level: '#1b5e20',
|
|
||||||
analysis: '#4a148c', particles: '#bf360c', display: '#212121',
|
|
||||||
};
|
|
||||||
return colors[cat] || '#333';
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export default function CrossSectionOverlay({
|
|||||||
<line
|
<line
|
||||||
x1={`${x1 * 100}%`} y1={`${y1 * 100}%`}
|
x1={`${x1 * 100}%`} y1={`${y1 * 100}%`}
|
||||||
x2={`${x2 * 100}%`} y2={`${y2 * 100}%`}
|
x2={`${x2 * 100}%`} y2={`${y2 * 100}%`}
|
||||||
stroke="#ffd700" strokeWidth="2" strokeDasharray="6 3"
|
stroke="var(--marker)" strokeWidth="2" strokeDasharray="6 3"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
@@ -78,12 +78,12 @@ export default function CrossSectionOverlay({
|
|||||||
className={`cs-marker ${aLocked ? 'cs-marker-locked' : ''}`}
|
className={`cs-marker ${aLocked ? 'cs-marker-locked' : ''}`}
|
||||||
style={{ left: `${x1 * 100}%`, top: `${y1 * 100}%` }}
|
style={{ left: `${x1 * 100}%`, top: `${y1 * 100}%` }}
|
||||||
onPointerDown={onPointerDown('p1')}
|
onPointerDown={onPointerDown('p1')}
|
||||||
/>
|
>A</div>
|
||||||
<div
|
<div
|
||||||
className={`cs-marker ${bLocked ? 'cs-marker-locked' : ''}`}
|
className={`cs-marker ${bLocked ? 'cs-marker-locked' : ''}`}
|
||||||
style={{ left: `${x2 * 100}%`, top: `${y2 * 100}%` }}
|
style={{ left: `${x2 * 100}%`, top: `${y2 * 100}%` }}
|
||||||
onPointerDown={onPointerDown('p2')}
|
onPointerDown={onPointerDown('p2')}
|
||||||
/>
|
>B</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,43 +8,9 @@ const CropBoxOverlay = lazy(() => import('./CropBoxOverlay'));
|
|||||||
const MaskPaintOverlay = lazy(() => import('./MaskPaintOverlay'));
|
const MaskPaintOverlay = lazy(() => import('./MaskPaintOverlay'));
|
||||||
const MarkupOverlay = lazy(() => import('./MarkupOverlay'));
|
const MarkupOverlay = lazy(() => import('./MarkupOverlay'));
|
||||||
|
|
||||||
// ── Constants ─────────────────────────────────────────────────────────
|
import {
|
||||||
|
DATA_TYPES, SOCKET_WIDGET_TYPES, TYPE_COLORS, CAT_COLORS,
|
||||||
const DATA_TYPES = new Set([
|
} from './constants';
|
||||||
'DATA_FIELD', 'IMAGE', 'LINE', 'MEASURE_TABLE', 'RECORD_TABLE', 'ANY_TABLE',
|
|
||||||
'COORD', 'STATS_SOURCE', 'CURSOR_SOURCE', 'VALUE_SOURCE', 'COLORMAP', 'SAVE_LAYER', 'FONT', 'FILE_PATH', 'DIRECTORY',
|
|
||||||
]);
|
|
||||||
const SOCKET_WIDGET_TYPES = new Set(['FLOAT', 'INT']);
|
|
||||||
|
|
||||||
const TYPE_COLORS = {
|
|
||||||
DATA_FIELD: '#3a7abf',
|
|
||||||
IMAGE: '#4caf50',
|
|
||||||
LINE: '#ff9800',
|
|
||||||
MEASURE_TABLE:'#35e2fd',
|
|
||||||
RECORD_TABLE:'#fbbf24',
|
|
||||||
ANY_TABLE: '#67e8f9',
|
|
||||||
COORD: '#e91e63',
|
|
||||||
FLOAT: '#7dd3fc',
|
|
||||||
INT: '#38bdf8',
|
|
||||||
STATS_SOURCE:'#c084fc',
|
|
||||||
CURSOR_SOURCE:'#a78bfa',
|
|
||||||
VALUE_SOURCE:'#60a5fa',
|
|
||||||
COLORMAP: '#f472b6',
|
|
||||||
SAVE_LAYER: '#22c55e',
|
|
||||||
FONT: '#fb7185',
|
|
||||||
FILE_PATH: '#f59e0b',
|
|
||||||
DIRECTORY: '#f97316',
|
|
||||||
};
|
|
||||||
|
|
||||||
const CAT_COLORS = {
|
|
||||||
io: '#37474f',
|
|
||||||
filters: '#1a237e',
|
|
||||||
modify: '#0f766e',
|
|
||||||
level: '#1b5e20',
|
|
||||||
analysis: '#4a148c',
|
|
||||||
particles:'#bf360c',
|
|
||||||
display: '#212121',
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Context (provided by App) ─────────────────────────────────────────
|
// ── Context (provided by App) ─────────────────────────────────────────
|
||||||
|
|
||||||
@@ -84,7 +50,7 @@ class PreviewBoundary extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="node-preview" style={{ color: '#94a3b8', padding: 8 }}>
|
<div className="node-preview" style={{ color: 'var(--text-secondary)', padding: 8 }}>
|
||||||
Preview unavailable.
|
Preview unavailable.
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -440,6 +406,58 @@ function getConnectedOutputInfo(store, nodeId, inputName) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve live COORDPAIR values by walking edges back to upstream Coordinate
|
||||||
|
* nodes' widget values. Returns [x1, y1, x2, y2] (a flat array for stable
|
||||||
|
* equality comparison) or null if the chain can't be fully resolved.
|
||||||
|
*
|
||||||
|
* Uses store.nodes (the reactive array) rather than nodeLookup so that
|
||||||
|
* upstream widgetValues changes trigger re-renders.
|
||||||
|
*/
|
||||||
|
function resolveLiveCoordPair(store, nodeId, coordPairInputName) {
|
||||||
|
const nodes = store.nodes;
|
||||||
|
const edges = store.edges;
|
||||||
|
if (!nodes || !edges) return null;
|
||||||
|
|
||||||
|
const findNode = (nid) => nodes.find((n) => n.id === nid);
|
||||||
|
|
||||||
|
// 1. Find the edge feeding this node's COORDPAIR input
|
||||||
|
const cpEdge = edges.find(
|
||||||
|
(e) => e.target === nodeId && e.targetHandle?.startsWith(`input::${coordPairInputName}::`)
|
||||||
|
);
|
||||||
|
if (!cpEdge) return null;
|
||||||
|
|
||||||
|
const cpNode = findNode(cpEdge.source);
|
||||||
|
if (!cpNode) return null;
|
||||||
|
|
||||||
|
// If the source node is a CoordinatePair, walk one more level to Coordinate nodes
|
||||||
|
if (cpNode.data?.className === 'CoordinatePair') {
|
||||||
|
const resolveCoord = (inputName) => {
|
||||||
|
const edge = edges.find(
|
||||||
|
(e) => e.target === cpNode.id && e.targetHandle?.startsWith(`input::${inputName}::`)
|
||||||
|
);
|
||||||
|
if (!edge) return null;
|
||||||
|
const srcNode = findNode(edge.source);
|
||||||
|
if (!srcNode?.data?.widgetValues) return null;
|
||||||
|
const x = srcNode.data.widgetValues.x;
|
||||||
|
const y = srcNode.data.widgetValues.y;
|
||||||
|
return (x != null && y != null) ? [x, y] : null;
|
||||||
|
};
|
||||||
|
const a = resolveCoord('a');
|
||||||
|
const b = resolveCoord('b');
|
||||||
|
if (!a || !b) return null;
|
||||||
|
return [a[0], a[1], b[0], b[1]];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the source is a node with x1/y1/x2/y2 widgets (e.g. another CrossSection output)
|
||||||
|
const wv = cpNode.data?.widgetValues;
|
||||||
|
if (wv && wv.x1 != null && wv.y1 != null && wv.x2 != null && wv.y2 != null) {
|
||||||
|
return [wv.x1, wv.y1, wv.x2, wv.y2];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function getBasename(value) {
|
function getBasename(value) {
|
||||||
if (typeof value !== 'string') return '';
|
if (typeof value !== 'string') return '';
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
@@ -732,6 +750,29 @@ function CustomNode({ id, data }) {
|
|||||||
useCallback((s) => getConnectedOutputInfo(s, id, 'path'), [id]),
|
useCallback((s) => getConnectedOutputInfo(s, id, 'path'), [id]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Find the COORDPAIR input name (if any) so we can resolve live upstream positions
|
||||||
|
const coordPairInputName = React.useMemo(() => {
|
||||||
|
const allInputs = { ...def.input.required, ...def.input.optional };
|
||||||
|
for (const [name, spec] of Object.entries(allInputs)) {
|
||||||
|
const type = Array.isArray(spec) ? spec[0] : spec;
|
||||||
|
if (type === 'COORDPAIR') return name;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [def]);
|
||||||
|
|
||||||
|
// Returns [x1, y1, x2, y2] or null — flat array for cheap equality check
|
||||||
|
const liveCoordPair = useStore(
|
||||||
|
useCallback(
|
||||||
|
(s) => coordPairInputName ? resolveLiveCoordPair(s, id, coordPairInputName) : null,
|
||||||
|
[id, coordPairInputName],
|
||||||
|
),
|
||||||
|
(a, b) => {
|
||||||
|
if (a === b) return true;
|
||||||
|
if (!a || !b) return false;
|
||||||
|
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3];
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Parse inputs into data handles and widgets
|
// Parse inputs into data handles and widgets
|
||||||
const required = def.input.required || {};
|
const required = def.input.required || {};
|
||||||
const optional = def.input.optional || {};
|
const optional = def.input.optional || {};
|
||||||
@@ -846,7 +887,7 @@ function CustomNode({ id, data }) {
|
|||||||
slot: i,
|
slot: i,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const catColor = CAT_COLORS[def.category] || '#333';
|
const catColor = CAT_COLORS[def.category] || 'var(--fallback-cat)';
|
||||||
const maxIORows = Math.max(renderedDataInputs.length, outputs.length);
|
const maxIORows = Math.max(renderedDataInputs.length, outputs.length);
|
||||||
const hasInteractiveLineOverlay = data.overlay?.kind === 'line_plot' && hiddenWidgets.has('x1');
|
const hasInteractiveLineOverlay = data.overlay?.kind === 'line_plot' && hiddenWidgets.has('x1');
|
||||||
const hasInteractiveOverlay = !!data.overlay && (
|
const hasInteractiveOverlay = !!data.overlay && (
|
||||||
@@ -904,7 +945,7 @@ function CustomNode({ id, data }) {
|
|||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
id={`input::${socketName}::${socketType}`}
|
id={`input::${socketName}::${socketType}`}
|
||||||
className="typed-handle"
|
className="typed-handle"
|
||||||
style={{ background: TYPE_COLORS[socketType] || '#999' }}
|
style={{ background: TYPE_COLORS[socketType] || 'var(--fallback-type)' }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
@@ -939,7 +980,7 @@ function CustomNode({ id, data }) {
|
|||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
id={`input::${inp.name}::${inp.type}`}
|
id={`input::${inp.name}::${inp.type}`}
|
||||||
className="typed-handle"
|
className="typed-handle"
|
||||||
style={{ background: TYPE_COLORS[inp.type] || '#999' }}
|
style={{ background: TYPE_COLORS[inp.type] || 'var(--fallback-type)' }}
|
||||||
/>
|
/>
|
||||||
<span className="io-label">{inp.label || inp.name}</span>
|
<span className="io-label">{inp.label || inp.name}</span>
|
||||||
{inlineWidgetsByInput.has(inp.name) && (
|
{inlineWidgetsByInput.has(inp.name) && (
|
||||||
@@ -967,7 +1008,7 @@ function CustomNode({ id, data }) {
|
|||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
id={`output::${out.slot}::${out.type}`}
|
id={`output::${out.slot}::${out.type}`}
|
||||||
className="typed-handle"
|
className="typed-handle"
|
||||||
style={{ background: TYPE_COLORS[out.type] || '#999' }}
|
style={{ background: TYPE_COLORS[out.type] || 'var(--fallback-type)' }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -1002,7 +1043,7 @@ function CustomNode({ id, data }) {
|
|||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
id={`input::${w.name}::${w.socketType}`}
|
id={`input::${w.name}::${w.socketType}`}
|
||||||
className="typed-handle"
|
className="typed-handle"
|
||||||
style={{ background: TYPE_COLORS[w.socketType] || '#999' }}
|
style={{ background: TYPE_COLORS[w.socketType] || 'var(--fallback-type)' }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<WidgetControl
|
<WidgetControl
|
||||||
@@ -1033,7 +1074,7 @@ function CustomNode({ id, data }) {
|
|||||||
{/* Interactive 3D surface view */}
|
{/* Interactive 3D surface view */}
|
||||||
{data.meshData && (
|
{data.meshData && (
|
||||||
<CollapsibleSection title="3D View" defaultOpen={true}>
|
<CollapsibleSection title="3D View" defaultOpen={true}>
|
||||||
<Suspense fallback={<div className="node-preview" style={{color:'#64748b',padding:4}}>Loading 3D...</div>}>
|
<Suspense fallback={<div className="node-preview" style={{color:'var(--text-muted)',padding:4}}>Loading 3D...</div>}>
|
||||||
<SurfaceView meshData={data.meshData} />
|
<SurfaceView meshData={data.meshData} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
@@ -1068,12 +1109,12 @@ function CustomNode({ id, data }) {
|
|||||||
{/* Interactive cross-section overlay */}
|
{/* Interactive cross-section overlay */}
|
||||||
{hasInteractiveOverlay && (
|
{hasInteractiveOverlay && (
|
||||||
<CollapsibleSection title={overlayTitle} defaultOpen={true}>
|
<CollapsibleSection title={overlayTitle} defaultOpen={true}>
|
||||||
<Suspense fallback={<div className="node-preview" style={{color:'#64748b',padding:4}}>Loading...</div>}>
|
<Suspense fallback={<div className="node-preview" style={{color:'var(--text-muted)',padding:4}}>Loading...</div>}>
|
||||||
{data.overlay.kind === 'line_plot' ? (
|
{data.overlay.kind === 'line_plot' ? (
|
||||||
<LinePlotOverlay
|
<LinePlotOverlay
|
||||||
overlay={data.overlay}
|
overlay={data.overlay}
|
||||||
x1={data.overlay.a_locked ? data.overlay.x1 : (data.widgetValues.x1 ?? data.overlay.x1)}
|
x1={data.overlay.a_locked ? (liveCoordPair?.[0] ?? data.overlay.x1) : (data.widgetValues.x1 ?? data.overlay.x1)}
|
||||||
x2={data.overlay.b_locked ? data.overlay.x2 : (data.widgetValues.x2 ?? data.overlay.x2)}
|
x2={data.overlay.b_locked ? (liveCoordPair?.[2] ?? data.overlay.x2) : (data.widgetValues.x2 ?? data.overlay.x2)}
|
||||||
aLocked={data.overlay.a_locked}
|
aLocked={data.overlay.a_locked}
|
||||||
bLocked={data.overlay.b_locked}
|
bLocked={data.overlay.b_locked}
|
||||||
nodeId={id}
|
nodeId={id}
|
||||||
@@ -1082,10 +1123,10 @@ function CustomNode({ id, data }) {
|
|||||||
) : data.overlay.kind === 'crop_box' ? (
|
) : data.overlay.kind === 'crop_box' ? (
|
||||||
<CropBoxOverlay
|
<CropBoxOverlay
|
||||||
image={data.overlay.image}
|
image={data.overlay.image}
|
||||||
x1={data.overlay.a_locked ? data.overlay.x1 : (data.widgetValues.x1 ?? data.overlay.x1)}
|
x1={data.overlay.a_locked ? (liveCoordPair?.[0] ?? data.overlay.x1) : (data.widgetValues.x1 ?? data.overlay.x1)}
|
||||||
y1={data.overlay.a_locked ? data.overlay.y1 : (data.widgetValues.y1 ?? data.overlay.y1)}
|
y1={data.overlay.a_locked ? (liveCoordPair?.[1] ?? data.overlay.y1) : (data.widgetValues.y1 ?? data.overlay.y1)}
|
||||||
x2={data.overlay.b_locked ? data.overlay.x2 : (data.widgetValues.x2 ?? data.overlay.x2)}
|
x2={data.overlay.b_locked ? (liveCoordPair?.[2] ?? data.overlay.x2) : (data.widgetValues.x2 ?? data.overlay.x2)}
|
||||||
y2={data.overlay.b_locked ? data.overlay.y2 : (data.widgetValues.y2 ?? data.overlay.y2)}
|
y2={data.overlay.b_locked ? (liveCoordPair?.[3] ?? data.overlay.y2) : (data.widgetValues.y2 ?? data.overlay.y2)}
|
||||||
aLocked={data.overlay.a_locked}
|
aLocked={data.overlay.a_locked}
|
||||||
bLocked={data.overlay.b_locked}
|
bLocked={data.overlay.b_locked}
|
||||||
nodeId={id}
|
nodeId={id}
|
||||||
@@ -1094,10 +1135,10 @@ function CustomNode({ id, data }) {
|
|||||||
) : data.overlay.kind === 'cursor_points' ? (
|
) : data.overlay.kind === 'cursor_points' ? (
|
||||||
<CrossSectionOverlay
|
<CrossSectionOverlay
|
||||||
image={data.overlay.image}
|
image={data.overlay.image}
|
||||||
x1={data.overlay.a_locked ? data.overlay.x1 : (data.widgetValues.x1 ?? data.overlay.x1)}
|
x1={data.overlay.a_locked ? (liveCoordPair?.[0] ?? data.overlay.x1) : (data.widgetValues.x1 ?? data.overlay.x1)}
|
||||||
y1={data.overlay.a_locked ? data.overlay.y1 : (data.widgetValues.y1 ?? data.overlay.y1)}
|
y1={data.overlay.a_locked ? (liveCoordPair?.[1] ?? data.overlay.y1) : (data.widgetValues.y1 ?? data.overlay.y1)}
|
||||||
x2={data.overlay.b_locked ? data.overlay.x2 : (data.widgetValues.x2 ?? data.overlay.x2)}
|
x2={data.overlay.b_locked ? (liveCoordPair?.[2] ?? data.overlay.x2) : (data.widgetValues.x2 ?? data.overlay.x2)}
|
||||||
y2={data.overlay.b_locked ? data.overlay.y2 : (data.widgetValues.y2 ?? data.overlay.y2)}
|
y2={data.overlay.b_locked ? (liveCoordPair?.[3] ?? data.overlay.y2) : (data.widgetValues.y2 ?? data.overlay.y2)}
|
||||||
aLocked={data.overlay.a_locked}
|
aLocked={data.overlay.a_locked}
|
||||||
bLocked={data.overlay.b_locked}
|
bLocked={data.overlay.b_locked}
|
||||||
nodeId={id}
|
nodeId={id}
|
||||||
@@ -1365,7 +1406,7 @@ function WidgetControl({ widget, nodeId, value, widgetValues, onChange, openFile
|
|||||||
if (type === 'STRING' && opts?.color_picker) {
|
if (type === 'STRING' && opts?.color_picker) {
|
||||||
const normalized = typeof val === 'string' && /^#[0-9a-fA-F]{6}$/.test(val)
|
const normalized = typeof val === 'string' && /^#[0-9a-fA-F]{6}$/.test(val)
|
||||||
? val
|
? val
|
||||||
: '#ffd54f';
|
: 'var(--shape-default)';
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!hideLabel && <label>{label}</label>}
|
{!hideLabel && <label>{label}</label>}
|
||||||
|
|||||||
@@ -214,14 +214,14 @@ export default function LinePlotOverlay({
|
|||||||
onLostPointerCapture={onPointerUp}
|
onLostPointerCapture={onPointerUp}
|
||||||
>
|
>
|
||||||
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} className="lineplot-svg">
|
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} className="lineplot-svg">
|
||||||
<rect x="0" y="0" width={width} height={height} fill="#0f172a" />
|
<rect x="0" y="0" width={width} height={height} fill="var(--bg-deep)" />
|
||||||
|
|
||||||
{xTicks.map((tick) => {
|
{xTicks.map((tick) => {
|
||||||
const x = scaleX(tick);
|
const x = scaleX(tick);
|
||||||
return (
|
return (
|
||||||
<g key={`x-${tick}`}>
|
<g key={`x-${tick}`}>
|
||||||
<line x1={x} y1={plotTop} x2={x} y2={plotTop + plotHeight} stroke="#334155" strokeWidth={gridStroke} opacity="0.45" />
|
<line x1={x} y1={plotTop} x2={x} y2={plotTop + plotHeight} stroke="var(--border-default)" strokeWidth={gridStroke} opacity="0.45" />
|
||||||
<text x={x} y={height - 10} textAnchor="middle" fontSize="11" fill="#94a3b8">
|
<text x={x} y={height - 10} textAnchor="middle" fontSize="11" fill="var(--text-secondary)">
|
||||||
{formatTick(tick)}
|
{formatTick(tick)}
|
||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
@@ -232,22 +232,22 @@ export default function LinePlotOverlay({
|
|||||||
const y = scaleY(tick);
|
const y = scaleY(tick);
|
||||||
return (
|
return (
|
||||||
<g key={`y-${tick}`}>
|
<g key={`y-${tick}`}>
|
||||||
<line x1={plotLeft} y1={y} x2={plotLeft + plotWidth} y2={y} stroke="#334155" strokeWidth={gridStroke} opacity="0.45" />
|
<line x1={plotLeft} y1={y} x2={plotLeft + plotWidth} y2={y} stroke="var(--border-default)" strokeWidth={gridStroke} opacity="0.45" />
|
||||||
<text x={plotLeft - 10} y={y + 4} textAnchor="end" fontSize="11" fill="#94a3b8">
|
<text x={plotLeft - 10} y={y + 4} textAnchor="end" fontSize="11" fill="var(--text-secondary)">
|
||||||
{formatTick(tick)}
|
{formatTick(tick)}
|
||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<rect x={plotLeft} y={plotTop} width={plotWidth} height={plotHeight} fill="none" stroke="#334155" strokeWidth={gridStroke + 0.3} />
|
<rect x={plotLeft} y={plotTop} width={plotWidth} height={plotHeight} fill="none" stroke="var(--border-default)" strokeWidth={gridStroke + 0.3} />
|
||||||
<path d={path} fill="none" stroke="#ff9800" strokeWidth={plotStroke} strokeLinecap="round" strokeLinejoin="round" />
|
<path d={path} fill="none" stroke="var(--plot-line)" strokeWidth={plotStroke} strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
|
||||||
{interactive && (
|
{interactive && (
|
||||||
<>
|
<>
|
||||||
<line x1={cursorA.x} y1={plotTop} x2={cursorA.x} y2={plotTop + plotHeight} stroke="#ffd700" strokeWidth={cursorStroke} strokeDasharray="10 6" opacity="0.95" />
|
<line x1={cursorA.x} y1={plotTop} x2={cursorA.x} y2={plotTop + plotHeight} stroke="var(--marker)" strokeWidth={cursorStroke} strokeDasharray="10 6" opacity="0.95" />
|
||||||
<line x1={cursorB.x} y1={plotTop} x2={cursorB.x} y2={plotTop + plotHeight} stroke="#ffd700" strokeWidth={cursorStroke} strokeDasharray="10 6" opacity="0.95" />
|
<line x1={cursorB.x} y1={plotTop} x2={cursorB.x} y2={plotTop + plotHeight} stroke="var(--marker)" strokeWidth={cursorStroke} strokeDasharray="10 6" opacity="0.95" />
|
||||||
<line x1={cursorA.x} y1={cursorA.y} x2={cursorB.x} y2={cursorB.y} stroke="#90caf9" strokeWidth={measureStroke} opacity="0.95" />
|
<line x1={cursorA.x} y1={cursorA.y} x2={cursorB.x} y2={cursorB.y} stroke="var(--accent-light)" strokeWidth={measureStroke} opacity="0.95" />
|
||||||
|
|
||||||
<circle
|
<circle
|
||||||
cx={cursorA.x}
|
cx={cursorA.x}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ function clampFraction(value) {
|
|||||||
return Math.max(0, Math.min(1, numeric));
|
return Math.max(0, Math.min(1, numeric));
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeColor(color, fallback = '#ffd54f') {
|
const SHAPE_DEFAULT_COLOR = '#ffd54f';
|
||||||
|
|
||||||
|
function sanitizeColor(color, fallback = SHAPE_DEFAULT_COLOR) {
|
||||||
if (typeof color !== 'string') return fallback;
|
if (typeof color !== 'string') return fallback;
|
||||||
const value = color.trim();
|
const value = color.trim();
|
||||||
return /^#[0-9a-fA-F]{6}$/.test(value) ? value.toLowerCase() : fallback;
|
return /^#[0-9a-fA-F]{6}$/.test(value) ? value.toLowerCase() : fallback;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||||
|
import { CANVAS_COLORS } from './constants';
|
||||||
|
|
||||||
function clampFraction(value) {
|
function clampFraction(value) {
|
||||||
const numeric = Number(value);
|
const numeric = Number(value);
|
||||||
@@ -52,8 +53,8 @@ function drawStroke(ctx, stroke, width, height, imageWidth, imageHeight, styles
|
|||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.lineCap = 'round';
|
ctx.lineCap = 'round';
|
||||||
ctx.lineJoin = 'round';
|
ctx.lineJoin = 'round';
|
||||||
ctx.strokeStyle = styles.strokeStyle || '#ffffff';
|
ctx.strokeStyle = styles.strokeStyle || CANVAS_COLORS.maskStroke;
|
||||||
ctx.fillStyle = styles.fillStyle || '#ffffff';
|
ctx.fillStyle = styles.fillStyle || CANVAS_COLORS.maskStroke;
|
||||||
ctx.lineWidth = lineWidth;
|
ctx.lineWidth = lineWidth;
|
||||||
|
|
||||||
const points = stroke.points.map((point) => ({
|
const points = stroke.points.map((point) => ({
|
||||||
@@ -160,7 +161,7 @@ export default function MaskPaintOverlay({
|
|||||||
cssHeight,
|
cssHeight,
|
||||||
imageWidth,
|
imageWidth,
|
||||||
imageHeight,
|
imageHeight,
|
||||||
{ strokeStyle: '#ffffff', fillStyle: '#ffffff' },
|
{ strokeStyle: CANVAS_COLORS.maskStroke, fillStyle: CANVAS_COLORS.maskStroke },
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const stroke of committedStrokes) {
|
for (const stroke of committedStrokes) {
|
||||||
@@ -172,7 +173,7 @@ export default function MaskPaintOverlay({
|
|||||||
|
|
||||||
ctx.drawImage(maskCanvas, 0, 0);
|
ctx.drawImage(maskCanvas, 0, 0);
|
||||||
ctx.globalCompositeOperation = 'source-in';
|
ctx.globalCompositeOperation = 'source-in';
|
||||||
ctx.fillStyle = 'rgba(255, 59, 59, 0.16)';
|
ctx.fillStyle = CANVAS_COLORS.maskOverlay;
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
ctx.globalCompositeOperation = 'source-over';
|
ctx.globalCompositeOperation = 'source-over';
|
||||||
}, [imageHeight, imageWidth]);
|
}, [imageHeight, imageWidth]);
|
||||||
|
|||||||
58
frontend/src/constants.js
Normal file
58
frontend/src/constants.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// ── Shared type & color constants ─────────────────────────────────────
|
||||||
|
|
||||||
|
export const DATA_TYPES = new Set([
|
||||||
|
'DATA_FIELD', 'IMAGE', 'LINE', 'MEASURE_TABLE', 'RECORD_TABLE', 'ANY_TABLE',
|
||||||
|
'COORD', 'STATS_SOURCE', 'CURSOR_SOURCE', 'VALUE_SOURCE', 'COLORMAP',
|
||||||
|
'SAVE_LAYER', 'FONT', 'FILE_PATH', 'DIRECTORY', 'COORDPAIR',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const SOCKET_WIDGET_TYPES = new Set(['FLOAT', 'INT']);
|
||||||
|
|
||||||
|
export const TYPE_COLORS = {
|
||||||
|
DATA_FIELD: '#3a7abf',
|
||||||
|
IMAGE: '#00ff08a0',
|
||||||
|
LINE: '#ffbe5c',
|
||||||
|
MEASURE_TABLE: '#35e2fd',
|
||||||
|
RECORD_TABLE: '#fbbf24',
|
||||||
|
ANY_TABLE: '#67e8f9',
|
||||||
|
COORD: '#e91ed1',
|
||||||
|
COORDPAIR: '#5c7cb8',
|
||||||
|
FLOAT: '#ab3197',
|
||||||
|
INT: '#38bdf8',
|
||||||
|
STATS_SOURCE: '#c084fc',
|
||||||
|
CURSOR_SOURCE: '#a78bfa',
|
||||||
|
VALUE_SOURCE: '#60a5fa',
|
||||||
|
COLORMAP: '#f472b6',
|
||||||
|
SAVE_LAYER: '#22c55e',
|
||||||
|
FONT: '#fb7185',
|
||||||
|
FILE_PATH: '#f59e0b',
|
||||||
|
DIRECTORY: '#f97316',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CAT_COLORS = {
|
||||||
|
io: '#37474f',
|
||||||
|
filters: '#1a237e',
|
||||||
|
modify: '#0f766e',
|
||||||
|
level: '#1b5e20',
|
||||||
|
analysis: '#4a148c',
|
||||||
|
particles: '#bf360c',
|
||||||
|
display: '#212121',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SOCKET_COMPATIBILITY = {
|
||||||
|
STATS_SOURCE: new Set(['DATA_FIELD', 'IMAGE', 'LINE', 'RECORD_TABLE']),
|
||||||
|
CURSOR_SOURCE: new Set(['DATA_FIELD', 'LINE']),
|
||||||
|
ANY_TABLE: new Set(['MEASURE_TABLE', 'RECORD_TABLE']),
|
||||||
|
VALUE_SOURCE: new Set(['FLOAT', 'MEASURE_TABLE']),
|
||||||
|
SAVE_LAYER: new Set(['DATA_FIELD', 'IMAGE']),
|
||||||
|
FLOAT: new Set(['INT']),
|
||||||
|
INT: new Set(['FLOAT']),
|
||||||
|
LINE: new Set(['COORDPAIR']),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Colors used in Canvas 2D / toBlob contexts where CSS var() is unavailable.
|
||||||
|
export const CANVAS_COLORS = {
|
||||||
|
bgDeep: '#0f172a',
|
||||||
|
maskStroke: '#ffffff',
|
||||||
|
maskOverlay: 'rgba(255, 59, 59, 0.16)',
|
||||||
|
};
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
const DATA_TYPES = new Set([
|
import { DATA_TYPES } from './constants';
|
||||||
'DATA_FIELD', 'IMAGE', 'LINE', 'MEASURE_TABLE', 'RECORD_TABLE', 'ANY_TABLE',
|
|
||||||
'COORD', 'STATS_SOURCE', 'CURSOR_SOURCE', 'VALUE_SOURCE', 'COLORMAP', 'SAVE_LAYER', 'FONT', 'FILE_PATH', 'DIRECTORY',
|
|
||||||
]);
|
|
||||||
|
|
||||||
function getInputName(handleId) {
|
function getInputName(handleId) {
|
||||||
return handleId.split('::')[1];
|
return handleId.split('::')[1];
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
|||||||
import { toBlob } from 'html-to-image';
|
import { toBlob } from 'html-to-image';
|
||||||
|
import { CANVAS_COLORS } from './constants';
|
||||||
|
|
||||||
export const OVERLAY_CAPTURE_SELECTORS = [
|
export const OVERLAY_CAPTURE_SELECTORS = [
|
||||||
'.lineplot-overlay',
|
'.lineplot-overlay',
|
||||||
@@ -115,7 +116,7 @@ async function renderElementToDataUrl(el, toBlobImpl) {
|
|||||||
const blob = await toBlobImpl(el, {
|
const blob = await toBlobImpl(el, {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
backgroundColor: '#0f172a',
|
backgroundColor: CANVAS_COLORS.bgDeep,
|
||||||
style: {
|
style: {
|
||||||
width: `${width}px`,
|
width: `${width}px`,
|
||||||
height: `${height}px`,
|
height: `${height}px`,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from pathlib import Path
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
sys.path.insert(0, ".")
|
sys.path.insert(0, ".")
|
||||||
from backend.data_types import DataField, MeasureTable, RecordTable, datafield_to_uint8, render_datafield_preview
|
from backend.data_types import DataField, LineData, MeasureTable, RecordTable, datafield_to_uint8, render_datafield_preview
|
||||||
|
|
||||||
|
|
||||||
def make_field(data=None, shape=(64, 64), xreal=1e-6, yreal=1e-6):
|
def make_field(data=None, shape=(64, 64), xreal=1e-6, yreal=1e-6):
|
||||||
@@ -518,7 +518,7 @@ def test_height_histogram():
|
|||||||
Histogram._broadcast_overlay_fn = lambda nid, data: overlays.append(data)
|
Histogram._broadcast_overlay_fn = lambda nid, data: overlays.append(data)
|
||||||
Histogram._current_node_id = "test"
|
Histogram._current_node_id = "test"
|
||||||
|
|
||||||
table, = node.process(
|
table, coord_pair = node.process(
|
||||||
field,
|
field,
|
||||||
n_bins=10,
|
n_bins=10,
|
||||||
y_scale="linear",
|
y_scale="linear",
|
||||||
@@ -527,6 +527,7 @@ def test_height_histogram():
|
|||||||
x2=0.8,
|
x2=0.8,
|
||||||
y2=0.5,
|
y2=0.5,
|
||||||
)
|
)
|
||||||
|
assert isinstance(coord_pair, tuple) and len(coord_pair) == 2
|
||||||
measurements = {row["quantity"]: row for row in table}
|
measurements = {row["quantity"]: row for row in table}
|
||||||
assert "A position" in measurements
|
assert "A position" in measurements
|
||||||
assert "A count" in measurements
|
assert "A count" in measurements
|
||||||
@@ -565,24 +566,30 @@ def test_cross_section():
|
|||||||
field = make_field(data=data, xreal=1e-6, yreal=1e-6)
|
field = make_field(data=data, xreal=1e-6, yreal=1e-6)
|
||||||
|
|
||||||
# Horizontal cross section at y=0.5
|
# Horizontal cross section at y=0.5
|
||||||
(profile,) = node.process(
|
profile, marker_pair = node.process(
|
||||||
field, x1=0.0, y1=0.5, x2=1.0, y2=0.5,
|
field, x1=0.0, y1=0.5, x2=1.0, y2=0.5,
|
||||||
extend="none", n_samples=100,
|
extend="none", n_samples=100,
|
||||||
)
|
)
|
||||||
|
assert isinstance(marker_pair, tuple) and len(marker_pair) == 2
|
||||||
|
assert isinstance(profile, LineData)
|
||||||
assert len(profile) == 100
|
assert len(profile) == 100
|
||||||
|
assert profile.x_unit == field.si_unit_xy
|
||||||
|
assert profile.y_unit == field.si_unit_z
|
||||||
|
assert np.isclose(profile.x_axis[0], 0.0)
|
||||||
|
assert np.isclose(profile.x_axis[-1], field.xreal)
|
||||||
# Profile should be a linear ramp from ~0 to ~10
|
# Profile should be a linear ramp from ~0 to ~10
|
||||||
assert profile[0] < 0.5, f"Start of profile: {profile[0]}"
|
assert profile[0] < 0.5, f"Start of profile: {profile[0]}"
|
||||||
assert profile[-1] > 9.5, f"End of profile: {profile[-1]}"
|
assert profile[-1] > 9.5, f"End of profile: {profile[-1]}"
|
||||||
|
|
||||||
# n_samples=0 should auto-calculate
|
# n_samples=0 should auto-calculate
|
||||||
(profile_auto,) = node.process(
|
profile_auto, _ = node.process(
|
||||||
field, x1=0.0, y1=0.5, x2=1.0, y2=0.5,
|
field, x1=0.0, y1=0.5, x2=1.0, y2=0.5,
|
||||||
extend="none", n_samples=0,
|
extend="none", n_samples=0,
|
||||||
)
|
)
|
||||||
assert len(profile_auto) >= 2
|
assert len(profile_auto) >= 2
|
||||||
|
|
||||||
# Test extend to edges — a short segment should be extended
|
# Test extend to edges — a short segment should be extended
|
||||||
(profile_ext,) = node.process(
|
profile_ext, _ = node.process(
|
||||||
field, x1=0.3, y1=0.5, x2=0.7, y2=0.5,
|
field, x1=0.3, y1=0.5, x2=0.7, y2=0.5,
|
||||||
extend="to_edges", n_samples=100,
|
extend="to_edges", n_samples=100,
|
||||||
)
|
)
|
||||||
@@ -591,11 +598,29 @@ def test_cross_section():
|
|||||||
assert profile_ext[-1] > 9.5
|
assert profile_ext[-1] > 9.5
|
||||||
|
|
||||||
# Diagonal cross section
|
# Diagonal cross section
|
||||||
(profile_diag,) = node.process(
|
profile_diag, _ = node.process(
|
||||||
field, x1=0.0, y1=0.0, x2=1.0, y2=1.0,
|
field, x1=0.0, y1=0.0, x2=1.0, y2=1.0,
|
||||||
extend="none", n_samples=50,
|
extend="none", n_samples=50,
|
||||||
)
|
)
|
||||||
assert len(profile_diag) == 50
|
assert len(profile_diag) == 50
|
||||||
|
|
||||||
|
from backend.nodes.analysis import Cursors, Stats
|
||||||
|
|
||||||
|
cursors = Cursors()
|
||||||
|
table, _ = cursors.process(profile, x1=0.25, y1=0.5, x2=0.75, y2=0.5)
|
||||||
|
rows = {row["quantity"]: row for row in table}
|
||||||
|
assert rows["dx"]["unit"] == field.si_unit_xy
|
||||||
|
assert rows["dy"]["unit"] == field.si_unit_z
|
||||||
|
|
||||||
|
captured = []
|
||||||
|
Stats._broadcast_value_fn = lambda nid, payload: captured.append(payload)
|
||||||
|
Stats._current_node_id = "test"
|
||||||
|
stats = Stats()
|
||||||
|
mean_value, = stats.process(profile, operation="mean", column="value")
|
||||||
|
assert mean_value > 0
|
||||||
|
assert captured[-1]["unit"] == field.si_unit_z
|
||||||
|
Stats._broadcast_value_fn = None
|
||||||
|
|
||||||
print(" PASS\n")
|
print(" PASS\n")
|
||||||
|
|
||||||
|
|
||||||
@@ -1629,7 +1654,8 @@ def test_line_cursors():
|
|||||||
Cursors._broadcast_overlay_fn = lambda nid, data: overlays.append(data)
|
Cursors._broadcast_overlay_fn = lambda nid, data: overlays.append(data)
|
||||||
Cursors._current_node_id = "test"
|
Cursors._current_node_id = "test"
|
||||||
|
|
||||||
table, = node.process(line, x1=0.25, y1=0.5, x2=0.75, y2=0.5)
|
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
|
||||||
|
|
||||||
# Should produce a 6-row table
|
# Should produce a 6-row table
|
||||||
assert len(table) == 6
|
assert len(table) == 6
|
||||||
@@ -1656,9 +1682,9 @@ def test_line_cursors():
|
|||||||
assert 0.0 <= overlays[0]["x1"] <= 1.0
|
assert 0.0 <= overlays[0]["x1"] <= 1.0
|
||||||
assert 0.0 <= overlays[0]["x2"] <= 1.0
|
assert 0.0 <= overlays[0]["x2"] <= 1.0
|
||||||
|
|
||||||
# With x_axis provided
|
# With LineData input (which carries its own x_axis)
|
||||||
x_axis = np.linspace(0, 1, 100).astype(np.float64)
|
line_data = LineData(data=line, x_axis=np.linspace(0, 1, 100))
|
||||||
table2, = node.process(line, x1=0.25, y1=0.5, x2=0.75, y2=0.5, x_axis=x_axis)
|
table2, _ = node.process(line_data, x1=0.25, y1=0.5, x2=0.75, y2=0.5)
|
||||||
assert len(table2) == 6
|
assert len(table2) == 6
|
||||||
|
|
||||||
# Field input should report dx/dy/dz and broadcast an image overlay
|
# Field input should report dx/dy/dz and broadcast an image overlay
|
||||||
@@ -1670,7 +1696,7 @@ def test_line_cursors():
|
|||||||
si_unit_z="nm",
|
si_unit_z="nm",
|
||||||
)
|
)
|
||||||
overlays.clear()
|
overlays.clear()
|
||||||
table3, = node.process(field, x1=0.2, y1=0.25, x2=0.7, y2=0.75)
|
table3, _ = node.process(field, x1=0.2, y1=0.25, x2=0.7, y2=0.75)
|
||||||
assert len(table3) == 9
|
assert len(table3) == 9
|
||||||
field_rows = {row["quantity"]: row for row in table3}
|
field_rows = {row["quantity"]: row for row in table3}
|
||||||
assert field_rows["dx"]["unit"] == "um"
|
assert field_rows["dx"]["unit"] == "um"
|
||||||
|
|||||||
Reference in New Issue
Block a user