clean up node menu

This commit is contained in:
2026-04-16 21:37:03 -07:00
parent 924b29757f
commit a4c8d2b01c
5 changed files with 110 additions and 20 deletions

View File

@@ -3,6 +3,7 @@ import { socketSpecAcceptsType } from './constants';
import { outputTypeCanConnectToTarget } from './connectionUtils'; import { outputTypeCanConnectToTarget } from './connectionUtils';
import { compareMenuNodes, compareMenuCategories } from './canvasEvents'; import { compareMenuNodes, compareMenuCategories } from './canvasEvents';
import { useFavorites } from './favorites'; import { useFavorites } from './favorites';
import { recordUsage, pickWeightedRandom } from './nodeUsage';
const FAVORITES_CATEGORY = 'favorites'; const FAVORITES_CATEGORY = 'favorites';
@@ -213,6 +214,29 @@ export default function ContextMenu({
setOpenCat(cat); setOpenCat(cat);
}, []); }, []);
const allNodeEntries = useMemo(() => {
const map = new Map<string, any>();
for (const category of categories) {
for (const item of category.items) {
if (!map.has(item.className)) map.set(item.className, item.def);
}
}
return map;
}, [categories]);
const handleAdd = useCallback((className: string, def: any) => {
recordUsage(className);
onAdd(className, def);
}, [onAdd]);
const handleRandomNode = useCallback(() => {
const classNames = [...allNodeEntries.keys()];
const pick = pickWeightedRandom(classNames);
if (!pick) return;
const def = allNodeEntries.get(pick);
if (def) { handleAdd(pick, def); onClose(); }
}, [allNodeEntries, handleAdd, onClose]);
if (categories.length === 0) { if (categories.length === 0) {
return ( return (
<div className="context-menu" ref={menuRef} style={{ left: menuPos.x, top: menuPos.y }} onClick={(e) => e.stopPropagation()}> <div className="context-menu" ref={menuRef} style={{ left: menuPos.x, top: menuPos.y }} onClick={(e) => e.stopPropagation()}>
@@ -256,7 +280,7 @@ export default function ContextMenu({
className="context-item" className="context-item"
onClick={() => { onCreateGroup(); onClose(); }} onClick={() => { onCreateGroup(); onClose(); }}
> >
create group Create Group
</div> </div>
)} )}
@@ -270,7 +294,7 @@ export default function ContextMenu({
key={className} key={className}
ref={idx === selectedIndex ? selectedItemRef : null} ref={idx === selectedIndex ? selectedItemRef : null}
className={`context-item${idx === selectedIndex ? ' context-item--selected' : ''}`} className={`context-item${idx === selectedIndex ? ' context-item--selected' : ''}`}
onClick={() => { onAdd(className, def); onClose(); }} onClick={() => { handleAdd(className, def); onClose(); }}
onMouseEnter={() => setSelectedIndex(idx)} onMouseEnter={() => setSelectedIndex(idx)}
> >
{def.display_name || className} {def.display_name || className}
@@ -288,11 +312,18 @@ export default function ContextMenu({
onMouseEnter={() => handleCatEnter(cat)} onMouseEnter={() => handleCatEnter(cat)}
> >
<span className="ctx-cat-label"> <span className="ctx-cat-label">
{cat === FAVORITES_CATEGORY ? '♥ favorites' : cat} {cat === FAVORITES_CATEGORY ? 'Favorites' : cat}
</span> </span>
<span className="ctx-cat-arrow"></span> <span className="ctx-cat-arrow"></span>
</div> </div>
))} ))}
<div
className="ctx-cat-item ctx-random-node"
onClick={handleRandomNode}
onMouseEnter={() => setOpenCat(null)}
>
<span className="ctx-cat-label">surprise me</span>
</div>
</div> </div>
)} )}
</div> </div>
@@ -314,7 +345,7 @@ export default function ContextMenu({
<div <div
key={className} key={className}
className="context-item" className="context-item"
onClick={() => { onAdd(className, def); onClose(); }} onClick={() => { handleAdd(className, def); onClose(); }}
> >
{def.display_name || className} {def.display_name || className}
</div> </div>

View File

@@ -1426,6 +1426,7 @@ function CustomNode({ id, data }: { id: string; data: NodeData }) {
{/* Interactive 3D surface view */} {/* Interactive 3D surface view */}
{!!data.meshData && ( {!!data.meshData && (
<CollapsibleSection title="3D View" defaultOpen={true}> <CollapsibleSection title="3D View" defaultOpen={true}>
<PreviewBoundary resetKey={String(data.meshData)}>
<Suspense fallback={<div className="node-preview" style={{color:'var(--text-muted)',padding:4}}>Loading 3D...</div>}> <Suspense fallback={<div className="node-preview" style={{color:'var(--text-muted)',padding:4}}>Loading 3D...</div>}>
<SurfaceView <SurfaceView
meshData={data.meshData as any} meshData={data.meshData as any}
@@ -1435,6 +1436,7 @@ function CustomNode({ id, data }: { id: string; data: NodeData }) {
onRuntimeValuesChange={ctx?.onRuntimeValuesChange} onRuntimeValuesChange={ctx?.onRuntimeValuesChange}
/> />
</Suspense> </Suspense>
</PreviewBoundary>
</CollapsibleSection> </CollapsibleSection>
)} )}

View File

@@ -133,6 +133,8 @@ export default function SurfaceView({ meshData, nodeId, widgetValues, runtimeVal
const pointerEnteredAtRef = useRef(0); const pointerEnteredAtRef = useRef(0);
const lastWheelAtRef = useRef(0); const lastWheelAtRef = useRef(0);
const gestureStartedInsideRef = useRef(false); const gestureStartedInsideRef = useRef(false);
const scheduleViewportSyncRef = useRef<(delay?: number, scheduleRun?: boolean) => void>(() => {});
const updateDiagnosticsRef = useRef<(patch: Partial<DiagnosticsState>) => void>(() => {});
const [diagnostics, setDiagnostics] = useState<DiagnosticsState>({ const [diagnostics, setDiagnostics] = useState<DiagnosticsState>({
status: meshData ? 'initializing' : 'waiting for mesh', status: meshData ? 'initializing' : 'waiting for mesh',
webgl: 'pending', webgl: 'pending',
@@ -239,6 +241,9 @@ export default function SurfaceView({ meshData, nodeId, widgetValues, runtimeVal
scheduleViewportSync(0, true); scheduleViewportSync(0, true);
}, [applyCameraState, scheduleViewportSync]); }, [applyCameraState, scheduleViewportSync]);
scheduleViewportSyncRef.current = scheduleViewportSync;
updateDiagnosticsRef.current = updateDiagnostics;
// Initialize Three.js scene once // Initialize Three.js scene once
useEffect(() => { useEffect(() => {
const container = containerRef.current; const container = containerRef.current;
@@ -256,8 +261,8 @@ export default function SurfaceView({ meshData, nodeId, widgetValues, runtimeVal
renderer.setPixelRatio(window.devicePixelRatio); renderer.setPixelRatio(window.devicePixelRatio);
renderer.setClearColor(0x0f172a); renderer.setClearColor(0x0f172a);
container.appendChild(renderer.domElement); container.appendChild(renderer.domElement);
updateDiagnostics({ updateDiagnosticsRef.current({
status: meshData ? 'renderer ready' : 'waiting for mesh', status: 'renderer ready',
webgl: `${renderer.capabilities.isWebGL2 ? 'webgl2' : 'webgl1'} / ${renderer.capabilities.precision}`, webgl: `${renderer.capabilities.isWebGL2 ? 'webgl2' : 'webgl1'} / ${renderer.capabilities.precision}`,
canvas: `${renderer.domElement.width}x${renderer.domElement.height} px`, canvas: `${renderer.domElement.width}x${renderer.domElement.height} px`,
render: `calls ${renderer.info.render.calls} tris ${renderer.info.render.triangles} geo ${renderer.info.memory.geometries}`, render: `calls ${renderer.info.render.calls} tris ${renderer.info.render.triangles} geo ${renderer.info.memory.geometries}`,
@@ -266,13 +271,13 @@ export default function SurfaceView({ meshData, nodeId, widgetValues, runtimeVal
const handleContextLost = (event: Event) => { const handleContextLost = (event: Event) => {
event.preventDefault(); event.preventDefault();
updateDiagnostics({ updateDiagnosticsRef.current({
status: 'webgl context lost', status: 'webgl context lost',
error: 'WebGL context lost', error: 'WebGL context lost',
}); });
}; };
const handleContextRestored = () => { const handleContextRestored = () => {
updateDiagnostics({ updateDiagnosticsRef.current({
status: 'webgl context restored', status: 'webgl context restored',
error: '', error: '',
}); });
@@ -303,7 +308,7 @@ export default function SurfaceView({ meshData, nodeId, widgetValues, runtimeVal
TWO: THREE.TOUCH.DOLLY_ROTATE, TWO: THREE.TOUCH.DOLLY_ROTATE,
}; };
renderer.domElement.style.touchAction = 'none'; renderer.domElement.style.touchAction = 'none';
const handleControlsEnd = () => scheduleViewportSync(120, true); const handleControlsEnd = () => scheduleViewportSyncRef.current(120, true);
controls.addEventListener('end', handleControlsEnd); controls.addEventListener('end', handleControlsEnd);
// Lighting // Lighting
@@ -341,7 +346,7 @@ export default function SurfaceView({ meshData, nodeId, widgetValues, runtimeVal
r.setSize(w, w); r.setSize(w, w);
c.aspect = 1; c.aspect = 1;
c.updateProjectionMatrix(); c.updateProjectionMatrix();
updateDiagnostics({ updateDiagnosticsRef.current({
canvas: `${r.domElement.width}x${r.domElement.height} px`, canvas: `${r.domElement.width}x${r.domElement.height} px`,
}); });
}); });
@@ -361,7 +366,8 @@ export default function SurfaceView({ meshData, nodeId, widgetValues, runtimeVal
} }
threeRef.current = null; threeRef.current = null;
}; };
}, [scheduleViewportSync, updateDiagnostics]); // eslint-disable-next-line react-hooks/exhaustive-deps
}, [applyCameraState]);
useEffect(() => { useEffect(() => {
if (meshData) { if (meshData) {

46
frontend/src/nodeUsage.ts Normal file
View File

@@ -0,0 +1,46 @@
const STORAGE_KEY = 'tono_node_usage_counts';
let counts: Record<string, number> = loadFromStorage();
function loadFromStorage(): Record<string, number> {
if (typeof localStorage === 'undefined') return {};
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return {};
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object') return {};
return parsed;
} catch {
return {};
}
}
function persist(): void {
if (typeof localStorage === 'undefined') return;
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(counts));
} catch {
// ignore
}
}
export function recordUsage(className: string): void {
counts = { ...counts, [className]: (counts[className] || 0) + 1 };
persist();
}
export function getUsageCount(className: string): number {
return counts[className] || 0;
}
export function pickWeightedRandom(classNames: string[]): string | null {
if (classNames.length === 0) return null;
const weights = classNames.map((cn) => 1 / (1 + (counts[cn] || 0)));
const total = weights.reduce((a, b) => a + b, 0);
let r = Math.random() * total;
for (let i = 0; i < classNames.length; i++) {
r -= weights[i];
if (r <= 0) return classNames[i];
}
return classNames[classNames.length - 1];
}

View File

@@ -2515,6 +2515,11 @@ html, body, #root {
.ctx-cat-favorites .ctx-cat-label { .ctx-cat-favorites .ctx-cat-label {
text-transform: none; text-transform: none;
} }
.ctx-random-node {
border-top: 1px solid var(--border-strong);
color: var(--text-secondary);
font-style: italic;
}
/* ── Submenu panel (separate fixed-position sibling) ── */ /* ── Submenu panel (separate fixed-position sibling) ── */
.ctx-submenu { .ctx-submenu {