work on igor note
This commit is contained in:
@@ -31,11 +31,11 @@ CUSTOM_FILE_FONT = "Custom File"
|
||||
PREVIEW_MARKUP_REFERENCE_DIM = 512
|
||||
|
||||
|
||||
class RecordTable(list):
|
||||
class DataTable(list):
|
||||
"""Tabular rows with a shared schema, e.g. grain statistics."""
|
||||
|
||||
|
||||
class MeasureTable(list):
|
||||
class RecordTable(list):
|
||||
"""Named scalar measurements, typically rows of quantity/value/unit."""
|
||||
|
||||
|
||||
|
||||
@@ -318,7 +318,7 @@ class ExecutionEngine:
|
||||
|
||||
def _fingerprint_bytes(self, value: Any) -> bytes:
|
||||
import numpy as np
|
||||
from backend.data_types import DataField, ImageData, LineData, MeasureTable, MeshModel, RecordTable
|
||||
from backend.data_types import DataField, ImageData, LineData, RecordTable, MeshModel, DataTable
|
||||
|
||||
if value is None:
|
||||
return b"null"
|
||||
@@ -381,7 +381,7 @@ class ExecutionEngine:
|
||||
).encode()
|
||||
return b"|".join([b"ndarray", header, memoryview(array).tobytes()])
|
||||
|
||||
if isinstance(value, (MeasureTable, RecordTable, list)):
|
||||
if isinstance(value, (RecordTable, DataTable, list)):
|
||||
return b"[" + b",".join(self._fingerprint_bytes(item) for item in value) + b"]"
|
||||
|
||||
if isinstance(value, tuple):
|
||||
@@ -494,7 +494,7 @@ class ExecutionEngine:
|
||||
on_preview(node_id, preview)
|
||||
return
|
||||
|
||||
if type_name in ("TABLE", "MEASURE_TABLE", "RECORD_TABLE") and isinstance(value, list) and on_table:
|
||||
if type_name in ("TABLE", "RECORD_TABLE", "DATA_TABLE") and isinstance(value, list) and on_table:
|
||||
on_table(node_id, value)
|
||||
return
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import numpy as np
|
||||
|
||||
from backend.node_registry import register_node
|
||||
from backend.data_types import LineData, MeasureTable
|
||||
from backend.data_types import LineData, RecordTable
|
||||
from backend.nodes.spectral_common import acf_line_from_data
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class ACF1D:
|
||||
|
||||
OUTPUTS = (
|
||||
('LINE', 'acf'),
|
||||
('MEASURE_TABLE', 'measurement'),
|
||||
('RECORD_TABLE', 'measurement'),
|
||||
)
|
||||
FUNCTION = "process"
|
||||
|
||||
@@ -58,4 +58,4 @@ class ACF1D:
|
||||
if peak_lag is not None:
|
||||
rows.append({"quantity": "Peak period", "value": peak_lag, "unit": x_unit})
|
||||
|
||||
return (acf_line, MeasureTable(rows))
|
||||
return (acf_line, RecordTable(rows))
|
||||
|
||||
@@ -5,7 +5,7 @@ import numpy as np
|
||||
from backend.data_types import (
|
||||
DataField,
|
||||
ImageData,
|
||||
MeasureTable,
|
||||
RecordTable,
|
||||
_apply_angle_measure_overlay,
|
||||
encode_preview,
|
||||
image_metadata,
|
||||
@@ -114,7 +114,7 @@ class AngleMeasure:
|
||||
|
||||
OUTPUTS = (
|
||||
('ANNOTATION_SOURCE', 'output'),
|
||||
('MEASURE_TABLE', 'measurements'),
|
||||
('RECORD_TABLE', 'measurements'),
|
||||
)
|
||||
FUNCTION = "process"
|
||||
|
||||
@@ -189,7 +189,7 @@ class AngleMeasure:
|
||||
stroke_width=resolved_stroke_width,
|
||||
color=resolved_color,
|
||||
)
|
||||
table = MeasureTable([
|
||||
table = RecordTable([
|
||||
{"quantity": "Angle", "value": angle_deg, "unit": "deg"},
|
||||
{"quantity": "Arm A length", "value": length_a, "unit": length_unit},
|
||||
{"quantity": "Arm B length", "value": length_b, "unit": length_unit},
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
import numpy as np
|
||||
from backend.node_registry import register_node
|
||||
from backend.execution_context import emit_overlay
|
||||
from backend.data_types import DataField, LineData, MeasureTable, encode_preview, render_datafield_preview
|
||||
from backend.data_types import DataField, LineData, RecordTable, encode_preview, render_datafield_preview
|
||||
|
||||
|
||||
@register_node(display_name="Cursors")
|
||||
@@ -28,7 +28,7 @@ class Cursors:
|
||||
}
|
||||
|
||||
OUTPUTS = (
|
||||
('MEASURE_TABLE', 'measurement'),
|
||||
('RECORD_TABLE', 'measurement'),
|
||||
('COORDPAIR', 'coord_pair'),
|
||||
)
|
||||
FUNCTION = "process"
|
||||
@@ -107,7 +107,7 @@ class Cursors:
|
||||
})
|
||||
|
||||
length = float(np.hypot(xb - xa, yb - ya))
|
||||
table = MeasureTable([
|
||||
table = RecordTable([
|
||||
{"quantity": "Length", "value": length, "unit": x_unit},
|
||||
{"quantity": "dx", "value": xb - xa, "unit": x_unit},
|
||||
{"quantity": "dy", "value": yb - ya, "unit": y_unit},
|
||||
@@ -159,7 +159,7 @@ class Cursors:
|
||||
"b_locked": locked,
|
||||
})
|
||||
|
||||
table = MeasureTable([
|
||||
table = RecordTable([
|
||||
{"quantity": "A x", "value": ax, "unit": field.si_unit_xy},
|
||||
{"quantity": "A y", "value": ay, "unit": field.si_unit_xy},
|
||||
{"quantity": "A z", "value": z1, "unit": field.si_unit_z},
|
||||
|
||||
@@ -8,7 +8,7 @@ from scipy.ndimage import map_coordinates
|
||||
from backend.data_types import (
|
||||
DataField,
|
||||
LineData,
|
||||
MeasureTable,
|
||||
RecordTable,
|
||||
_apply_markup_overlay,
|
||||
encode_preview,
|
||||
render_datafield_preview,
|
||||
@@ -289,7 +289,7 @@ class Curvature:
|
||||
|
||||
OUTPUTS = (
|
||||
('ANNOTATION_SOURCE', 'output'),
|
||||
('MEASURE_TABLE', 'measurements'),
|
||||
('RECORD_TABLE', 'measurements'),
|
||||
('LINE', 'profile_1'),
|
||||
('LINE', 'profile_2'),
|
||||
)
|
||||
@@ -313,7 +313,7 @@ class Curvature:
|
||||
|
||||
if results is None:
|
||||
emit_warning("Curvature requires at least six usable pixels for the quadratic fit.")
|
||||
table = MeasureTable([])
|
||||
table = RecordTable([])
|
||||
emit_table(table)
|
||||
emit_preview(encode_preview(render_datafield_preview(field, field.colormap)))
|
||||
empty = _empty_profile(field.si_unit_xy, field.si_unit_z)
|
||||
@@ -345,7 +345,7 @@ class Curvature:
|
||||
markup_spec = _curvature_markup(field, results["x0"], results["y0"], intersections)
|
||||
output = field.replace(overlays=[*field.overlays, markup_spec])
|
||||
|
||||
table = MeasureTable([
|
||||
table = RecordTable([
|
||||
{"quantity": "Center x position", "value": float(results["x0"]), "unit": field.si_unit_xy},
|
||||
{"quantity": "Center y position", "value": float(results["y0"]), "unit": field.si_unit_xy},
|
||||
{"quantity": "Center value", "value": float(results["z0"]), "unit": field.si_unit_z},
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import numpy as np
|
||||
|
||||
from backend.node_registry import register_node
|
||||
from backend.data_types import LineData, MeasureTable
|
||||
from backend.data_types import LineData, RecordTable
|
||||
|
||||
|
||||
@register_node(display_name="FFT 1D")
|
||||
@@ -21,7 +21,7 @@ class FFT1D:
|
||||
|
||||
OUTPUTS = (
|
||||
("LINE", "frequency_plot"),
|
||||
('MEASURE_TABLE', 'measurement'),
|
||||
('RECORD_TABLE', 'measurement'),
|
||||
)
|
||||
|
||||
FUNCTION = "process"
|
||||
@@ -55,7 +55,7 @@ class FFT1D:
|
||||
|
||||
peak_period = float(period_axis[np.argmax(spectrum)])
|
||||
|
||||
table = MeasureTable([
|
||||
table = RecordTable([
|
||||
{"quantity": "Peak period", "value": peak_period, "unit": spatial_unit},
|
||||
])
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from dataclasses import dataclass
|
||||
import numpy as np
|
||||
from scipy.ndimage import map_coordinates
|
||||
|
||||
from backend.data_types import LineData, MeasureTable
|
||||
from backend.data_types import LineData, RecordTable
|
||||
from backend.execution_context import emit_overlay, emit_table, emit_warning
|
||||
from backend.node_registry import register_node
|
||||
|
||||
@@ -307,7 +307,7 @@ class FractalDimension:
|
||||
OUTPUTS = (
|
||||
('FLOAT', 'dimension'),
|
||||
('LINE', 'curve'),
|
||||
('MEASURE_TABLE', 'measurements'),
|
||||
('RECORD_TABLE', 'measurements'),
|
||||
)
|
||||
FUNCTION = "process"
|
||||
|
||||
@@ -348,7 +348,7 @@ class FractalDimension:
|
||||
dimension = float("nan")
|
||||
emit_warning("Fractal fit range contains fewer than two usable points.")
|
||||
|
||||
table = MeasureTable([
|
||||
table = RecordTable([
|
||||
{"quantity": "Dimension", "value": float(dimension), "unit": ""},
|
||||
{"quantity": "Fit slope", "value": float(slope), "unit": ""},
|
||||
{"quantity": "Fit intercept", "value": float(intercept), "unit": ""},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
import numpy as np
|
||||
from backend.node_registry import register_node
|
||||
from backend.data_types import DataField, RecordTable
|
||||
from backend.data_types import DataField, DataTable
|
||||
from backend.nodes.helpers import _square_unit
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ class GrainAnalysis:
|
||||
}
|
||||
|
||||
OUTPUTS = (
|
||||
('RECORD_TABLE', 'grain_stats'),
|
||||
('DATA_TABLE', 'grain_stats'),
|
||||
)
|
||||
FUNCTION = "process"
|
||||
|
||||
@@ -38,7 +38,7 @@ class GrainAnalysis:
|
||||
xy_unit = str(field.si_unit_xy or "").strip()
|
||||
z_unit = str(field.si_unit_z or "").strip()
|
||||
|
||||
rows = RecordTable()
|
||||
rows = DataTable()
|
||||
for gid in range(1, n_grains + 1):
|
||||
grain_pixels = labeled == gid
|
||||
area_px = int(grain_pixels.sum())
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
import numpy as np
|
||||
from backend.node_registry import register_node
|
||||
from backend.execution_context import emit_overlay
|
||||
from backend.data_types import DataField, MeasureTable
|
||||
from backend.data_types import DataField, RecordTable
|
||||
|
||||
|
||||
@register_node(display_name="Histogram")
|
||||
@@ -22,7 +22,7 @@ class Histogram:
|
||||
}
|
||||
|
||||
OUTPUTS = (
|
||||
('MEASURE_TABLE', 'measurements'),
|
||||
('RECORD_TABLE', 'measurements'),
|
||||
('COORDPAIR', 'marker_pair'),
|
||||
)
|
||||
FUNCTION = "process"
|
||||
@@ -88,7 +88,7 @@ class Histogram:
|
||||
"b_locked": False,
|
||||
})
|
||||
|
||||
table = MeasureTable([
|
||||
table = RecordTable([
|
||||
{"quantity": "delta Y", "value": yb - ya, "unit": count_unit},
|
||||
{"quantity": "delta X", "value": xb - xa, "unit": field.si_unit_z},
|
||||
{"quantity": "A position", "value": xa, "unit": field.si_unit_z},
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import re
|
||||
|
||||
from backend.node_registry import register_node
|
||||
from backend.data_types import MeasureTable
|
||||
from backend.data_types import DataTable
|
||||
from backend.nodes.helpers import _resolve_path, _import_ibw_loader
|
||||
|
||||
|
||||
@@ -22,14 +22,10 @@ def _parse_ibw_note(note_bytes: bytes) -> list[dict]:
|
||||
if not match:
|
||||
continue
|
||||
key = match.group(1).strip()
|
||||
raw_val = match.group(2).strip()
|
||||
value = match.group(2).strip()
|
||||
if not key:
|
||||
continue
|
||||
try:
|
||||
value = float(raw_val)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
rows.append({"quantity": key, "value": value, "unit": ""})
|
||||
rows.append({"key": key, "value": value})
|
||||
|
||||
return rows
|
||||
|
||||
@@ -48,13 +44,13 @@ class IBWNote:
|
||||
}
|
||||
|
||||
OUTPUTS = (
|
||||
('MEASURE_TABLE', 'note'),
|
||||
('DATA_TABLE', 'note'),
|
||||
)
|
||||
FUNCTION = "load"
|
||||
|
||||
DESCRIPTION = (
|
||||
"Read the Note metadata from an .ibw file and display numeric entries "
|
||||
"as a measurement table. Non-numeric note entries are skipped."
|
||||
"Read the Note metadata from an .ibw file and display all entries "
|
||||
"as a table of key/value pairs."
|
||||
)
|
||||
|
||||
def load(self, filename: str = "", path: str | None = None) -> tuple:
|
||||
@@ -73,6 +69,6 @@ class IBWNote:
|
||||
|
||||
rows = _parse_ibw_note(note_bytes)
|
||||
if not rows:
|
||||
raise ValueError("No numeric metadata found in the .ibw note.")
|
||||
raise ValueError("No metadata found in the .ibw note.")
|
||||
|
||||
return (MeasureTable(rows),)
|
||||
return (DataTable(rows),)
|
||||
|
||||
@@ -2,6 +2,8 @@ from __future__ import annotations
|
||||
from functools import lru_cache
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
import nanonispy as nap
|
||||
import gwyfile
|
||||
|
||||
from backend.node_registry import register_node
|
||||
from backend.execution_context import emit_warning
|
||||
@@ -25,6 +27,7 @@ class Image:
|
||||
}
|
||||
|
||||
OUTPUTS = (
|
||||
("FILE_PATH", 'path'),
|
||||
('DATA_FIELD', 'field'),
|
||||
)
|
||||
FUNCTION = "load"
|
||||
@@ -65,7 +68,7 @@ class Image:
|
||||
if ext not in _SPM_EXTENSIONS:
|
||||
self._send_warning("Uncalibrated data — no physical dimensions.")
|
||||
|
||||
return fields
|
||||
return (str(path_obj.resolve()),) + fields
|
||||
|
||||
def _send_warning(self, message: str):
|
||||
emit_warning(message)
|
||||
@@ -92,11 +95,6 @@ class Image:
|
||||
|
||||
@staticmethod
|
||||
def _load_gwy_all(path: Path) -> list[DataField]:
|
||||
try:
|
||||
import gwyfile
|
||||
except ImportError:
|
||||
raise ImportError("Install 'gwyfile' package to load .gwy files: pip install gwyfile")
|
||||
|
||||
obj = gwyfile.load(str(path))
|
||||
channels = gwyfile.util.get_datafields(obj)
|
||||
if not channels:
|
||||
@@ -118,11 +116,6 @@ class Image:
|
||||
|
||||
@staticmethod
|
||||
def _load_sxm_all(path: Path) -> list[DataField]:
|
||||
try:
|
||||
import nanonispy as nap
|
||||
except ImportError:
|
||||
raise ImportError("Install 'nanonispy' package to load .sxm files: pip install nanonispy")
|
||||
|
||||
sxm = nap.read.Scan(str(path))
|
||||
signals = sxm.signals
|
||||
if not signals:
|
||||
|
||||
@@ -45,8 +45,20 @@ class PreviewImage:
|
||||
input=None,
|
||||
colormap_map=None,
|
||||
) -> tuple:
|
||||
field = input if isinstance(input, DataField) else None
|
||||
image = None if field is not None else input
|
||||
if isinstance(input, DataField):
|
||||
field = input
|
||||
image = None
|
||||
elif isinstance(input, np.ndarray):
|
||||
field = None
|
||||
image = input
|
||||
elif input is not None:
|
||||
raise TypeError(
|
||||
f"Preview expects an IMAGE or DATA_FIELD — got {type(input).__name__}. "
|
||||
"Check that you are connected to the DATA_FIELD output, not the path socket."
|
||||
)
|
||||
else:
|
||||
field = None
|
||||
image = None
|
||||
|
||||
resolved_colormap = resolve_colormap_input(
|
||||
colormap,
|
||||
|
||||
@@ -9,8 +9,8 @@ class PrintTable:
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"table": ("MEASURE_TABLE", {
|
||||
"accepted_types": ["RECORD_TABLE"],
|
||||
"table": ("RECORD_TABLE", {
|
||||
"accepted_types": ["DATA_TABLE"],
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ class Save:
|
||||
"IMAGE",
|
||||
"ANNOTATION_SOURCE",
|
||||
"LINE",
|
||||
"MEASURE_TABLE",
|
||||
"RECORD_TABLE",
|
||||
"DATA_TABLE",
|
||||
"MESH_MODEL",
|
||||
"FLOAT",
|
||||
],
|
||||
@@ -48,8 +48,8 @@ class Save:
|
||||
"IMAGE": ["PNG", "TIFF", "NPZ"],
|
||||
"ANNOTATION_SOURCE": ["PNG", "TIFF", "NPZ"],
|
||||
"LINE": ["CSV", "NPZ", "JSON"],
|
||||
"MEASURE_TABLE": ["CSV", "JSON"],
|
||||
"RECORD_TABLE": ["CSV", "JSON"],
|
||||
"DATA_TABLE": ["CSV", "JSON"],
|
||||
"FLOAT": ["TXT", "JSON"],
|
||||
"MESH_MODEL": ["OBJ", "STL"],
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
import numpy as np
|
||||
from backend.node_registry import register_node
|
||||
from backend.data_types import DataField, MeasureTable
|
||||
from backend.data_types import DataField, RecordTable
|
||||
|
||||
|
||||
@register_node(display_name="Statistics")
|
||||
@@ -15,7 +15,7 @@ class Statistics:
|
||||
}
|
||||
|
||||
OUTPUTS = (
|
||||
('MEASURE_TABLE', 'stats'),
|
||||
('RECORD_TABLE', 'stats'),
|
||||
)
|
||||
FUNCTION = "process"
|
||||
|
||||
@@ -31,7 +31,7 @@ class Statistics:
|
||||
skewness = float(np.mean(((d - mean) / rms) ** 3)) if rms > 0 else 0.0
|
||||
kurtosis = float(np.mean(((d - mean) / rms) ** 4)) if rms > 0 else 0.0
|
||||
|
||||
table = MeasureTable([
|
||||
table = RecordTable([
|
||||
{"quantity": "min", "value": float(d.min()), "unit": field.si_unit_z},
|
||||
{"quantity": "max", "value": float(d.max()), "unit": field.si_unit_z},
|
||||
{"quantity": "mean", "value": mean, "unit": field.si_unit_z},
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
import numpy as np
|
||||
from backend.node_registry import register_node
|
||||
from backend.execution_context import emit_value
|
||||
from backend.data_types import DataField, LineData, MeasureTable
|
||||
from backend.data_types import DataField, LineData, RecordTable
|
||||
from backend.nodes.helpers import (
|
||||
LINE_OPS,
|
||||
TABLE_OPS,
|
||||
@@ -17,7 +17,7 @@ from backend.nodes.helpers import (
|
||||
|
||||
@register_node(display_name="Stats")
|
||||
class Stats:
|
||||
"""Polymorphic scalar stats node for LINE, RECORD_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 = ""
|
||||
@@ -27,20 +27,20 @@ class Stats:
|
||||
return {
|
||||
"required": {
|
||||
"input": ("DATA_FIELD", {
|
||||
"accepted_types": ["IMAGE", "LINE", "RECORD_TABLE"],
|
||||
"accepted_types": ["IMAGE", "LINE", "DATA_TABLE"],
|
||||
}),
|
||||
"column": ("STRING", {
|
||||
"default": "value",
|
||||
"choices_from_table_input": "input",
|
||||
"show_when_source_type": {
|
||||
"input": ["RECORD_TABLE"],
|
||||
"input": ["DATA_TABLE"],
|
||||
},
|
||||
}),
|
||||
"operation": ("STRING", {
|
||||
"default": "mean",
|
||||
"choices_by_source_type": {
|
||||
"LINE": list(LINE_OPS.keys()),
|
||||
"RECORD_TABLE": list(TABLE_OPS.keys()),
|
||||
"DATA_TABLE": list(TABLE_OPS.keys()),
|
||||
"DATA_FIELD": list(ARRAY_OPS.keys()),
|
||||
"IMAGE": list(ARRAY_OPS.keys()),
|
||||
},
|
||||
@@ -62,7 +62,7 @@ class Stats:
|
||||
def process(self, input, operation: str, column: str = "value") -> tuple:
|
||||
source_type, values, resolved_column = self._resolve_input_values(input, column)
|
||||
|
||||
if source_type == "RECORD_TABLE":
|
||||
if source_type == "DATA_TABLE":
|
||||
ops = TABLE_OPS
|
||||
elif source_type == "LINE":
|
||||
ops = LINE_OPS
|
||||
@@ -93,7 +93,7 @@ class Stats:
|
||||
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 == "DATA_TABLE" and isinstance(input_value, list) and column:
|
||||
return _apply_scalar_unit(_common_table_unit(input_value, column), operation)
|
||||
|
||||
return ""
|
||||
@@ -103,7 +103,7 @@ class Stats:
|
||||
values = np.asarray(input_value.data, dtype=np.float64)
|
||||
return ("DATA_FIELD", values.ravel(), None)
|
||||
|
||||
if isinstance(input_value, MeasureTable):
|
||||
if isinstance(input_value, RecordTable):
|
||||
raise ValueError("Stats only accepts record tables, not measurement tables.")
|
||||
|
||||
if isinstance(input_value, list):
|
||||
@@ -113,7 +113,7 @@ class Stats:
|
||||
values = extract_numeric_table_values(input_value, column_name)
|
||||
if not values:
|
||||
raise ValueError(f"Column '{column_name}' has no numeric values.")
|
||||
return ("RECORD_TABLE", np.asarray(values, dtype=np.float64), column_name)
|
||||
return ("DATA_TABLE", np.asarray(values, dtype=np.float64), column_name)
|
||||
|
||||
if isinstance(input_value, LineData):
|
||||
values = np.asarray(input_value.data, dtype=np.float64)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
from backend.node_registry import register_node
|
||||
from backend.execution_context import emit_value
|
||||
from backend.data_types import MeasureTable
|
||||
from backend.data_types import RecordTable
|
||||
from backend.nodes.helpers import _measurement_entry, _measurement_value, _scalar_payload
|
||||
|
||||
|
||||
@@ -12,13 +12,13 @@ class ValueDisplay:
|
||||
return {
|
||||
"required": {
|
||||
"value": ("FLOAT", {
|
||||
"accepted_types": ["MEASURE_TABLE"],
|
||||
"accepted_types": ["RECORD_TABLE"],
|
||||
}),
|
||||
"measurement": ("STRING", {
|
||||
"default": "",
|
||||
"choices_from_measure_input": "value",
|
||||
"show_when_source_type": {
|
||||
"value": ["MEASURE_TABLE"],
|
||||
"value": ["RECORD_TABLE"],
|
||||
},
|
||||
}),
|
||||
}
|
||||
@@ -36,7 +36,7 @@ class ValueDisplay:
|
||||
|
||||
def display_value(self, value, measurement: str = "") -> tuple:
|
||||
unit = ""
|
||||
if isinstance(value, MeasureTable):
|
||||
if isinstance(value, RecordTable):
|
||||
row = _measurement_entry(value, measurement)
|
||||
numeric = _measurement_value(value, measurement)
|
||||
unit = row.get("unit", "") if isinstance(row.get("unit"), str) else ""
|
||||
|
||||
@@ -1271,7 +1271,7 @@ function Flow() {
|
||||
if (!isTrackedNodeRequestCurrent(loadNodeOutputRequestVersionsRef.current, nodeId, requestVersion)) {
|
||||
return;
|
||||
}
|
||||
setNodeOutputs(nodeId, ['DATA_FIELD'], ['field'], { output_paths: [] });
|
||||
setNodeOutputs(nodeId, ['FILE_PATH', 'DATA_FIELD'], ['path', 'field'], { output_paths: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1281,8 +1281,8 @@ function Flow() {
|
||||
}
|
||||
setNodeOutputs(
|
||||
nodeId,
|
||||
channels.map((channel) => channel.type),
|
||||
channels.map((channel) => channel.name),
|
||||
['FILE_PATH', ...channels.map((channel) => channel.type)],
|
||||
['path', ...channels.map((channel) => channel.name)],
|
||||
{ output_paths: [] },
|
||||
);
|
||||
}, [getResolvedPathInput, reactFlow, setNodeOutputs]);
|
||||
|
||||
@@ -786,6 +786,17 @@ function ColorMapStopsEditor({ nodeId, name, value, onChange }) {
|
||||
}
|
||||
|
||||
function NodeTable({ rows }) {
|
||||
const [query, setQuery] = useState('');
|
||||
const scrollRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = scrollRef.current;
|
||||
if (!el) return;
|
||||
const handler = (e) => e.stopPropagation();
|
||||
el.addEventListener('wheel', handler, { passive: false });
|
||||
return () => el.removeEventListener('wheel', handler);
|
||||
}, []);
|
||||
|
||||
const columns = getTableColumns(rows);
|
||||
if (columns.length === 0) return null;
|
||||
const lowerColumns = columns.map((column) => String(column).toLowerCase());
|
||||
@@ -804,9 +815,32 @@ function NodeTable({ rows }) {
|
||||
return '';
|
||||
};
|
||||
|
||||
const filteredRows = query.trim()
|
||||
? rows.filter((row) =>
|
||||
columns.some((col) => {
|
||||
const cell = formatTableRowCell(row, col);
|
||||
return String(cell).toLowerCase().includes(query.toLowerCase());
|
||||
})
|
||||
)
|
||||
: rows;
|
||||
|
||||
return (
|
||||
<div className="node-table-wrap">
|
||||
<div className="node-table-scroll">
|
||||
{rows.length > 5 && (
|
||||
<div
|
||||
className="node-table-search"
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<input
|
||||
className="node-table-search-input nodrag"
|
||||
type="text"
|
||||
placeholder="Search…"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="node-table-scroll" ref={scrollRef}>
|
||||
<table className="node-table-grid">
|
||||
{hasMeasurementLayout && (
|
||||
<colgroup>
|
||||
@@ -823,7 +857,7 @@ function NodeTable({ rows }) {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row, rowIndex) => (
|
||||
{filteredRows.map((row, rowIndex) => (
|
||||
<tr key={row.id ?? row.quantity ?? rowIndex}>
|
||||
{columns.map((column) => {
|
||||
const value = row?.[column];
|
||||
@@ -1379,7 +1413,7 @@ function WidgetControl({ widget, nodeId, value, widgetValues, onChange, openFile
|
||||
const tableInputName = opts?.choices_from_table_input;
|
||||
if (!tableInputName) return [];
|
||||
const sourceType = getSourceTypeForInput(s, nodeId, tableInputName);
|
||||
if (sourceType !== 'RECORD_TABLE') return [];
|
||||
if (sourceType !== 'DATA_TABLE') return [];
|
||||
const sourceNode = getSourceNodeForInput(s, nodeId, tableInputName);
|
||||
const rows = sourceNode?.data?.tableRows;
|
||||
return Array.isArray(rows) ? getTableColumns(rows) : [];
|
||||
@@ -1393,7 +1427,7 @@ function WidgetControl({ widget, nodeId, value, widgetValues, onChange, openFile
|
||||
const measurementInputName = opts?.choices_from_measure_input;
|
||||
if (!measurementInputName) return [];
|
||||
const sourceType = getSourceTypeForInput(s, nodeId, measurementInputName);
|
||||
if (sourceType !== 'MEASURE_TABLE') return [];
|
||||
if (sourceType !== 'RECORD_TABLE') return [];
|
||||
const sourceNode = getSourceNodeForInput(s, nodeId, measurementInputName);
|
||||
const rows = sourceNode?.data?.tableRows;
|
||||
return Array.isArray(rows) ? getMeasurementChoices(rows) : [];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// ── Shared type & color constants ─────────────────────────────────────
|
||||
|
||||
export const DATA_TYPES = new Set([
|
||||
'DATA_FIELD', 'IMAGE', 'LINE', 'MEASURE_TABLE', 'RECORD_TABLE',
|
||||
'DATA_FIELD', 'IMAGE', 'LINE', 'RECORD_TABLE', 'DATA_TABLE',
|
||||
'COORD', 'ANNOTATION_SOURCE', 'COLORMAP',
|
||||
'MESH_MODEL', 'FONT', 'FILE_PATH', 'DIRECTORY', 'COORDPAIR',
|
||||
]);
|
||||
@@ -12,8 +12,8 @@ export const TYPE_COLORS = {
|
||||
DATA_FIELD: '#3a7abf',
|
||||
IMAGE: '#00ff08a0',
|
||||
LINE: '#ffbe5c',
|
||||
MEASURE_TABLE: '#35e2fd',
|
||||
RECORD_TABLE: '#ff7474',
|
||||
RECORD_TABLE: '#35e2fd',
|
||||
DATA_TABLE: '#ff7474',
|
||||
COORD: '#e91ed1',
|
||||
COORDPAIR: '#5cb861',
|
||||
FLOAT: '#ab3197',
|
||||
|
||||
@@ -1296,6 +1296,26 @@ html, body, #root {
|
||||
padding: 4px 10px 8px;
|
||||
}
|
||||
|
||||
.node-table-search {
|
||||
padding: 0 0 4px;
|
||||
}
|
||||
|
||||
.node-table-search-input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 3px 7px;
|
||||
font-size: 11px;
|
||||
background: var(--bg-deep);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: 4px;
|
||||
color: var(--text-primary);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.node-table-search-input:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.node-table-scroll {
|
||||
max-height: 220px;
|
||||
overflow: auto;
|
||||
@@ -1310,7 +1330,7 @@ html, body, #root {
|
||||
font-family: "SF Mono", "Fira Code", monospace;
|
||||
font-size: 10px;
|
||||
color: var(--text-table);
|
||||
table-layout: auto;
|
||||
table-layout: fixed;
|
||||
font-variant-numeric: tabular-nums lining-nums;
|
||||
}
|
||||
|
||||
@@ -1319,6 +1339,8 @@ html, body, #root {
|
||||
padding: 6px 8px;
|
||||
border-bottom: 1px solid var(--border-table);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
@@ -19,13 +19,13 @@ test('retired save alias types are no longer first-class socket types', () => {
|
||||
});
|
||||
|
||||
test('accepted_types extend canonical socket compatibility without reintroducing alias types', () => {
|
||||
const spec = ['MEASURE_TABLE', { accepted_types: ['RECORD_TABLE'] }];
|
||||
const spec = ['RECORD_TABLE', { accepted_types: ['DATA_TABLE'] }];
|
||||
|
||||
assert.equal(isDataSocketSpec(spec), true);
|
||||
assert.deepEqual(
|
||||
Array.from(getAcceptedSocketTypes(spec)).sort(),
|
||||
['MEASURE_TABLE', 'RECORD_TABLE'],
|
||||
['RECORD_TABLE', 'DATA_TABLE'],
|
||||
);
|
||||
assert.equal(socketSpecAcceptsType('RECORD_TABLE', spec), true);
|
||||
assert.equal(socketSpecAcceptsType('DATA_TABLE', spec), true);
|
||||
assert.equal(socketSpecAcceptsType('LINE', spec), false);
|
||||
});
|
||||
|
||||
@@ -487,7 +487,7 @@ test('serializeExecutionGraph treats accepted_types inputs as sockets, not widge
|
||||
className: 'TableSource',
|
||||
definition: {
|
||||
input: { required: {}, optional: {} },
|
||||
output: ['RECORD_TABLE'],
|
||||
output: ['DATA_TABLE'],
|
||||
output_name: ['rows'],
|
||||
manual_trigger: false,
|
||||
},
|
||||
@@ -501,7 +501,7 @@ test('serializeExecutionGraph treats accepted_types inputs as sockets, not widge
|
||||
definition: {
|
||||
input: {
|
||||
required: {
|
||||
table: ['MEASURE_TABLE', { accepted_types: ['RECORD_TABLE'] }],
|
||||
table: ['RECORD_TABLE', { accepted_types: ['DATA_TABLE'] }],
|
||||
},
|
||||
optional: {},
|
||||
},
|
||||
@@ -514,9 +514,9 @@ test('serializeExecutionGraph treats accepted_types inputs as sockets, not widge
|
||||
const edges = [
|
||||
{
|
||||
source: '1',
|
||||
sourceHandle: 'output::0::RECORD_TABLE',
|
||||
sourceHandle: 'output::0::DATA_TABLE',
|
||||
target: '2',
|
||||
targetHandle: 'input::table::MEASURE_TABLE',
|
||||
targetHandle: 'input::table::RECORD_TABLE',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -542,7 +542,7 @@ test('hasBlockingAutoRunInput still blocks unconnected accepted_types sockets',
|
||||
manual_trigger: false,
|
||||
input: {
|
||||
required: {
|
||||
input: ['DATA_FIELD', { accepted_types: ['IMAGE', 'LINE', 'RECORD_TABLE'] }],
|
||||
input: ['DATA_FIELD', { accepted_types: ['IMAGE', 'LINE', 'DATA_TABLE'] }],
|
||||
},
|
||||
optional: {},
|
||||
},
|
||||
@@ -556,7 +556,7 @@ test('hasBlockingAutoRunInput still blocks unconnected accepted_types sockets',
|
||||
hasBlockingAutoRunInput(node, [
|
||||
{
|
||||
source: '1',
|
||||
sourceHandle: 'output::0::RECORD_TABLE',
|
||||
sourceHandle: 'output::0::DATA_TABLE',
|
||||
target: '2',
|
||||
targetHandle: 'input::input::DATA_FIELD',
|
||||
},
|
||||
|
||||
@@ -16,7 +16,7 @@ test('buildDefaultWidgetValues keeps non-data required widget defaults', () => {
|
||||
input: {
|
||||
required: {
|
||||
input: ['ANNOTATION_SOURCE', { label: 'Input' }],
|
||||
table: ['MEASURE_TABLE', { accepted_types: ['RECORD_TABLE'] }],
|
||||
table: ['RECORD_TABLE', { accepted_types: ['DATA_TABLE'] }],
|
||||
shape: [['line', 'rectangle', 'circle', 'arrow'], { default: 'arrow' }],
|
||||
stroke_color: ['STRING', { default: '#ff0000', color_picker: true }],
|
||||
stroke_width: ['INT', { default: 3 }],
|
||||
|
||||
@@ -12,7 +12,7 @@ from pathlib import Path
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, ".")
|
||||
from backend.data_types import DataField, LineData, MeasureTable, RecordTable, datafield_to_uint8, render_datafield_preview
|
||||
from backend.data_types import DataField, LineData, RecordTable, DataTable, datafield_to_uint8, render_datafield_preview
|
||||
|
||||
|
||||
def make_field(data=None, shape=(64, 64), xreal=1e-6, yreal=1e-6):
|
||||
@@ -2000,8 +2000,8 @@ def test_print_table():
|
||||
node = PrintTable()
|
||||
|
||||
table_spec = PrintTable.INPUT_TYPES()["required"]["table"]
|
||||
assert table_spec[0] == "MEASURE_TABLE"
|
||||
assert table_spec[1]["accepted_types"] == ["RECORD_TABLE"]
|
||||
assert table_spec[0] == "RECORD_TABLE"
|
||||
assert table_spec[1]["accepted_types"] == ["DATA_TABLE"]
|
||||
|
||||
captured = []
|
||||
PrintTable._broadcast_table_fn = lambda node_id, rows: captured.append(rows)
|
||||
@@ -2023,7 +2023,7 @@ def test_value_display():
|
||||
node = ValueDisplay()
|
||||
value_spec = ValueDisplay.INPUT_TYPES()["required"]["value"]
|
||||
assert value_spec[0] == "FLOAT"
|
||||
assert value_spec[1]["accepted_types"] == ["MEASURE_TABLE"]
|
||||
assert value_spec[1]["accepted_types"] == ["RECORD_TABLE"]
|
||||
|
||||
captured = []
|
||||
ValueDisplay._broadcast_value_fn = lambda node_id, payload: captured.append((node_id, payload))
|
||||
@@ -2033,7 +2033,7 @@ def test_value_display():
|
||||
assert result == (3.25,)
|
||||
assert captured == [("test", {"value": 3.25})]
|
||||
|
||||
measurements = MeasureTable([
|
||||
measurements = RecordTable([
|
||||
{"quantity": "delta X", "value": 1.7e-7, "unit": "m"},
|
||||
{"quantity": "delta Y", "value": 463, "unit": "count"},
|
||||
])
|
||||
@@ -2845,7 +2845,7 @@ def test_stats():
|
||||
node = Stats()
|
||||
input_spec = Stats.INPUT_TYPES()["required"]["input"]
|
||||
assert input_spec[0] == "DATA_FIELD"
|
||||
assert input_spec[1]["accepted_types"] == ["IMAGE", "LINE", "RECORD_TABLE"]
|
||||
assert input_spec[1]["accepted_types"] == ["IMAGE", "LINE", "DATA_TABLE"]
|
||||
|
||||
captured = []
|
||||
Stats._broadcast_value_fn = lambda node_id, payload: captured.append((node_id, payload))
|
||||
@@ -2858,7 +2858,7 @@ def test_stats():
|
||||
roughness, = node.process(line, operation="Rq", column="value")
|
||||
assert np.isclose(roughness, np.sqrt(np.mean((line - line.mean()) ** 2)))
|
||||
|
||||
table = RecordTable([
|
||||
table = DataTable([
|
||||
{"name": "a", "value": 3.0, "unit": "m", "other": 10.0},
|
||||
{"name": "b", "value": 7.0, "unit": "m", "other": 20.0},
|
||||
])
|
||||
@@ -2894,7 +2894,7 @@ def test_stats():
|
||||
|
||||
try:
|
||||
node.process(
|
||||
MeasureTable([{"quantity": "min", "value": 1.0, "unit": "m"}]),
|
||||
RecordTable([{"quantity": "min", "value": 1.0, "unit": "m"}]),
|
||||
operation="max",
|
||||
column="value",
|
||||
)
|
||||
@@ -3026,7 +3026,7 @@ def test_view3d():
|
||||
def test_save_generic():
|
||||
print("=== Test: Save ===")
|
||||
from backend.nodes.save import Save
|
||||
from backend.data_types import DataField, ImageData, LineData, MeasureTable, MeshModel, RecordTable
|
||||
from backend.data_types import DataField, ImageData, LineData, RecordTable, MeshModel, DataTable
|
||||
import tifffile
|
||||
from PIL import Image as PILImage
|
||||
|
||||
@@ -3037,8 +3037,8 @@ def test_save_generic():
|
||||
"IMAGE",
|
||||
"ANNOTATION_SOURCE",
|
||||
"LINE",
|
||||
"MEASURE_TABLE",
|
||||
"RECORD_TABLE",
|
||||
"DATA_TABLE",
|
||||
"MESH_MODEL",
|
||||
"FLOAT",
|
||||
]
|
||||
@@ -3137,7 +3137,7 @@ def test_save_generic():
|
||||
assert np.array_equal(annotation_npz["image"], image)
|
||||
|
||||
# Save tables as CSV and JSON
|
||||
measure_table = MeasureTable([
|
||||
measure_table = RecordTable([
|
||||
{"quantity": "Rq", "value": 1.23, "unit": "nm"},
|
||||
{"quantity": "Ra", "value": 0.98, "unit": "nm"},
|
||||
])
|
||||
@@ -3148,7 +3148,7 @@ def test_save_generic():
|
||||
node.save(filename="measurements_json", directory_path=tmpdir, format="JSON", value=measure_table)
|
||||
assert json.loads(Path(tmpdir, "measurements_json.json").read_text(encoding="utf-8")) == list(measure_table)
|
||||
|
||||
record_table = RecordTable([
|
||||
record_table = DataTable([
|
||||
{"label": "particle-1", "height": 12.0, "area": 44.0},
|
||||
{"label": "particle-2", "height": 8.0, "area": 21.0},
|
||||
])
|
||||
|
||||
Reference in New Issue
Block a user