remove linemath and tablemath

This commit is contained in:
2026-03-25 22:39:21 -07:00
parent 7f3dfa8fdf
commit 6de239caa1
11 changed files with 251 additions and 195 deletions

View File

@@ -219,10 +219,10 @@ class ExecutionEngine:
) -> None:
"""Wire up broadcast callbacks on display node classes."""
from backend.nodes.display import PreviewImage, PrintTable, View3D, ValueDisplay, Markup
from backend.nodes.analysis import CrossSection, LineCursors, TableMath, Stats, HeightHistogram
from backend.nodes.analysis import CrossSection, LineCursors, TableMath, Stats, Histogram
from backend.nodes.modify import CropResizeField, RotateField
from backend.nodes.mask import ThresholdMask, MaskMorphology, MaskInvert, MaskCombine, DrawMask
from backend.nodes.io import SaveImage, LoadFile, LoadDemo
from backend.nodes.io import SaveImage, Image, ImageDemo
PreviewImage._broadcast_fn = on_preview
ThresholdMask._broadcast_fn = on_preview
@@ -235,26 +235,26 @@ class ExecutionEngine:
ValueDisplay._broadcast_value_fn = on_value
TableMath._broadcast_value_fn = on_value
Stats._broadcast_value_fn = on_value
HeightHistogram._broadcast_overlay_fn = on_overlay
Histogram._broadcast_overlay_fn = on_overlay
CrossSection._broadcast_overlay_fn = on_overlay
LineCursors._broadcast_overlay_fn = on_overlay
CropResizeField._broadcast_overlay_fn = on_overlay
RotateField._broadcast_warning_fn = on_warning
Markup._broadcast_overlay_fn = on_overlay
LoadFile._broadcast_warning_fn = on_warning
LoadDemo._broadcast_warning_fn = on_warning
Image._broadcast_warning_fn = on_warning
ImageDemo._broadcast_warning_fn = on_warning
SaveImage._broadcast_warning_fn = on_warning
def _set_node_id_on_display(self, cls: type, node_id: str) -> None:
"""Inform display nodes of their current node_id for WS tagging."""
from backend.nodes.display import PreviewImage, PrintTable, View3D, ValueDisplay, Markup
from backend.nodes.analysis import CrossSection, LineCursors, TableMath, Stats, HeightHistogram
from backend.nodes.analysis import CrossSection, LineCursors, TableMath, Stats, Histogram
from backend.nodes.modify import CropResizeField, RotateField
from backend.nodes.mask import ThresholdMask, MaskMorphology, MaskInvert, MaskCombine, DrawMask
from backend.nodes.io import LoadFile, LoadDemo, SaveImage
if cls in (PreviewImage, PrintTable, View3D, ValueDisplay, TableMath, Stats, HeightHistogram, CrossSection, LineCursors, CropResizeField, RotateField, Markup,
from backend.nodes.io import Image, ImageDemo, SaveImage
if cls in (PreviewImage, PrintTable, View3D, ValueDisplay, TableMath, Stats, Histogram, CrossSection, LineCursors, CropResizeField, RotateField, Markup,
ThresholdMask, MaskMorphology, MaskInvert, MaskCombine, DrawMask,
LoadFile, LoadDemo, SaveImage):
Image, ImageDemo, SaveImage):
cls._current_node_id = node_id
def _auto_preview(
@@ -275,12 +275,12 @@ class ExecutionEngine:
from backend.data_types import (
DataField, image_to_uint8, encode_preview, render_datafield_preview,
)
from backend.nodes.io import LoadFile, LoadDemo
from backend.nodes.io import Image, ImageDemo
if getattr(cls, "_CUSTOM_PREVIEW", False):
return
if cls in (LoadFile, LoadDemo) and on_preview:
if cls in (Image, ImageDemo) and on_preview:
preview = self._render_load_node_preview(result, inputs or {})
if preview:
on_preview(node_id, preview)

98
backend/node_menu.py Normal file
View File

@@ -0,0 +1,98 @@
"""
Central Add Node menu manifest.
Edit MENU_LAYOUT to rearrange which nodes appear under each menu leaf and
their order within that leaf. Node classes not listed here fall back to their
class CATEGORY.
"""
from __future__ import annotations
from typing import Any
MENU_LAYOUT: dict[str, list[str]] = {
"Add": [
"Image",
"ImageDemo",
"Folder",
"ColorMap",
"Number",
"RangeSlider",
"Coordinate",
"Font",
],
"Output": [
"PreviewImage",
"SaveImage",
"View3D",
"PrintTable",
"ValueDisplay",
],
"Overlay": [
"Markup",
"Annotations",
],
"Modify": [
"ColormapAdjust",
"CropResizeField",
"RotateField",
],
"Filter": [
"GaussianFilter",
"MedianFilter",
"EdgeDetect",
"FFTFilter1D",
"FFTFilter2D",
],
"Frequency": [
"FFT2D",
"InverseFFT2D",
],
"Flatten": [
"PlaneLevelField",
"PolyLevelField",
"FixZero",
],
"Measure": [
"Statistics",
"Histogram",
"LineCursors",
"CrossSection",
"Stats",
],
"Mask": [
"DrawMask",
"ThresholdMask",
"MaskMorphology",
"MaskInvert",
"MaskCombine",
],
"Particles": [
"ParticleAnalysis",
],
}
_CATEGORY_ORDER = {category: index for index, category in enumerate(MENU_LAYOUT)}
_NODE_METADATA: dict[str, dict[str, Any]] = {}
for category, class_names in MENU_LAYOUT.items():
for node_order, class_name in enumerate(class_names):
_NODE_METADATA[class_name] = {
"category": category,
"category_order": _CATEGORY_ORDER[category],
"menu_order": node_order,
}
def get_menu_metadata(class_name: str, fallback_category: str = "uncategorized") -> dict[str, Any]:
metadata = _NODE_METADATA.get(class_name)
if metadata is not None:
return dict(metadata)
fallback_order = _CATEGORY_ORDER.get(fallback_category, len(_CATEGORY_ORDER))
return {
"category": fallback_category,
"category_order": fallback_order,
"menu_order": 10_000,
}

View File

@@ -9,6 +9,8 @@ the execution engine and the /nodes REST endpoint.
from __future__ import annotations
from typing import Any
from backend.node_menu import get_menu_metadata
NODE_CLASS_MAPPINGS: dict[str, type] = {}
NODE_DISPLAY_NAME_MAPPINGS: dict[str, str] = {}
@@ -37,11 +39,14 @@ def get_node_info(class_name: str) -> dict[str, Any]:
"""
cls = NODE_CLASS_MAPPINGS[class_name]
input_types: dict = cls.INPUT_TYPES()
menu_metadata = get_menu_metadata(class_name, getattr(cls, "CATEGORY", "uncategorized"))
return {
"name": class_name,
"display_name": NODE_DISPLAY_NAME_MAPPINGS.get(class_name, class_name),
"category": getattr(cls, "CATEGORY", "uncategorized"),
"category": menu_metadata["category"],
"category_order": menu_metadata["category_order"],
"menu_order": menu_metadata["menu_order"],
"input": input_types,
"input_order": {k: list(v.keys()) for k, v in input_types.items()},
"output": list(cls.RETURN_TYPES),

View File

@@ -2,8 +2,8 @@
Analysis nodes — statistics, histograms, FFT, cross sections.
Gwyddion equivalents:
StatisticsNode → gwy_data_field_get_min/max/avg/rms (libprocess/stats.h)
HeightHistogram → DH (height distribution), gwy_data_field_dh
Statistics → gwy_data_field_get_min/max/avg/rms (libprocess/stats.h)
Histogram → DH (height distribution), gwy_data_field_dh
FFT2D → gwy_data_field_2dfft + gwy_data_field_2dpsdf
CrossSection → gwy_data_field_get_profile (libprocess/datafield.c)
"""
@@ -16,11 +16,11 @@ from backend.data_types import DataField, MeasureTable, RecordTable, datafield_t
# ---------------------------------------------------------------------------
# StatisticsNode
# Statistics
# ---------------------------------------------------------------------------
@register_node(display_name="Statistics")
class StatisticsNode:
class Statistics:
@classmethod
def INPUT_TYPES(cls):
return {
@@ -59,11 +59,11 @@ class StatisticsNode:
# ---------------------------------------------------------------------------
# HeightHistogram
# Histogram
# ---------------------------------------------------------------------------
@register_node(display_name="Height Histogram")
class HeightHistogram:
class Histogram:
@classmethod
def INPUT_TYPES(cls):
return {
@@ -130,9 +130,9 @@ class HeightHistogram:
yb = float(counts[idx_b]) if len(counts) else 0.0
count_unit = "count" if y_scale == "linear" else "log10(1+count)"
if HeightHistogram._broadcast_overlay_fn is not None:
HeightHistogram._broadcast_overlay_fn(
HeightHistogram._current_node_id,
if Histogram._broadcast_overlay_fn is not None:
Histogram._broadcast_overlay_fn(
Histogram._current_node_id,
{
"kind": "line_plot",
"section_title": "Histogram",
@@ -754,36 +754,6 @@ def _op_da(z):
return float(np.mean(np.abs(np.diff(z))))
@register_node(display_name="Line Math")
class LineMath:
"""Compute a single scalar value from a LINE profile."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"line": ("LINE",),
"operation": (list(LINE_OPS.keys()),),
}
}
RETURN_TYPES = ("MEASURE_TABLE",)
RETURN_NAMES = ("result",)
FUNCTION = "process"
CATEGORY = "analysis"
DESCRIPTION = (
"Compute a single scalar measurement from a LINE profile. "
"Includes basic stats and Gwyddion-convention roughness parameters."
)
def process(self, line, operation: str) -> tuple:
z = np.asarray(line, dtype=np.float64).ravel()
fn, unit = LINE_OPS[operation]
value = fn(z)
table = MeasureTable([{"quantity": operation, "value": value, "unit": unit}])
return (table,)
# ---------------------------------------------------------------------------
# TableMath — scalar measurement from a numeric record-table column
# ---------------------------------------------------------------------------
@@ -869,56 +839,6 @@ def _scalar_payload(value: float, unit: str = "") -> dict:
return payload
@register_node(display_name="Table Math")
class TableMath:
"""Compute a scalar reduction over one numeric column in a record table."""
_broadcast_value_fn = None
_current_node_id: str = ""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"table": ("RECORD_TABLE",),
"column": ("STRING", {
"default": "value",
"choices_from_table_input": "table",
}),
"operation": (list(TABLE_OPS.keys()),),
}
}
RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ("value",)
FUNCTION = "process"
CATEGORY = "analysis"
DESCRIPTION = (
"Compute a scalar reduction over one numeric record-table column. "
"Useful for max, min, avg, median, sum, range, std, variance, and count."
)
def process(self, table: list, column: str, operation: str) -> tuple:
if isinstance(table, MeasureTable):
raise ValueError("Table Math only accepts record tables, not measurement tables.")
if not isinstance(table, list) or not table:
raise ValueError("Table Math requires a non-empty record table input.")
column_name = resolve_table_column_name(table, column)
values = extract_numeric_table_values(table, column_name)
if not values:
raise ValueError(f"Column '{column_name}' has no numeric values.")
op = TABLE_OPS.get(operation)
if op is None:
raise ValueError(f"Unsupported table operation: {operation}")
result = op(np.asarray(values, dtype=np.float64))
if TableMath._broadcast_value_fn is not None:
TableMath._broadcast_value_fn(TableMath._current_node_id, result)
return (result,)
def extract_numeric_table_values(table: list, column: str) -> list[float]:
values = []
for row in table:

View File

@@ -125,11 +125,11 @@ def list_folder_paths(folderpath: str) -> list[dict]:
# ---------------------------------------------------------------------------
# LoadFile (unified loader — replaces LoadImage + LoadSPM)
# Image (unified loader — replaces LoadImage + LoadSPM)
# ---------------------------------------------------------------------------
@register_node(display_name="Load File")
class LoadFile:
@register_node(display_name="Image")
class Image:
@classmethod
def INPUT_TYPES(cls):
return {
@@ -185,8 +185,8 @@ class LoadFile:
return (field,)
def _send_warning(self, message: str):
fn = LoadFile._broadcast_warning_fn
nid = LoadFile._current_node_id
fn = Image._broadcast_warning_fn
nid = Image._current_node_id
if fn and nid:
fn(nid, message)
@@ -353,7 +353,7 @@ class LoadFile:
# ---------------------------------------------------------------------------
# LoadDemo
# ImageDemo
# ---------------------------------------------------------------------------
def _list_demo_files() -> list[str]:
@@ -366,8 +366,8 @@ def _list_demo_files() -> list[str]:
)
@register_node(display_name="Load Demo File")
class LoadDemo:
@register_node(display_name="Image (Demo)")
class ImageDemo:
@classmethod
def INPUT_TYPES(cls):
choices = _list_demo_files() or ["(no demo files found)"]
@@ -388,7 +388,7 @@ class LoadDemo:
DESCRIPTION = "Load a bundled demo file so you can try the app without providing your own data."
def load(self, name: str = "", colormap: str = "viridis", colormap_map=None):
loader = LoadFile()
loader = Image()
demo_path = DEMO_DIR / name
if not demo_path.exists():
raise FileNotFoundError(f"Demo file not found: {name}")