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]] = {
"Add": [
"Input": [
"Image",
"ImageDemo",
"Folder",
"ColorMap",
"Number",
"RangeSlider",
"Coordinate",
"CoordinatePair",
"Font",
],
"Output": [
"Display": [
"ColorMap",
"Font",
"ColormapAdjust",
"PreviewImage",
"ValueDisplay",
"View3D",
@@ -36,11 +37,10 @@ MENU_LAYOUT: dict[str, list[str]] = {
"Annotations",
"AngleMeasure",
],
"Modify": [
"Geometry": [
"CropResizeField",
"RotateField",
"FlipField",
"ColormapAdjust",
],
"Filter": [
"GaussianFilter",
@@ -50,25 +50,31 @@ MENU_LAYOUT: dict[str, list[str]] = {
"FFTFilter2D",
"ScarRemoval",
],
"Frequency": [
"PSDF",
"Spectral": [
"FFT2D",
"InverseFFT2D",
"FFTFilter1D",
"FFTFilter2D",
"ACF",
"PSDF",
],
"Flatten": [
"Level & Correct": [
"FixZero",
"LineCorrection",
"PlaneLevelField",
"PolyLevelField",
"FacetLevelField",
"LineCorrection",
"ScarRemoval",
],
"Measure": [
"AngleMeasure",
"CrossSection",
"Histogram",
"Cursors",
"Curvature",
"FractalDimension",
"ACF",
"PSDF",
"Statistics",
"Stats",
],
@@ -78,12 +84,14 @@ MENU_LAYOUT: dict[str, list[str]] = {
"MaskMorphology",
"MaskInvert",
"MaskOperations",
],
"Grains": [
"GrainAnalysis",
"GrainDistanceTransform",
"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]] = {}
for category, class_names in MENU_LAYOUT.items():
for node_order, class_name in enumerate(class_names):
_NODE_METADATA[class_name] = {
metadata = _NODE_METADATA.setdefault(class_name, {
"category": category,
"category_order": _CATEGORY_ORDER[category],
"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]:
@@ -107,4 +121,9 @@ def get_menu_metadata(class_name: str) -> dict[str, Any]:
"category": "Unsorted",
"category_order": len(_CATEGORY_ORDER),
"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_order": menu_metadata["category_order"],
"menu_order": menu_metadata["menu_order"],
"menu_categories": list(menu_metadata.get("menu_categories", [])),
"input": input_types,
"input_order": {k: list(v.keys()) for k, v in input_types.items()},
"output": list(cls.RETURN_TYPES),

View File

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

View File

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

View File

@@ -287,7 +287,7 @@ def test_flip_field():
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")
assert np.array_equal(flipped_x.data, np.flipud(data))
@@ -569,7 +569,7 @@ def test_facet_level():
node = FacetLevelField()
plane_node = PlaneLevelField()
assert get_node_info("FacetLevelField")["category"] == "Flatten"
assert get_node_info("FacetLevelField")["category"] == "Level & Correct"
N = 96
yy, xx = np.mgrid[0:N, 0:N]
@@ -749,7 +749,7 @@ def test_line_correction():
from backend.nodes.line_correction import LineCorrection
node = LineCorrection()
assert get_node_info("LineCorrection")["category"] == "Flatten"
assert get_node_info("LineCorrection")["category"] == "Level & Correct"
rows = 96
cols = 128
@@ -815,7 +815,9 @@ def test_scar_removal():
from backend.nodes.scar_removal import 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
cols = 128
@@ -874,7 +876,9 @@ def test_angle_measure():
from backend.data_types import ImageData
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"]
optional_inputs = AngleMeasure.INPUT_TYPES().get("optional", {})
assert required_inputs["color"][1]["default"] == "#ff9800"