rework menu system

This commit is contained in:
2026-03-28 00:28:43 -07:00
parent 4baadd4c3e
commit 4368aeb4a0
5 changed files with 99 additions and 41 deletions

View File

@@ -12,18 +12,19 @@ from typing import Any
MENU_LAYOUT: dict[str, list[str]] = { MENU_LAYOUT: dict[str, list[str]] = {
"Add": [ "Input": [
"Image", "Image",
"ImageDemo", "ImageDemo",
"Folder", "Folder",
"ColorMap",
"Number", "Number",
"RangeSlider", "RangeSlider",
"Coordinate", "Coordinate",
"CoordinatePair", "CoordinatePair",
"Font",
], ],
"Output": [ "Display": [
"ColorMap",
"Font",
"ColormapAdjust",
"PreviewImage", "PreviewImage",
"ValueDisplay", "ValueDisplay",
"View3D", "View3D",
@@ -36,11 +37,10 @@ MENU_LAYOUT: dict[str, list[str]] = {
"Annotations", "Annotations",
"AngleMeasure", "AngleMeasure",
], ],
"Modify": [ "Geometry": [
"CropResizeField", "CropResizeField",
"RotateField", "RotateField",
"FlipField", "FlipField",
"ColormapAdjust",
], ],
"Filter": [ "Filter": [
"GaussianFilter", "GaussianFilter",
@@ -50,25 +50,31 @@ MENU_LAYOUT: dict[str, list[str]] = {
"FFTFilter2D", "FFTFilter2D",
"ScarRemoval", "ScarRemoval",
], ],
"Frequency": [ "Spectral": [
"PSDF",
"FFT2D", "FFT2D",
"InverseFFT2D", "InverseFFT2D",
"FFTFilter1D",
"FFTFilter2D",
"ACF",
"PSDF",
], ],
"Flatten": [ "Level & Correct": [
"FixZero", "FixZero",
"LineCorrection",
"PlaneLevelField", "PlaneLevelField",
"PolyLevelField", "PolyLevelField",
"FacetLevelField", "FacetLevelField",
"LineCorrection",
"ScarRemoval",
], ],
"Measure": [ "Measure": [
"AngleMeasure",
"CrossSection", "CrossSection",
"Histogram", "Histogram",
"Cursors", "Cursors",
"Curvature", "Curvature",
"FractalDimension", "FractalDimension",
"ACF", "ACF",
"PSDF",
"Statistics", "Statistics",
"Stats", "Stats",
], ],
@@ -78,12 +84,14 @@ MENU_LAYOUT: dict[str, list[str]] = {
"MaskMorphology", "MaskMorphology",
"MaskInvert", "MaskInvert",
"MaskOperations", "MaskOperations",
],
"Grains": [
"GrainAnalysis",
"GrainDistanceTransform", "GrainDistanceTransform",
"WatershedSegmentation", "WatershedSegmentation",
], ],
"Grains": [
"GrainDistanceTransform",
"WatershedSegmentation",
"GrainAnalysis",
],
} }
@@ -91,11 +99,17 @@ _CATEGORY_ORDER = {category: index for index, category in enumerate(MENU_LAYOUT)
_NODE_METADATA: dict[str, dict[str, Any]] = {} _NODE_METADATA: dict[str, dict[str, Any]] = {}
for category, class_names in MENU_LAYOUT.items(): for category, class_names in MENU_LAYOUT.items():
for node_order, class_name in enumerate(class_names): for node_order, class_name in enumerate(class_names):
_NODE_METADATA[class_name] = { metadata = _NODE_METADATA.setdefault(class_name, {
"category": category, "category": category,
"category_order": _CATEGORY_ORDER[category], "category_order": _CATEGORY_ORDER[category],
"menu_order": node_order, "menu_order": node_order,
} "menu_categories": [],
})
metadata["menu_categories"].append({
"category": category,
"category_order": _CATEGORY_ORDER[category],
"menu_order": node_order,
})
def get_menu_metadata(class_name: str) -> dict[str, Any]: def get_menu_metadata(class_name: str) -> dict[str, Any]:
@@ -107,4 +121,9 @@ def get_menu_metadata(class_name: str) -> dict[str, Any]:
"category": "Unsorted", "category": "Unsorted",
"category_order": len(_CATEGORY_ORDER), "category_order": len(_CATEGORY_ORDER),
"menu_order": 10_000, "menu_order": 10_000,
"menu_categories": [{
"category": "Unsorted",
"category_order": len(_CATEGORY_ORDER),
"menu_order": 10_000,
}],
} }

