work on curvature node

This commit is contained in:
2026-03-28 22:34:10 -07:00
parent 6e113725d5
commit 80b74dfdfd
5 changed files with 86 additions and 46 deletions

View File

@@ -3,7 +3,7 @@ from backend.nodes import (
# IO # IO
colormap, colormap,
crop_resize, crop_resize,
fft_2d_invert, fft_2d_inverse,
filter_fft_1d, filter_fft_1d,
filter_fft_2d, filter_fft_2d,
filter_gaussian, filter_gaussian,

View File

@@ -159,6 +159,8 @@ def _compute_curvature_results(
x0 = float(xc / q + 0.5 * xreal + field.xoff) x0 = float(xc / q + 0.5 * xreal + field.xoff)
y0 = float(yc / q + 0.5 * yreal + field.yoff) y0 = float(yc / q + 0.5 * yreal + field.yoff)
print(f"debug: {x0}, {y0}, {r1}, {r2}")
return { return {
"degree": float(degree), "degree": float(degree),
"x0": x0, "x0": x0,
@@ -290,8 +292,8 @@ class Curvature:
OUTPUTS = ( OUTPUTS = (
('ANNOTATION_SOURCE', 'output'), ('ANNOTATION_SOURCE', 'output'),
('RECORD_TABLE', 'measurements'), ('RECORD_TABLE', 'measurements'),
('LINE', 'profile_1'), ('LINE', 'profile_x'),
('LINE', 'profile_2'), ('LINE', 'profile_y'),
) )
FUNCTION = "process" FUNCTION = "process"
@@ -338,7 +340,7 @@ class Curvature:
profiles = [] profiles = []
for pair in intersections[:2]: for pair in intersections[:2]:
profiles.append(_profile_from_intersections(field, pair[0], pair[1])) profiles.append(_profile_from_intersections(field, pair[1], pair[0]))
while len(profiles) < 2: while len(profiles) < 2:
profiles.append(_empty_profile(field.si_unit_xy, field.si_unit_z)) profiles.append(_empty_profile(field.si_unit_xy, field.si_unit_z))
@@ -346,18 +348,36 @@ class Curvature:
output = field.replace(overlays=[*field.overlays, markup_spec]) output = field.replace(overlays=[*field.overlays, markup_spec])
table = RecordTable([ table = RecordTable([
{"quantity": "Center x position", "value": float(results["x0"]), "unit": field.si_unit_xy}, {"quantity": "Curvature radius 1", "value": results["r1"], "unit": field.si_unit_xy},
{"quantity": "Center y position", "value": float(results["y0"]), "unit": field.si_unit_xy}, {"quantity": "Curvature radius 2", "value": results["r2"], "unit": field.si_unit_xy},
{"quantity": "Center value", "value": float(results["z0"]), "unit": field.si_unit_z}, {"quantity": "Center x position", "value": results["x0"], "unit": field.si_unit_xy},
{"quantity": "Curvature radius 1", "value": float(results["r1"]), "unit": field.si_unit_xy}, {"quantity": "Center y position", "value": results["y0"], "unit": field.si_unit_xy},
{"quantity": "Curvature radius 2", "value": float(results["r2"]), "unit": field.si_unit_xy}, {"quantity": "Center value", "value": results["z0"], "unit": field.si_unit_z},
{"quantity": "Direction 1", "value": float(np.degrees(results["phi1"])), "unit": "deg"}, {"quantity": "Direction 1", "value": results["phi1"], "unit": "deg"},
{"quantity": "Direction 2", "value": float(np.degrees(results["phi2"])), "unit": "deg"}, {"quantity": "Direction 2", "value": results["phi2"], "unit": "deg"},
]) ])
preview_base = render_datafield_preview(field, field.colormap) preview_base = render_datafield_preview(field, field.colormap)
emit_preview(encode_preview(_apply_markup_overlay(preview_base, field, markup_spec))) panels = []
emit_table(table)
for p, title in zip(profiles, ["X Principal Axis", "Y Principal Axis"]):
if len(p.data) > 0:
panels.append({
"title": title,
"kind": "line_plot",
"line": p.data.tolist(),
"x_axis": p.x_axis.tolist(),
"x_unit": field.si_unit_xy,
})
panels.append({
"title": "Overview",
"kind": "image",
"image": encode_preview(_apply_markup_overlay(preview_base, field, markup_spec)),
})
emit_preview({"kind": "panels", "panels": panels})
# emit_table(table)
if warnings: if warnings:
emit_warning(warnings[0]) emit_warning(warnings[0])

View File

@@ -90,8 +90,8 @@ class Histogram:
}) })
table = RecordTable([ table = RecordTable([
{"quantity": "delta Y", "value": yb - ya, "unit": count_unit},
{"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": "A position", "value": xa, "unit": field.si_unit_z}, {"quantity": "A position", "value": xa, "unit": field.si_unit_z},
{"quantity": "A count", "value": ya, "unit": count_unit}, {"quantity": "A count", "value": ya, "unit": count_unit},
{"quantity": "B position", "value": xb, "unit": field.si_unit_z}, {"quantity": "B position", "value": xb, "unit": field.si_unit_z},

View File

@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from backend.node_registry import register_node from backend.node_registry import register_node
from backend.execution_context import emit_value from backend.execution_context import emit_table, emit_value
from backend.data_types import RecordTable from backend.data_types import RecordTable
from backend.nodes.helpers import _measurement_entry, _measurement_value, _scalar_payload from backend.nodes.helpers import _measurement_entry, _measurement_value, _scalar_payload
@@ -13,6 +13,7 @@ class ValueDisplay:
"required": { "required": {
"value": ("FLOAT", { "value": ("FLOAT", {
"accepted_types": ["RECORD_TABLE"], "accepted_types": ["RECORD_TABLE"],
"socket_only": True,
}), }),
"measurement": ("STRING", { "measurement": ("STRING", {
"default": "", "default": "",
@@ -37,6 +38,7 @@ class ValueDisplay:
def display_value(self, value, measurement: str = "") -> tuple: def display_value(self, value, measurement: str = "") -> tuple:
unit = "" unit = ""
if isinstance(value, RecordTable): if isinstance(value, RecordTable):
emit_table(value)
row = _measurement_entry(value, measurement) row = _measurement_entry(value, measurement)
numeric = _measurement_value(value, measurement) numeric = _measurement_value(value, measurement)
unit = row.get("unit", "") if isinstance(row.get("unit"), str) else "" unit = row.get("unit", "") if isinstance(row.get("unit"), str) else ""

View File

@@ -1056,6 +1056,9 @@ function CustomNode({ id, data }) {
const combinedInputNames = new Set(combinedInputNameByWidgetName.values()); const combinedInputNames = new Set(combinedInputNameByWidgetName.values());
const renderedDataInputs = dataInputs.filter((input) => !combinedInputNames.has(input.name)); const renderedDataInputs = dataInputs.filter((input) => !combinedInputNames.has(input.name));
// Computed directly from React props so it updates reliably when tableRows changes.
const nodeTableMeasurementChoices = getMeasurementChoices(data.tableRows || []);
const inlineWidgetsByInput = new Map(); const inlineWidgetsByInput = new Map();
const topWidgets = []; const topWidgets = [];
const standaloneWidgets = []; const standaloneWidgets = [];
@@ -1153,6 +1156,7 @@ function CustomNode({ id, data }) {
widgetValues={data.widgetValues} widgetValues={data.widgetValues}
onChange={ctx.onWidgetChange} onChange={ctx.onWidgetChange}
openFileBrowser={ctx.openFileBrowser} openFileBrowser={ctx.openFileBrowser}
measurementChoices={nodeTableMeasurementChoices}
/> />
)} )}
</div> </div>
@@ -1219,7 +1223,6 @@ function CustomNode({ id, data }) {
{scalarDisplay && ( {scalarDisplay && (
<div className="node-value-display"> <div className="node-value-display">
<div className="node-value-label">Value</div>
<div className="node-value-box"> <div className="node-value-box">
<span className="node-value-box-number">{scalarDisplay.valueText}</span> <span className="node-value-box-number">{scalarDisplay.valueText}</span>
{scalarDisplay.unitText && ( {scalarDisplay.unitText && (
@@ -1257,6 +1260,7 @@ function CustomNode({ id, data }) {
widgetValues={data.widgetValues} widgetValues={data.widgetValues}
onChange={ctx.onWidgetChange} onChange={ctx.onWidgetChange}
openFileBrowser={ctx.openFileBrowser} openFileBrowser={ctx.openFileBrowser}
measurementChoices={nodeTableMeasurementChoices}
/> />
)} )}
</div> </div>
@@ -1307,29 +1311,46 @@ function CustomNode({ id, data }) {
)} )}
{/* Collapsible preview image */} {/* Collapsible preview image */}
{data.previewImage {data.previewImage && !hidePreviewForInteractiveMask && (
&& !hidePreviewForInteractiveMask typeof data.previewImage === 'object' && data.previewImage.kind === 'panels'
&& !(hasInteractiveLineOverlay && typeof data.previewImage === 'object' && data.previewImage.kind === 'line_plot') && ( ? data.previewImage.panels.map((panel, pi) => (
<CollapsibleSection title="Preview" defaultOpen={true}> <CollapsibleSection key={pi} title={panel.title || 'Preview'} defaultOpen={true}>
<PreviewBoundary <PreviewBoundary
resetKey={typeof data.previewImage === 'string' ? data.previewImage : JSON.stringify({ resetKey={JSON.stringify({ kind: panel.kind, title: panel.title, len: panel.line?.length })}
kind: data.previewImage.kind, fallbackImage={panel.fallback_image ?? null}
len: data.previewImage.line?.length, >
layers: data.previewImage.layers?.length, {panel.kind === 'line_plot' ? (
})} <LinePlotOverlay overlay={panel} interactive={false} />
fallbackImage={typeof data.previewImage === 'object' ? data.previewImage.fallback_image : null} ) : panel.kind === 'image' ? (
> <div className="node-preview">
{typeof data.previewImage === 'string' ? ( <img src={panel.image} alt={panel.title || 'preview'} draggable={false} />
<div className="node-preview"> </div>
<img src={data.previewImage} alt="preview" draggable={false} /> ) : null}
</div> </PreviewBoundary>
) : data.previewImage.kind === 'layer_gallery' ? ( </CollapsibleSection>
<LayerGalleryPreview overlay={data.previewImage} /> ))
) : data.previewImage.kind === 'line_plot' ? ( : !(hasInteractiveLineOverlay && typeof data.previewImage === 'object' && data.previewImage.kind === 'line_plot') && (
<LinePlotOverlay overlay={data.previewImage} interactive={false} /> <CollapsibleSection title="Preview" defaultOpen={true}>
) : null} <PreviewBoundary
</PreviewBoundary> resetKey={typeof data.previewImage === 'string' ? data.previewImage : JSON.stringify({
</CollapsibleSection> kind: data.previewImage.kind,
len: data.previewImage.line?.length,
layers: data.previewImage.layers?.length,
})}
fallbackImage={typeof data.previewImage === 'object' ? data.previewImage.fallback_image : null}
>
{typeof data.previewImage === 'string' ? (
<div className="node-preview">
<img src={data.previewImage} alt="preview" draggable={false} />
</div>
) : data.previewImage.kind === 'layer_gallery' ? (
<LayerGalleryPreview overlay={data.previewImage} />
) : data.previewImage.kind === 'line_plot' ? (
<LinePlotOverlay overlay={data.previewImage} interactive={false} />
) : null}
</PreviewBoundary>
</CollapsibleSection>
)
)} )}
{/* Interactive cross-section overlay */} {/* Interactive cross-section overlay */}
@@ -1452,7 +1473,7 @@ function CustomNode({ id, data }) {
// ── Widget renderer ─────────────────────────────────────────────────── // ── Widget renderer ───────────────────────────────────────────────────
function WidgetControl({ widget, nodeId, value, widgetValues, onChange, openFileBrowser, hideLabel = false }) { function WidgetControl({ widget, nodeId, value, widgetValues, onChange, openFileBrowser, hideLabel = false, measurementChoices }) {
const { name, type, opts } = widget; const { name, type, opts } = widget;
const label = formatUiLabel(opts?.label || name); const label = formatUiLabel(opts?.label || name);
const val = value ?? opts?.default ?? ''; const val = value ?? opts?.default ?? '';
@@ -1484,12 +1505,9 @@ function WidgetControl({ widget, nodeId, value, widgetValues, onChange, openFile
const dynamicMeasurementChoices = useStore( const dynamicMeasurementChoices = useStore(
useCallback( useCallback(
(s) => { (s) => {
const measurementInputName = opts?.choices_from_measure_input; if (!opts?.choices_from_measure_input) return [];
if (!measurementInputName) return []; const node = s.nodeLookup?.get(nodeId) || s.nodes?.find((n) => n.id === nodeId);
const sourceType = getSourceTypeForInput(s, nodeId, measurementInputName); const rows = node?.data?.tableRows;
if (sourceType !== 'RECORD_TABLE') return [];
const sourceNode = getSourceNodeForInput(s, nodeId, measurementInputName);
const rows = sourceNode?.data?.tableRows;
return Array.isArray(rows) ? getMeasurementChoices(rows) : []; return Array.isArray(rows) ? getMeasurementChoices(rows) : [];
}, },
[nodeId, opts?.choices_from_measure_input], [nodeId, opts?.choices_from_measure_input],