clean up node menu
This commit is contained in:
@@ -3,6 +3,7 @@ import { socketSpecAcceptsType } from './constants';
|
||||
import { outputTypeCanConnectToTarget } from './connectionUtils';
|
||||
import { compareMenuNodes, compareMenuCategories } from './canvasEvents';
|
||||
import { useFavorites } from './favorites';
|
||||
import { recordUsage, pickWeightedRandom } from './nodeUsage';
|
||||
|
||||
const FAVORITES_CATEGORY = 'favorites';
|
||||
|
||||
@@ -213,6 +214,29 @@ export default function ContextMenu({
|
||||
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) {
|
||||
return (
|
||||
<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"
|
||||
onClick={() => { onCreateGroup(); onClose(); }}
|
||||
>
|
||||
create group
|
||||
Create Group
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -270,7 +294,7 @@ export default function ContextMenu({
|
||||
key={className}
|
||||
ref={idx === selectedIndex ? selectedItemRef : null}
|
||||
className={`context-item${idx === selectedIndex ? ' context-item--selected' : ''}`}
|
||||
onClick={() => { onAdd(className, def); onClose(); }}
|
||||
onClick={() => { handleAdd(className, def); onClose(); }}
|
||||
onMouseEnter={() => setSelectedIndex(idx)}
|
||||
>
|
||||
{def.display_name || className}
|
||||
@@ -288,11 +312,18 @@ export default function ContextMenu({
|
||||
onMouseEnter={() => handleCatEnter(cat)}
|
||||
>
|
||||
<span className="ctx-cat-label">
|
||||
{cat === FAVORITES_CATEGORY ? '♥ favorites' : cat}
|
||||
{cat === FAVORITES_CATEGORY ? 'Favorites' : cat}
|
||||
</span>
|
||||
<span className="ctx-cat-arrow">▶</span>
|
||||
</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>
|
||||
@@ -314,7 +345,7 @@ export default function ContextMenu({
|
||||
<div
|
||||
key={className}
|
||||
className="context-item"
|
||||
onClick={() => { onAdd(className, def); onClose(); }}
|
||||
onClick={() => { handleAdd(className, def); onClose(); }}
|
||||
>
|
||||
{def.display_name || className}
|
||||
</div>
|
||||
|
||||
@@ -1426,15 +1426,17 @@ function CustomNode({ id, data }: { id: string; data: NodeData }) {
|
||||
{/* Interactive 3D surface view */}
|
||||
{!!data.meshData && (
|
||||
<CollapsibleSection title="3D View" defaultOpen={true}>
|
||||
<Suspense fallback={<div className="node-preview" style={{color:'var(--text-muted)',padding:4}}>Loading 3D...</div>}>
|
||||
<SurfaceView
|
||||
meshData={data.meshData as any}
|
||||
nodeId={id}
|
||||
widgetValues={data.widgetValues}
|
||||
runtimeValues={data.runtimeValues}
|
||||
onRuntimeValuesChange={ctx?.onRuntimeValuesChange}
|
||||
/>
|
||||
</Suspense>
|
||||
<PreviewBoundary resetKey={String(data.meshData)}>
|
||||
<Suspense fallback={<div className="node-preview" style={{color:'var(--text-muted)',padding:4}}>Loading 3D...</div>}>
|
||||
<SurfaceView
|
||||
meshData={data.meshData as any}
|
||||
nodeId={id}
|
||||
widgetValues={data.widgetValues}
|
||||
runtimeValues={data.runtimeValues}
|
||||
onRuntimeValuesChange={ctx?.onRuntimeValuesChange}
|
||||
/>
|
||||
</Suspense>
|
||||
</PreviewBoundary>
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
|
||||
|
||||
@@ -133,6 +133,8 @@ export default function SurfaceView({ meshData, nodeId, widgetValues, runtimeVal
|
||||
const pointerEnteredAtRef = useRef(0);
|
||||
const lastWheelAtRef = useRef(0);
|
||||
const gestureStartedInsideRef = useRef(false);
|
||||
const scheduleViewportSyncRef = useRef<(delay?: number, scheduleRun?: boolean) => void>(() => {});
|
||||
const updateDiagnosticsRef = useRef<(patch: Partial<DiagnosticsState>) => void>(() => {});
|
||||
const [diagnostics, setDiagnostics] = useState<DiagnosticsState>({
|
||||
status: meshData ? 'initializing' : 'waiting for mesh',
|
||||
webgl: 'pending',
|
||||
@@ -239,6 +241,9 @@ export default function SurfaceView({ meshData, nodeId, widgetValues, runtimeVal
|
||||
scheduleViewportSync(0, true);
|
||||
}, [applyCameraState, scheduleViewportSync]);
|
||||
|
||||
scheduleViewportSyncRef.current = scheduleViewportSync;
|
||||
updateDiagnosticsRef.current = updateDiagnostics;
|
||||
|
||||
// Initialize Three.js scene once
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
@@ -256,8 +261,8 @@ export default function SurfaceView({ meshData, nodeId, widgetValues, runtimeVal
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
renderer.setClearColor(0x0f172a);
|
||||
container.appendChild(renderer.domElement);
|
||||
updateDiagnostics({
|
||||
status: meshData ? 'renderer ready' : 'waiting for mesh',
|
||||
updateDiagnosticsRef.current({
|
||||
status: 'renderer ready',
|
||||
webgl: `${renderer.capabilities.isWebGL2 ? 'webgl2' : 'webgl1'} / ${renderer.capabilities.precision}`,
|
||||
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}`,
|
||||
@@ -266,13 +271,13 @@ export default function SurfaceView({ meshData, nodeId, widgetValues, runtimeVal
|
||||
|
||||
const handleContextLost = (event: Event) => {
|
||||
event.preventDefault();
|
||||
updateDiagnostics({
|
||||
updateDiagnosticsRef.current({
|
||||
status: 'webgl context lost',
|
||||
error: 'WebGL context lost',
|
||||
});
|
||||
};
|
||||
const handleContextRestored = () => {
|
||||
updateDiagnostics({
|
||||
updateDiagnosticsRef.current({
|
||||
status: 'webgl context restored',
|
||||
error: '',
|
||||
});
|
||||
@@ -303,7 +308,7 @@ export default function SurfaceView({ meshData, nodeId, widgetValues, runtimeVal
|
||||
TWO: THREE.TOUCH.DOLLY_ROTATE,
|
||||
};
|
||||
renderer.domElement.style.touchAction = 'none';
|
||||
const handleControlsEnd = () => scheduleViewportSync(120, true);
|
||||
const handleControlsEnd = () => scheduleViewportSyncRef.current(120, true);
|
||||
controls.addEventListener('end', handleControlsEnd);
|
||||
|
||||
// Lighting
|
||||
@@ -341,7 +346,7 @@ export default function SurfaceView({ meshData, nodeId, widgetValues, runtimeVal
|
||||
r.setSize(w, w);
|
||||
c.aspect = 1;
|
||||
c.updateProjectionMatrix();
|
||||
updateDiagnostics({
|
||||
updateDiagnosticsRef.current({
|
||||
canvas: `${r.domElement.width}x${r.domElement.height} px`,
|
||||
});
|
||||
});
|
||||
@@ -361,7 +366,8 @@ export default function SurfaceView({ meshData, nodeId, widgetValues, runtimeVal
|
||||
}
|
||||
threeRef.current = null;
|
||||
};
|
||||
}, [scheduleViewportSync, updateDiagnostics]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [applyCameraState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (meshData) {
|
||||
|
||||
46
frontend/src/nodeUsage.ts
Normal file
46
frontend/src/nodeUsage.ts
Normal 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];
|
||||
}
|
||||
@@ -2515,6 +2515,11 @@ html, body, #root {
|
||||
.ctx-cat-favorites .ctx-cat-label {
|
||||
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) ── */
|
||||
.ctx-submenu {
|
||||
|
||||
Reference in New Issue
Block a user