work on igor note

This commit is contained in:
2026-03-28 18:48:25 -07:00
parent ce74cf0a3e
commit 559b1ae09a
26 changed files with 166 additions and 109 deletions

View File

@@ -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."""

View File

@@ -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

View File

@@ -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))

View File

@@ -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},

View File

@@ -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},

View File

@@ -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},

View File

@@ -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},
])

View File

@@ -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": ""},

View File

@@ -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())

View File

@@ -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},

View File

@@ -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),)

View File

@@ -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:

View File

@@ -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,

View File

@@ -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"],
}),
}
}

View File

@@ -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"],
},

View File

@@ -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},

View File

@@ -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)

View File

@@ -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 ""

View File

@@ -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]);

View File

@@ -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) : [];

View File

@@ -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',

View File

@@ -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;
}

View File

@@ -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);
});

View File

@@ -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',
},

View File

@@ -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 }],

View File

@@ -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},
])