make cursors polymorphic
This commit is contained in:
@@ -219,7 +219,7 @@ class ExecutionEngine:
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Wire up broadcast callbacks on display node classes."""
|
"""Wire up broadcast callbacks on display node classes."""
|
||||||
from backend.nodes.display import PreviewImage, PrintTable, View3D, ValueDisplay, Markup
|
from backend.nodes.display import PreviewImage, PrintTable, View3D, ValueDisplay, Markup
|
||||||
from backend.nodes.analysis import CrossSection, LineCursors, Stats, Histogram
|
from backend.nodes.analysis import CrossSection, Cursors, Stats, Histogram
|
||||||
from backend.nodes.modify import CropResizeField, RotateField
|
from backend.nodes.modify import CropResizeField, RotateField
|
||||||
from backend.nodes.mask import ThresholdMask, MaskMorphology, MaskInvert, MaskCombine, DrawMask
|
from backend.nodes.mask import ThresholdMask, MaskMorphology, MaskInvert, MaskCombine, DrawMask
|
||||||
from backend.nodes.io import SaveImage, Image, ImageDemo
|
from backend.nodes.io import SaveImage, Image, ImageDemo
|
||||||
@@ -236,7 +236,7 @@ class ExecutionEngine:
|
|||||||
Stats._broadcast_value_fn = on_value
|
Stats._broadcast_value_fn = on_value
|
||||||
Histogram._broadcast_overlay_fn = on_overlay
|
Histogram._broadcast_overlay_fn = on_overlay
|
||||||
CrossSection._broadcast_overlay_fn = on_overlay
|
CrossSection._broadcast_overlay_fn = on_overlay
|
||||||
LineCursors._broadcast_overlay_fn = on_overlay
|
Cursors._broadcast_overlay_fn = on_overlay
|
||||||
CropResizeField._broadcast_overlay_fn = on_overlay
|
CropResizeField._broadcast_overlay_fn = on_overlay
|
||||||
RotateField._broadcast_warning_fn = on_warning
|
RotateField._broadcast_warning_fn = on_warning
|
||||||
Markup._broadcast_overlay_fn = on_overlay
|
Markup._broadcast_overlay_fn = on_overlay
|
||||||
@@ -247,11 +247,11 @@ class ExecutionEngine:
|
|||||||
def _set_node_id_on_display(self, cls: type, node_id: str) -> None:
|
def _set_node_id_on_display(self, cls: type, node_id: str) -> None:
|
||||||
"""Inform display nodes of their current node_id for WS tagging."""
|
"""Inform display nodes of their current node_id for WS tagging."""
|
||||||
from backend.nodes.display import PreviewImage, PrintTable, View3D, ValueDisplay, Markup
|
from backend.nodes.display import PreviewImage, PrintTable, View3D, ValueDisplay, Markup
|
||||||
from backend.nodes.analysis import CrossSection, LineCursors, Stats, Histogram
|
from backend.nodes.analysis import CrossSection, Cursors, Stats, Histogram
|
||||||
from backend.nodes.modify import CropResizeField, RotateField
|
from backend.nodes.modify import CropResizeField, RotateField
|
||||||
from backend.nodes.mask import ThresholdMask, MaskMorphology, MaskInvert, MaskCombine, DrawMask
|
from backend.nodes.mask import ThresholdMask, MaskMorphology, MaskInvert, MaskCombine, DrawMask
|
||||||
from backend.nodes.io import Image, ImageDemo, SaveImage
|
from backend.nodes.io import Image, ImageDemo, SaveImage
|
||||||
if cls in (PreviewImage, PrintTable, View3D, ValueDisplay, Stats, Histogram, CrossSection, LineCursors, CropResizeField, RotateField, Markup,
|
if cls in (PreviewImage, PrintTable, View3D, ValueDisplay, Stats, Histogram, CrossSection, Cursors, CropResizeField, RotateField, Markup,
|
||||||
ThresholdMask, MaskMorphology, MaskInvert, MaskCombine, DrawMask,
|
ThresholdMask, MaskMorphology, MaskInvert, MaskCombine, DrawMask,
|
||||||
Image, ImageDemo, SaveImage):
|
Image, ImageDemo, SaveImage):
|
||||||
cls._current_node_id = node_id
|
cls._current_node_id = node_id
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ MENU_LAYOUT: dict[str, list[str]] = {
|
|||||||
"Measure": [
|
"Measure": [
|
||||||
"Statistics",
|
"Statistics",
|
||||||
"Histogram",
|
"Histogram",
|
||||||
"LineCursors",
|
|
||||||
"CrossSection",
|
"CrossSection",
|
||||||
|
"Cursors",
|
||||||
"Stats",
|
"Stats",
|
||||||
],
|
],
|
||||||
"Mask": [
|
"Mask": [
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ 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
|
from backend.data_types import DataField, MeasureTable, RecordTable, datafield_to_uint8, encode_preview, render_datafield_preview
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -159,18 +159,18 @@ class Histogram:
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# LineCursors — interactive measurement cursors on any LINE plot
|
# Cursors — interactive measurement cursors on lines or fields
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@register_node(display_name="Line Cursors")
|
@register_node(display_name="Cursors")
|
||||||
class LineCursors:
|
class Cursors:
|
||||||
"""Place two draggable cursors on any LINE plot to measure values and deltas."""
|
"""Place two draggable cursors on a line plot or field to measure deltas."""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"line": ("LINE",),
|
"line": ("CURSOR_SOURCE", {"label": "input"}),
|
||||||
"x1": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.01, "hidden": True}),
|
"x1": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.01, "hidden": True}),
|
||||||
"y1": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01, "hidden": True}),
|
"y1": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01, "hidden": True}),
|
||||||
"x2": ("FLOAT", {"default": 0.75, "min": 0.0, "max": 1.0, "step": 0.01, "hidden": True}),
|
"x2": ("FLOAT", {"default": 0.75, "min": 0.0, "max": 1.0, "step": 0.01, "hidden": True}),
|
||||||
@@ -186,8 +186,9 @@ class LineCursors:
|
|||||||
FUNCTION = "process"
|
FUNCTION = "process"
|
||||||
CATEGORY = "analysis"
|
CATEGORY = "analysis"
|
||||||
DESCRIPTION = (
|
DESCRIPTION = (
|
||||||
"Place two cursors on any line plot (histogram, cross section, profile) "
|
"Place two cursors on a line plot or 2D field. "
|
||||||
"to measure positions, values, and deltas. Drag the markers to reposition."
|
"On lines it reports x/y positions and dx/dy. "
|
||||||
|
"On fields it reports x/y/z at both markers plus dx/dy/dz."
|
||||||
)
|
)
|
||||||
|
|
||||||
_broadcast_overlay_fn = None
|
_broadcast_overlay_fn = None
|
||||||
@@ -196,6 +197,20 @@ class LineCursors:
|
|||||||
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,
|
x_axis=None,
|
||||||
|
) -> tuple:
|
||||||
|
if isinstance(line, DataField):
|
||||||
|
return self._process_field(line, x1=x1, y1=y1, x2=x2, y2=y2)
|
||||||
|
|
||||||
|
return self._process_line(line, x1=x1, y1=y1, x2=x2, y2=y2, x_axis=x_axis)
|
||||||
|
|
||||||
|
def _process_line(
|
||||||
|
self,
|
||||||
|
line,
|
||||||
|
x1: float,
|
||||||
|
y1: float,
|
||||||
|
x2: float,
|
||||||
|
y2: float,
|
||||||
|
x_axis=None,
|
||||||
) -> tuple:
|
) -> tuple:
|
||||||
y = np.asarray(line, dtype=np.float64).ravel()
|
y = np.asarray(line, dtype=np.float64).ravel()
|
||||||
n = len(y)
|
n = len(y)
|
||||||
@@ -224,12 +239,12 @@ class LineCursors:
|
|||||||
xb, yb = float(x[idx_b]), float(y[idx_b])
|
xb, yb = float(x[idx_b]), float(y[idx_b])
|
||||||
|
|
||||||
# --- Broadcast overlay ---
|
# --- Broadcast overlay ---
|
||||||
if LineCursors._broadcast_overlay_fn is not None:
|
if Cursors._broadcast_overlay_fn is not None:
|
||||||
LineCursors._broadcast_overlay_fn(
|
Cursors._broadcast_overlay_fn(
|
||||||
LineCursors._current_node_id,
|
Cursors._current_node_id,
|
||||||
{
|
{
|
||||||
"kind": "line_plot",
|
"kind": "line_plot",
|
||||||
"section_title": "Line Cursors",
|
"section_title": "Cursors",
|
||||||
"line": y.tolist(),
|
"line": y.tolist(),
|
||||||
"x_axis": x.tolist(),
|
"x_axis": x.tolist(),
|
||||||
"x1": x1,
|
"x1": x1,
|
||||||
@@ -243,12 +258,69 @@ class LineCursors:
|
|||||||
|
|
||||||
# --- Output table ---
|
# --- Output table ---
|
||||||
table = MeasureTable([
|
table = MeasureTable([
|
||||||
{"quantity": "A position", "value": xa, "unit": ""},
|
{"quantity": "A x", "value": xa, "unit": ""},
|
||||||
{"quantity": "A value", "value": ya, "unit": ""},
|
{"quantity": "A y", "value": ya, "unit": ""},
|
||||||
{"quantity": "B position", "value": xb, "unit": ""},
|
{"quantity": "B x", "value": xb, "unit": ""},
|
||||||
{"quantity": "B value", "value": yb, "unit": ""},
|
{"quantity": "B y", "value": yb, "unit": ""},
|
||||||
{"quantity": "delta X", "value": xb - xa, "unit": ""},
|
{"quantity": "dx", "value": xb - xa, "unit": ""},
|
||||||
{"quantity": "delta Y", "value": yb - ya, "unit": ""},
|
{"quantity": "dy", "value": yb - ya, "unit": ""},
|
||||||
|
])
|
||||||
|
return (table,)
|
||||||
|
|
||||||
|
def _process_field(
|
||||||
|
self,
|
||||||
|
field: DataField,
|
||||||
|
x1: float,
|
||||||
|
y1: float,
|
||||||
|
x2: float,
|
||||||
|
y2: float,
|
||||||
|
) -> tuple:
|
||||||
|
from scipy.ndimage import map_coordinates
|
||||||
|
|
||||||
|
x1 = float(np.clip(x1, 0.0, 1.0))
|
||||||
|
y1 = float(np.clip(y1, 0.0, 1.0))
|
||||||
|
x2 = float(np.clip(x2, 0.0, 1.0))
|
||||||
|
y2 = float(np.clip(y2, 0.0, 1.0))
|
||||||
|
|
||||||
|
px1 = x1 * max(field.xres - 1, 0)
|
||||||
|
py1 = y1 * max(field.yres - 1, 0)
|
||||||
|
px2 = x2 * max(field.xres - 1, 0)
|
||||||
|
py2 = y2 * max(field.yres - 1, 0)
|
||||||
|
|
||||||
|
z1 = float(map_coordinates(field.data, [[py1], [px1]], order=1, mode="nearest")[0])
|
||||||
|
z2 = float(map_coordinates(field.data, [[py2], [px2]], order=1, mode="nearest")[0])
|
||||||
|
|
||||||
|
ax = float(field.xoff + x1 * field.xreal)
|
||||||
|
ay = float(field.yoff + y1 * field.yreal)
|
||||||
|
bx = float(field.xoff + x2 * field.xreal)
|
||||||
|
by = float(field.yoff + y2 * field.yreal)
|
||||||
|
|
||||||
|
if Cursors._broadcast_overlay_fn is not None:
|
||||||
|
Cursors._broadcast_overlay_fn(
|
||||||
|
Cursors._current_node_id,
|
||||||
|
{
|
||||||
|
"kind": "cursor_points",
|
||||||
|
"section_title": "Cursors",
|
||||||
|
"image": encode_preview(render_datafield_preview(field, field.colormap)),
|
||||||
|
"x1": x1,
|
||||||
|
"y1": y1,
|
||||||
|
"x2": x2,
|
||||||
|
"y2": y2,
|
||||||
|
"a_locked": False,
|
||||||
|
"b_locked": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
table = MeasureTable([
|
||||||
|
{"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},
|
||||||
|
{"quantity": "B x", "value": bx, "unit": field.si_unit_xy},
|
||||||
|
{"quantity": "B y", "value": by, "unit": field.si_unit_xy},
|
||||||
|
{"quantity": "B z", "value": z2, "unit": field.si_unit_z},
|
||||||
|
{"quantity": "dx", "value": bx - ax, "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},
|
||||||
])
|
])
|
||||||
return (table,)
|
return (table,)
|
||||||
|
|
||||||
@@ -570,8 +642,8 @@ 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": {
|
||||||
"point_a": ("COORD",),
|
"marker_A": ("COORD",),
|
||||||
"point_b": ("COORD",),
|
"marker_B": ("COORD",),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,15 +664,15 @@ 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,
|
||||||
point_a=None, point_b=None,
|
marker_A=None, marker_B=None,
|
||||||
) -> tuple:
|
) -> tuple:
|
||||||
from scipy.ndimage import map_coordinates
|
from scipy.ndimage import map_coordinates
|
||||||
|
|
||||||
# COORD inputs override widget values
|
# COORD inputs override widget values
|
||||||
if point_a is not None:
|
if marker_A is not None:
|
||||||
x1, y1 = float(point_a[0]), float(point_a[1])
|
x1, y1 = float(marker_A[0]), float(marker_A[1])
|
||||||
if point_b is not None:
|
if marker_B is not None:
|
||||||
x2, y2 = float(point_b[0]), float(point_b[1])
|
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)
|
||||||
@@ -642,8 +714,8 @@ 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": point_a is not None,
|
"a_locked": marker_A is not None,
|
||||||
"b_locked": point_b is not None,
|
"b_locked": marker_B is not None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -27,11 +27,12 @@ import {
|
|||||||
|
|
||||||
const DATA_TYPES = new Set([
|
const DATA_TYPES = new Set([
|
||||||
'DATA_FIELD', 'IMAGE', 'LINE', 'MEASURE_TABLE', 'RECORD_TABLE', 'ANY_TABLE',
|
'DATA_FIELD', 'IMAGE', 'LINE', 'MEASURE_TABLE', 'RECORD_TABLE', 'ANY_TABLE',
|
||||||
'COORD', 'STATS_SOURCE', 'VALUE_SOURCE', 'COLORMAP', 'SAVE_LAYER', 'FONT', 'FILE_PATH', 'DIRECTORY',
|
'COORD', 'STATS_SOURCE', 'CURSOR_SOURCE', 'VALUE_SOURCE', 'COLORMAP', 'SAVE_LAYER', 'FONT', 'FILE_PATH', 'DIRECTORY',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const SOCKET_COMPATIBILITY = {
|
const SOCKET_COMPATIBILITY = {
|
||||||
STATS_SOURCE: new Set(['DATA_FIELD', 'IMAGE', 'LINE', 'RECORD_TABLE']),
|
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']),
|
ANY_TABLE: new Set(['MEASURE_TABLE', 'RECORD_TABLE']),
|
||||||
VALUE_SOURCE: new Set(['FLOAT', 'MEASURE_TABLE']),
|
VALUE_SOURCE: new Set(['FLOAT', 'MEASURE_TABLE']),
|
||||||
SAVE_LAYER: new Set(['DATA_FIELD', 'IMAGE']),
|
SAVE_LAYER: new Set(['DATA_FIELD', 'IMAGE']),
|
||||||
@@ -50,6 +51,7 @@ const TYPE_COLORS = {
|
|||||||
FLOAT: '#7dd3fc',
|
FLOAT: '#7dd3fc',
|
||||||
INT: '#38bdf8',
|
INT: '#38bdf8',
|
||||||
STATS_SOURCE:'#c084fc',
|
STATS_SOURCE:'#c084fc',
|
||||||
|
CURSOR_SOURCE:'#a78bfa',
|
||||||
VALUE_SOURCE:'#60a5fa',
|
VALUE_SOURCE:'#60a5fa',
|
||||||
COLORMAP: '#f472b6',
|
COLORMAP: '#f472b6',
|
||||||
SAVE_LAYER: '#22c55e',
|
SAVE_LAYER: '#22c55e',
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export default function CrossSectionOverlay({
|
|||||||
image, x1, y1, x2, y2,
|
image, x1, y1, x2, y2,
|
||||||
aLocked, bLocked,
|
aLocked, bLocked,
|
||||||
nodeId, onWidgetChange,
|
nodeId, onWidgetChange,
|
||||||
|
showLine = true,
|
||||||
}) {
|
}) {
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
const [dragging, setDragging] = useState(null); // 'p1' or 'p2'
|
const [dragging, setDragging] = useState(null); // 'p1' or 'p2'
|
||||||
@@ -62,13 +63,15 @@ export default function CrossSectionOverlay({
|
|||||||
<img src={image} alt="field" draggable={false} className="cs-image" />
|
<img src={image} alt="field" draggable={false} className="cs-image" />
|
||||||
|
|
||||||
{/* Line connecting the two markers */}
|
{/* Line connecting the two markers */}
|
||||||
<svg className="cs-svg">
|
{showLine && (
|
||||||
<line
|
<svg className="cs-svg">
|
||||||
x1={`${x1 * 100}%`} y1={`${y1 * 100}%`}
|
<line
|
||||||
x2={`${x2 * 100}%`} y2={`${y2 * 100}%`}
|
x1={`${x1 * 100}%`} y1={`${y1 * 100}%`}
|
||||||
stroke="#ffd700" strokeWidth="2" strokeDasharray="6 3"
|
x2={`${x2 * 100}%`} y2={`${y2 * 100}%`}
|
||||||
/>
|
stroke="#ffd700" strokeWidth="2" strokeDasharray="6 3"
|
||||||
</svg>
|
/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Endpoint markers — locked markers get a different style */}
|
{/* Endpoint markers — locked markers get a different style */}
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const MarkupOverlay = lazy(() => import('./MarkupOverlay'));
|
|||||||
|
|
||||||
const DATA_TYPES = new Set([
|
const DATA_TYPES = new Set([
|
||||||
'DATA_FIELD', 'IMAGE', 'LINE', 'MEASURE_TABLE', 'RECORD_TABLE', 'ANY_TABLE',
|
'DATA_FIELD', 'IMAGE', 'LINE', 'MEASURE_TABLE', 'RECORD_TABLE', 'ANY_TABLE',
|
||||||
'COORD', 'STATS_SOURCE', 'VALUE_SOURCE', 'COLORMAP', 'SAVE_LAYER', 'FONT', 'FILE_PATH', 'DIRECTORY',
|
'COORD', 'STATS_SOURCE', 'CURSOR_SOURCE', 'VALUE_SOURCE', 'COLORMAP', 'SAVE_LAYER', 'FONT', 'FILE_PATH', 'DIRECTORY',
|
||||||
]);
|
]);
|
||||||
const SOCKET_WIDGET_TYPES = new Set(['FLOAT', 'INT']);
|
const SOCKET_WIDGET_TYPES = new Set(['FLOAT', 'INT']);
|
||||||
|
|
||||||
@@ -27,6 +27,7 @@ const TYPE_COLORS = {
|
|||||||
FLOAT: '#7dd3fc',
|
FLOAT: '#7dd3fc',
|
||||||
INT: '#38bdf8',
|
INT: '#38bdf8',
|
||||||
STATS_SOURCE:'#c084fc',
|
STATS_SOURCE:'#c084fc',
|
||||||
|
CURSOR_SOURCE:'#a78bfa',
|
||||||
VALUE_SOURCE:'#60a5fa',
|
VALUE_SOURCE:'#60a5fa',
|
||||||
COLORMAP: '#f472b6',
|
COLORMAP: '#f472b6',
|
||||||
SAVE_LAYER: '#22c55e',
|
SAVE_LAYER: '#22c55e',
|
||||||
@@ -861,6 +862,8 @@ function CustomNode({ id, data }) {
|
|||||||
? 'Markup'
|
? 'Markup'
|
||||||
: data.overlay?.kind === 'crop_box'
|
: data.overlay?.kind === 'crop_box'
|
||||||
? 'Crop'
|
? 'Crop'
|
||||||
|
: data.overlay?.kind === 'cursor_points'
|
||||||
|
? 'Cursors'
|
||||||
: data.overlay?.kind === 'line_plot'
|
: data.overlay?.kind === 'line_plot'
|
||||||
? 'Line Plot'
|
? 'Line Plot'
|
||||||
: 'Cross Section');
|
: 'Cross Section');
|
||||||
@@ -1088,6 +1091,19 @@ function CustomNode({ id, data }) {
|
|||||||
nodeId={id}
|
nodeId={id}
|
||||||
onWidgetChange={ctx.onWidgetChange}
|
onWidgetChange={ctx.onWidgetChange}
|
||||||
/>
|
/>
|
||||||
|
) : data.overlay.kind === 'cursor_points' ? (
|
||||||
|
<CrossSectionOverlay
|
||||||
|
image={data.overlay.image}
|
||||||
|
x1={data.overlay.a_locked ? data.overlay.x1 : (data.widgetValues.x1 ?? data.overlay.x1)}
|
||||||
|
y1={data.overlay.a_locked ? data.overlay.y1 : (data.widgetValues.y1 ?? data.overlay.y1)}
|
||||||
|
x2={data.overlay.b_locked ? data.overlay.x2 : (data.widgetValues.x2 ?? data.overlay.x2)}
|
||||||
|
y2={data.overlay.b_locked ? data.overlay.y2 : (data.widgetValues.y2 ?? data.overlay.y2)}
|
||||||
|
aLocked={data.overlay.a_locked}
|
||||||
|
bLocked={data.overlay.b_locked}
|
||||||
|
nodeId={id}
|
||||||
|
onWidgetChange={ctx.onWidgetChange}
|
||||||
|
showLine={false}
|
||||||
|
/>
|
||||||
) : data.overlay.kind === 'mask_paint' ? (
|
) : data.overlay.kind === 'mask_paint' ? (
|
||||||
<MaskPaintOverlay
|
<MaskPaintOverlay
|
||||||
image={data.overlay.image}
|
image={data.overlay.image}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const DATA_TYPES = new Set([
|
const DATA_TYPES = new Set([
|
||||||
'DATA_FIELD', 'IMAGE', 'LINE', 'MEASURE_TABLE', 'RECORD_TABLE', 'ANY_TABLE',
|
'DATA_FIELD', 'IMAGE', 'LINE', 'MEASURE_TABLE', 'RECORD_TABLE', 'ANY_TABLE',
|
||||||
'COORD', 'STATS_SOURCE', 'VALUE_SOURCE', 'COLORMAP', 'SAVE_LAYER', 'FONT', 'FILE_PATH', 'DIRECTORY',
|
'COORD', 'STATS_SOURCE', 'CURSOR_SOURCE', 'VALUE_SOURCE', 'COLORMAP', 'SAVE_LAYER', 'FONT', 'FILE_PATH', 'DIRECTORY',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function getInputName(handleId) {
|
function getInputName(handleId) {
|
||||||
|
|||||||
@@ -1612,40 +1612,40 @@ def test_execution_engine_numeric_socket_coercion():
|
|||||||
|
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# Analysis — LineCursors
|
# Analysis — Cursors
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|
||||||
def test_line_cursors():
|
def test_line_cursors():
|
||||||
print("=== Test: LineCursors ===")
|
print("=== Test: Cursors ===")
|
||||||
from backend.nodes.analysis import LineCursors
|
from backend.nodes.analysis import Cursors
|
||||||
|
|
||||||
node = LineCursors()
|
node = Cursors()
|
||||||
|
|
||||||
# Create a simple linear ramp
|
# Create a simple linear ramp
|
||||||
line = np.linspace(0, 10, 100).astype(np.float64)
|
line = np.linspace(0, 10, 100).astype(np.float64)
|
||||||
|
|
||||||
# Capture overlay
|
# Capture overlay
|
||||||
overlays = []
|
overlays = []
|
||||||
LineCursors._broadcast_overlay_fn = lambda nid, data: overlays.append(data)
|
Cursors._broadcast_overlay_fn = lambda nid, data: overlays.append(data)
|
||||||
LineCursors._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, = node.process(line, x1=0.25, y1=0.5, x2=0.75, y2=0.5)
|
||||||
|
|
||||||
# Should produce a 6-row table
|
# Should produce a 6-row table
|
||||||
assert len(table) == 6
|
assert len(table) == 6
|
||||||
quantities = {row["quantity"] for row in table}
|
quantities = {row["quantity"] for row in table}
|
||||||
assert "A position" in quantities
|
assert "A x" in quantities
|
||||||
assert "B position" in quantities
|
assert "B x" in quantities
|
||||||
assert "delta X" in quantities
|
assert "dx" in quantities
|
||||||
assert "delta Y" in quantities
|
assert "dy" in quantities
|
||||||
|
|
||||||
# B should be at a later position than A
|
# B should be at a later position than A
|
||||||
a_pos = next(r["value"] for r in table if r["quantity"] == "A position")
|
a_pos = next(r["value"] for r in table if r["quantity"] == "A x")
|
||||||
b_pos = next(r["value"] for r in table if r["quantity"] == "B position")
|
b_pos = next(r["value"] for r in table if r["quantity"] == "B x")
|
||||||
assert b_pos > a_pos
|
assert b_pos > a_pos
|
||||||
|
|
||||||
# Delta Y should reflect the height difference along the ramp
|
# Delta Y should reflect the height difference along the ramp
|
||||||
dy = next(r["value"] for r in table if r["quantity"] == "delta Y")
|
dy = next(r["value"] for r in table if r["quantity"] == "dy")
|
||||||
assert dy > 0 # ramp goes upward
|
assert dy > 0 # ramp goes upward
|
||||||
|
|
||||||
# Overlay should have been broadcast
|
# Overlay should have been broadcast
|
||||||
@@ -1661,7 +1661,28 @@ def test_line_cursors():
|
|||||||
table2, = node.process(line, x1=0.25, y1=0.5, x2=0.75, y2=0.5, x_axis=x_axis)
|
table2, = node.process(line, x1=0.25, y1=0.5, x2=0.75, y2=0.5, x_axis=x_axis)
|
||||||
assert len(table2) == 6
|
assert len(table2) == 6
|
||||||
|
|
||||||
LineCursors._broadcast_overlay_fn = None
|
# Field input should report dx/dy/dz and broadcast an image overlay
|
||||||
|
field = DataField(
|
||||||
|
data=np.arange(100, dtype=np.float64).reshape(10, 10),
|
||||||
|
xreal=2.0,
|
||||||
|
yreal=4.0,
|
||||||
|
si_unit_xy="um",
|
||||||
|
si_unit_z="nm",
|
||||||
|
)
|
||||||
|
overlays.clear()
|
||||||
|
table3, = node.process(field, x1=0.2, y1=0.25, x2=0.7, y2=0.75)
|
||||||
|
assert len(table3) == 9
|
||||||
|
field_rows = {row["quantity"]: row for row in table3}
|
||||||
|
assert field_rows["dx"]["unit"] == "um"
|
||||||
|
assert field_rows["dy"]["unit"] == "um"
|
||||||
|
assert field_rows["dz"]["unit"] == "nm"
|
||||||
|
assert np.isclose(field_rows["dx"]["value"], 1.0)
|
||||||
|
assert np.isclose(field_rows["dy"]["value"], 2.0)
|
||||||
|
assert len(overlays) == 1
|
||||||
|
assert overlays[0]["kind"] == "cursor_points"
|
||||||
|
assert overlays[0]["image"].startswith("data:image/png;base64,")
|
||||||
|
|
||||||
|
Cursors._broadcast_overlay_fn = None
|
||||||
print(" PASS\n")
|
print(" PASS\n")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user