fix value display node

This commit is contained in:
2026-03-29 12:13:08 -07:00
parent 80b74dfdfd
commit e3c381ee07
9 changed files with 207 additions and 31 deletions

View File

@@ -17,7 +17,6 @@ MENU_LAYOUT: dict[str, list[str]] = {
"Note",
"ImageDemo",
"Folder",
"Number",
"RangeSlider",
"Coordinate",
"CoordinatePair",
@@ -27,7 +26,7 @@ MENU_LAYOUT: dict[str, list[str]] = {
"Font",
"ColormapAdjust",
"PreviewImage",
"ValueDisplay",
"ValueIO",
"View3D",
"Save",
"SaveImage",

View File

@@ -43,10 +43,9 @@ from backend.nodes import (
markup,
preview_image,
statistics,
value_io,
view_3d,
print_table,
value_display,
# Analysis
curvature,
fractal_dimension,
histogram,

View File

@@ -154,13 +154,12 @@ def _compute_curvature_results(
+ coeffs[5] * y_norm * y_norm
)
r1 = float("inf") if abs(kappa1) <= 1e-14 else float(1.0 / (q * q * kappa1))
r2 = float("inf") if abs(kappa2) <= 1e-14 else float(1.0 / (q * q * kappa2))
#todo: fix inf case
r1 = float(np.inf) if abs(kappa1) <= 1e-14 else float(1.0 / (q * q * kappa1))
r2 = float(np.inf) if abs(kappa2) <= 1e-14 else float(1.0 / (q * q * kappa2))
x0 = float(xc / q + 0.5 * xreal + field.xoff)
y0 = float(yc / q + 0.5 * yreal + field.yoff)
print(f"debug: {x0}, {y0}, {r1}, {r2}")
return {
"degree": float(degree),
"x0": x0,
@@ -292,8 +291,8 @@ class Curvature:
OUTPUTS = (
('ANNOTATION_SOURCE', 'output'),
('RECORD_TABLE', 'measurements'),
('LINE', 'profile_x'),
('LINE', 'profile_y'),
('LINE', 'profile_a'),
('LINE', 'profile_b'),
)
FUNCTION = "process"
@@ -340,7 +339,7 @@ class Curvature:
profiles = []
for pair in intersections[:2]:
profiles.append(_profile_from_intersections(field, pair[1], pair[0]))
profiles.append(_profile_from_intersections(field, pair[0], pair[1]))
while len(profiles) < 2:
profiles.append(_empty_profile(field.si_unit_xy, field.si_unit_z))
@@ -360,7 +359,7 @@ class Curvature:
preview_base = render_datafield_preview(field, field.colormap)
panels = []
for p, title in zip(profiles, ["X Principal Axis", "Y Principal Axis"]):
for p, title in zip(profiles, ["Principal Axis A", "Principal Axis B"]):
if len(p.data) > 0:
panels.append({
"title": title,
@@ -376,7 +375,7 @@ class Curvature:
})
emit_preview({"kind": "panels", "panels": panels})
# emit_table(table)
emit_table(table)
if warnings:
emit_warning(warnings[0])

View File

@@ -4,6 +4,7 @@ Shared helper functions for argonode nodes.
from __future__ import annotations
import json
import re
from functools import lru_cache
from pathlib import Path
from typing import Callable
@@ -16,6 +17,62 @@ from backend.runtime_paths import demo_dir, input_dir, output_dir
# Scalar payload helpers (from display.py)
# ---------------------------------------------------------------------------
_SI_PREFIXES: dict[str, float] = {
'Y': 1e24, 'Z': 1e21, 'E': 1e18, 'P': 1e15, 'T': 1e12,
'G': 1e9, 'M': 1e6, 'k': 1e3,
'm': 1e-3, 'u': 1e-6, 'µ': 1e-6, 'n': 1e-9, 'p': 1e-12,
'f': 1e-15, 'a': 1e-18, 'z': 1e-21, 'y': 1e-24,
}
_PREFIXABLE_UNITS: frozenset[str] = frozenset({
'm', 's', 'A', 'V', 'W', 'Hz', 'F', 'C', 'J', 'N', 'Pa',
'T', 'H', 'S', 'g', 'K', 'Ohm', 'ohm', 'Ω',
})
_NUMBER_RE = re.compile(
r'^([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)\s*(.*)?$'
)
def parse_number_with_unit(text: str) -> tuple[float, str]:
"""Parse a string like '1.5 nm' into (1.5e-9, 'm').
The numeric part may use scientific notation. The unit is stripped of any
recognised SI prefix and the raw value is scaled accordingly, so the
returned float is always in the base SI unit. Units that are not
prefixable are returned unchanged alongside the unscaled value.
Examples::
parse_number_with_unit("1 um") → (1e-6, "m")
parse_number_with_unit("500 nm") → (5e-7, "m")
parse_number_with_unit("3.14") → (3.14, "")
parse_number_with_unit("2 kHz") → (2000.0, "Hz")
"""
text = text.strip()
if not text:
return 0.0, ""
m = _NUMBER_RE.match(text)
if not m:
raise ValueError(f"Cannot parse number: {text!r}")
numeric = float(m.group(1))
unit_str = (m.group(2) or "").strip()
if not unit_str:
return numeric, ""
# Try prefix + base-unit split (handle multi-byte µ as a prefix)
if len(unit_str) >= 2:
prefix_char = unit_str[0]
rest = unit_str[1:]
if prefix_char in _SI_PREFIXES and rest in _PREFIXABLE_UNITS:
return numeric * _SI_PREFIXES[prefix_char], rest
return numeric, unit_str
def _scalar_payload(value: float, unit: str = "") -> dict:
payload = {"value": float(value)}
if isinstance(unit, str) and unit.strip():
@@ -24,7 +81,7 @@ def _scalar_payload(value: float, unit: str = "") -> dict:
# ---------------------------------------------------------------------------
# Measurement helpers (from display.py — used by ValueDisplay)
# Measurement helpers (from display.py — used by ValueIO)
# ---------------------------------------------------------------------------
def _measurement_names(table: list) -> list[str]:

View File

@@ -2,18 +2,21 @@ from __future__ import annotations
from backend.node_registry import register_node
from backend.execution_context import emit_table, emit_value
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, parse_number_with_unit
@register_node(display_name="Value Display")
class ValueDisplay:
class ValueIO:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("FLOAT", {
"accepted_types": ["RECORD_TABLE"],
"socket_only": True,
"number_input": ("STRING", {
"text_input": True,
"default": "0",
"placeholder": "e.g. 1.5 nm",
"hide_when_input_connected": "value",
"hide_label": True,
}),
"measurement": ("STRING", {
"default": "",
@@ -22,7 +25,13 @@ class ValueDisplay:
"value": ["RECORD_TABLE"],
},
}),
}
},
"optional": {
"value": ("FLOAT", {
"accepted_types": ["RECORD_TABLE"],
"socket_only": True,
}),
},
}
OUTPUTS = (
@@ -35,14 +44,16 @@ class ValueDisplay:
_broadcast_value_fn = None
_current_node_id: str = ""
def display_value(self, value, measurement: str = "") -> tuple:
def display_value(self, number_input: str = "0", value=None, measurement: str = "") -> tuple:
unit = ""
if isinstance(value, RecordTable):
emit_table(value)
row = _measurement_entry(value, measurement)
numeric = _measurement_value(value, measurement)
unit = row.get("unit", "") if isinstance(row.get("unit"), str) else ""
else:
elif value is not None:
numeric = float(value)
else:
numeric, unit = parse_number_with_unit(str(number_input))
emit_value(_scalar_payload(numeric, unit))
return (numeric,)

View File

@@ -31,6 +31,7 @@ from __future__ import annotations
import asyncio
import json
import logging
import math
import sys
from collections import defaultdict
from copy import deepcopy
@@ -205,6 +206,10 @@ def create_app(
value = payload
unit = ""
# JSON cannot encode non-finite floats; convert to string representations.
if isinstance(value, float) and not math.isfinite(value):
value = "" if value > 0 else ("-∞" if math.isinf(value) else "NaN")
data = {"node_id": node_id, "value": value}
if isinstance(unit, str) and unit.strip():
data["unit"] = unit.strip()

View File

@@ -15,7 +15,7 @@ import {
} from './constants';
import { getGroupMinimumSize } from './groupSizing.js';
import { buildCombinedInputNameByWidgetName, formatUiLabel } from './nodeWidgetLayout.js';
import { applySIPrefix, formatNumericCell, formatTableRowCell, getTableColumns } from './valueFormatting.js';
import { applySIPrefix, formatNumericCell, formatTableRowCell, getTableColumns, parseNumberWithUnit } from './valueFormatting.js';
// ── Context (provided by App) ─────────────────────────────────────────
@@ -443,7 +443,11 @@ function getScalarPayload(scalarValue) {
return Number.isFinite(scalarValue) ? { value: scalarValue, unit: '' } : null;
}
if (!scalarValue || typeof scalarValue !== 'object') return null;
const numeric = Number(scalarValue.value);
const raw = scalarValue.value;
if (typeof raw === 'string') {
return { valueText: raw, unitText: typeof scalarValue.unit === 'string' ? scalarValue.unit : '' };
}
const numeric = Number(raw);
if (!Number.isFinite(numeric)) return null;
return {
value: numeric,
@@ -454,6 +458,7 @@ function getScalarPayload(scalarValue) {
function formatScalarDisplay(scalarValue) {
const payload = getScalarPayload(scalarValue);
if (!payload) return null;
if ('valueText' in payload) return payload;
if (payload.unit) {
const prefixed = applySIPrefix(payload.value, payload.unit);
@@ -1471,6 +1476,55 @@ function CustomNode({ id, data }) {
);
}
// ── Editable value-box for text_input FLOAT widgets ──────────────────
function TextInputValueBox({ val, placeholder, nodeId, name, label, hideLabel, onChange }) {
const [editing, setEditing] = useState(false);
const parsed = parseNumberWithUnit(val);
const display = parsed ? formatScalarDisplay({ value: parsed.numeric, unit: parsed.unit }) : null;
return (
<>
{!hideLabel && <label>{label}</label>}
<div
className="node-value-box nodrag"
style={{ cursor: editing ? 'text' : 'pointer' }}
onClick={() => !editing && setEditing(true)}
>
{editing ? (
<input
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
className="nodrag"
type="text"
value={val}
placeholder={placeholder}
onChange={(e) => onChange(nodeId, name, e.target.value)}
onBlur={() => setEditing(false)}
style={{
background: 'transparent',
border: 'none',
outline: 'none',
color: 'inherit',
font: 'inherit',
textAlign: 'center',
width: '100%',
padding: 0,
}}
/>
) : display ? (
<>
<span className="node-value-box-number">{display.valueText}</span>
{display.unitText && <span className="node-value-box-unit">{display.unitText}</span>}
</>
) : (
<span className="node-value-box-number" style={{ opacity: 0.4 }}>{placeholder || '0'}</span>
)}
</div>
</>
);
}
// ── Widget renderer ───────────────────────────────────────────────────
function WidgetControl({ widget, nodeId, value, widgetValues, onChange, openFileBrowser, hideLabel = false, measurementChoices }) {
@@ -1701,6 +1755,20 @@ function WidgetControl({ widget, nodeId, value, widgetValues, onChange, openFile
);
}
if (opts?.text_input) {
return (
<TextInputValueBox
val={val}
placeholder={placeholder || opts?.placeholder || ''}
nodeId={nodeId}
name={name}
label={label}
hideLabel={hideLabel || !!opts.hide_label}
onChange={onChange}
/>
);
}
if (type === 'FLOAT') {
if (opts?.slider) {
const rawMin = opts?.min_widget ? widgetValues?.[opts.min_widget] : opts?.min;

View File

@@ -1,3 +1,41 @@
const SI_PREFIX_MULTIPLIERS = {
Y: 1e24, Z: 1e21, E: 1e18, P: 1e15, T: 1e12,
G: 1e9, M: 1e6, k: 1e3,
m: 1e-3, u: 1e-6, µ: 1e-6, n: 1e-9, p: 1e-12,
f: 1e-15, a: 1e-18, z: 1e-21, y: 1e-24,
};
const NUMBER_WITH_UNIT_RE = /^([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)\s*(.*)?$/;
/**
* Parse a string like "1.5 nm" into { numeric: 1.5e-9, unit: "m" }.
* Returns null if the string does not start with a valid number.
* The numeric value is scaled to the base SI unit via the prefix.
*/
export function parseNumberWithUnit(text) {
const s = String(text ?? '').trim();
if (!s) return { numeric: 0, unit: '' };
const m = s.match(NUMBER_WITH_UNIT_RE);
if (!m) return null;
const numeric = parseFloat(m[1]);
const unitStr = (m[2] ?? '').trim();
if (!unitStr) return { numeric, unit: '' };
if (unitStr.length >= 2) {
const prefix = unitStr[0];
const rest = unitStr.slice(1);
const multiplier = SI_PREFIX_MULTIPLIERS[prefix];
if (multiplier !== undefined && PREFIXABLE_UNITS.has(rest)) {
return { numeric: numeric * multiplier, unit: rest };
}
}
return { numeric, unit: unitStr };
}
const SI_PREFIXES = [
{ exp: -24, prefix: 'y' },
{ exp: -21, prefix: 'z' },

View File

@@ -2017,17 +2017,17 @@ def test_print_table():
def test_value_display():
print("=== Test: ValueDisplay ===")
from backend.nodes.value_display import ValueDisplay
print("=== Test: ValueIO ===")
from backend.nodes.value_io import ValueIO
node = ValueDisplay()
value_spec = ValueDisplay.INPUT_TYPES()["required"]["value"]
node = ValueIO()
value_spec = ValueIO.INPUT_TYPES()["required"]["value"]
assert value_spec[0] == "FLOAT"
assert value_spec[1]["accepted_types"] == ["RECORD_TABLE"]
captured = []
ValueDisplay._broadcast_value_fn = lambda node_id, payload: captured.append((node_id, payload))
ValueDisplay._current_node_id = "test"
ValueIO._broadcast_value_fn = lambda node_id, payload: captured.append((node_id, payload))
ValueIO._current_node_id = "test"
result = node.display_value(3.25)
assert result == (3.25,)
@@ -2041,7 +2041,7 @@ def test_value_display():
assert result == (1.7e-7,)
assert captured[-1] == ("test", {"value": 1.7e-7, "unit": "m"})
ValueDisplay._broadcast_value_fn = None
ValueIO._broadcast_value_fn = None
print(" PASS\n")