clean up node menu
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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
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 {
|
.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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user