View File

@@ -47,6 +47,7 @@ def get_node_info(class_name: str) -> dict[str, Any]:
"category": menu_metadata["category"], "category": menu_metadata["category"],
"category_order": menu_metadata["category_order"], "category_order": menu_metadata["category_order"],
"menu_order": menu_metadata["menu_order"], "menu_order": menu_metadata["menu_order"],
"menu_categories": list(menu_metadata.get("menu_categories", [])),
"input": input_types, "input": input_types,
"input_order": {k: list(v.keys()) for k, v in input_types.items()}, "input_order": {k: list(v.keys()) for k, v in input_types.items()},
"output": list(cls.RETURN_TYPES), "output": list(cls.RETURN_TYPES),

View File

@@ -404,8 +404,16 @@ function canStartCanvasRightDragZoom(target) {
} }
function compareMenuNodes(a, b) { function compareMenuNodes(a, b) {
const orderA = Number.isFinite(a?.def?.menu_order) ? a.def.menu_order : Number.MAX_SAFE_INTEGER; const orderA = Number.isFinite(a?.menu_order)
const orderB = Number.isFinite(b?.def?.menu_order) ? b.def.menu_order : Number.MAX_SAFE_INTEGER; ? a.menu_order
: Number.isFinite(a?.def?.menu_order)
? a.def.menu_order
: Number.MAX_SAFE_INTEGER;
const orderB = Number.isFinite(b?.menu_order)
? b.menu_order
: Number.isFinite(b?.def?.menu_order)
? b.def.menu_order
: Number.MAX_SAFE_INTEGER;
if (orderA !== orderB) return orderA - orderB; if (orderA !== orderB) return orderA - orderB;
const nameA = (a?.def?.display_name || a?.className || '').toLowerCase(); const nameA = (a?.def?.display_name || a?.className || '').toLowerCase();
@@ -615,19 +623,37 @@ function ContextMenu({ x, y, nodeDefs, onAdd, onClose, filterType, filterDirecti
if (!hasMatch) continue; if (!hasMatch) continue;
} }
} }
const cat = def.category || 'uncategorized'; const menuCategories = Array.isArray(def.menu_categories) && def.menu_categories.length > 0
if (!cats[cat]) { ? def.menu_categories
cats[cat] = { : [{
name: cat, category: def.category || 'uncategorized',
order: Number.isFinite(def.category_order) ? def.category_order : Number.MAX_SAFE_INTEGER, category_order: def.category_order,
items: [], menu_order: def.menu_order,
}; }];
for (const menuCategory of menuCategories) {
const cat = menuCategory?.category || def.category || 'uncategorized';
if (!cats[cat]) {
cats[cat] = {
name: cat,
order: Number.isFinite(menuCategory?.category_order)
? menuCategory.category_order
: Number.MAX_SAFE_INTEGER,
items: [],
};
}
cats[cat].order = Math.min(
cats[cat].order,
Number.isFinite(menuCategory?.category_order)
? menuCategory.category_order
: Number.MAX_SAFE_INTEGER,
);
cats[cat].items.push({
className,
def,
menu_order: Number.isFinite(menuCategory?.menu_order) ? menuCategory.menu_order : def.menu_order,
});
} }
cats[cat].order = Math.min(
cats[cat].order,
Number.isFinite(def.category_order) ? def.category_order : Number.MAX_SAFE_INTEGER,
);
cats[cat].items.push({ className, def });
} }
return Object.values(cats) return Object.values(cats)
.map((category) => ({ .map((category) => ({
@@ -642,10 +668,15 @@ function ContextMenu({ x, y, nodeDefs, onAdd, onClose, filterType, filterDirecti
if (!search.trim()) return null; if (!search.trim()) return null;
const q = search.toLowerCase(); const q = search.toLowerCase();
const results = []; const results = [];
const seen = new Set();
for (const category of categories) { for (const category of categories) {
for (const { className, def } of category.items) { for (const { className, def } of category.items) {
if (seen.has(className)) continue;
const name = (def.display_name || className).toLowerCase(); const name = (def.display_name || className).toLowerCase();
if (name.includes(q)) results.push({ className, def }); if (name.includes(q)) {
results.push({ className, def });
seen.add(className);
}
} }
} }
return results; return results;

View File

@@ -33,13 +33,16 @@ export const TYPE_COLORS = {
}; };
export const CAT_COLORS = { export const CAT_COLORS = {
io: '#37474f', Input: '#37474f',
filters: '#1a237e', Display: '#212121',
modify: '#0f766e', Overlay: '#0f766e',
level: '#1b5e20', Geometry: '#0d9488',
analysis: '#4a148c', Filter: '#1a237e',
Spectral: '#4c1d95',
'Level & Correct': '#1b5e20',
Measure: '#4a148c',
Mask: '#7c2d12',
Grains: '#bf360c', Grains: '#bf360c',
display: '#212121',
}; };
export const SOCKET_COMPATIBILITY = { export const SOCKET_COMPATIBILITY = {

View File

@@ -287,7 +287,7 @@ def test_flip_field():
overlays=[markup_overlay, annotation_overlay], overlays=[markup_overlay, annotation_overlay],
) )
assert get_node_info("FlipField")["category"] == "Modify" assert get_node_info("FlipField")["category"] == "Geometry"
flipped_x, = node.process(field, axis="x") flipped_x, = node.process(field, axis="x")
assert np.array_equal(flipped_x.data, np.flipud(data)) assert np.array_equal(flipped_x.data, np.flipud(data))
@@ -569,7 +569,7 @@ def test_facet_level():
node = FacetLevelField() node = FacetLevelField()
plane_node = PlaneLevelField() plane_node = PlaneLevelField()
assert get_node_info("FacetLevelField")["category"] == "Flatten" assert get_node_info("FacetLevelField")["category"] == "Level & Correct"
N = 96 N = 96
yy, xx = np.mgrid[0:N, 0:N] yy, xx = np.mgrid[0:N, 0:N]
@@ -749,7 +749,7 @@ def test_line_correction():
from backend.nodes.line_correction import LineCorrection from backend.nodes.line_correction import LineCorrection
node = LineCorrection() node = LineCorrection()
assert get_node_info("LineCorrection")["category"] == "Flatten" assert get_node_info("LineCorrection")["category"] == "Level & Correct"
rows = 96 rows = 96
cols = 128 cols = 128
@@ -815,7 +815,9 @@ def test_scar_removal():
from backend.nodes.scar_removal import ScarRemoval from backend.nodes.scar_removal import ScarRemoval
node = ScarRemoval() node = ScarRemoval()
assert get_node_info("ScarRemoval")["category"] == "Filter" info = get_node_info("ScarRemoval")
assert info["category"] == "Filter"
assert {entry["category"] for entry in info["menu_categories"]} == {"Filter", "Level & Correct"}
rows = 96 rows = 96
cols = 128 cols = 128
@@ -874,7 +876,9 @@ def test_angle_measure():
from backend.data_types import ImageData from backend.data_types import ImageData
node = AngleMeasure() node = AngleMeasure()
assert get_node_info("AngleMeasure")["category"] == "Overlay" info = get_node_info("AngleMeasure")
assert info["category"] == "Overlay"
assert {entry["category"] for entry in info["menu_categories"]} == {"Overlay", "Measure"}
required_inputs = AngleMeasure.INPUT_TYPES()["required"] required_inputs = AngleMeasure.INPUT_TYPES()["required"]
optional_inputs = AngleMeasure.INPUT_TYPES().get("optional", {}) optional_inputs = AngleMeasure.INPUT_TYPES().get("optional", {})
assert required_inputs["color"][1]["default"] == "#ff9800" assert required_inputs["color"][1]["default"] == "#ff9800"