diff --git a/pkgs/clan-app/ui/src/index.tsx b/pkgs/clan-app/ui/src/index.tsx index 595c06822..a8046948a 100644 --- a/pkgs/clan-app/ui/src/index.tsx +++ b/pkgs/clan-app/ui/src/index.tsx @@ -20,4 +20,4 @@ if (import.meta.env.DEV) { await import("solid-devtools"); } -render(() => , root!); +render(() => , root!); diff --git a/pkgs/clan-app/ui/src/scene/qubes.tsx b/pkgs/clan-app/ui/src/scene/qubes.tsx index df82dbb87..cc56a14dd 100644 --- a/pkgs/clan-app/ui/src/scene/qubes.tsx +++ b/pkgs/clan-app/ui/src/scene/qubes.tsx @@ -15,10 +15,20 @@ export function CubeScene() { let camera: THREE.PerspectiveCamera; let renderer: THREE.WebGLRenderer; let raycaster: THREE.Raycaster; - let controls: any; // OrbitControls type + let meshMap = new Map(); let baseMap = new Map(); // Map for cube bases + let sharedCubeGeometry: THREE.BoxGeometry; + let sharedBaseGeometry: THREE.BoxGeometry; + + // Used for development purposes + // Vite does hot-reload but we need to ensure the animation loop doesn't run multiple times + // This flag prevents multiple animation loops from running simultaneously + // It is set to true when the component mounts and false when it unmounts + let isAnimating = false; // Flag to prevent multiple loops + let frameCount = 0; + const [cubes, setCubes] = createSignal([]); const [selectedIds, setSelectedIds] = createSignal>(new Set()); const [cameraInfo, setCameraInfo] = createSignal({ @@ -66,9 +76,8 @@ export function CubeScene() { // Create white base for cube function createCubeBase(cube_pos: [number, number, number]) { - const baseGeometry = new THREE.BoxGeometry(1.2, 0.05, 1.2); // 1.2 times cube size, thin height const baseMaterials = createBaseMaterials(); - const base = new THREE.Mesh(baseGeometry, baseMaterials); + const base = new THREE.Mesh(sharedBaseGeometry, baseMaterials); // tranlate_y = - cube_height / 2 - base_height / 2 base.position.set(cube_pos[0], cube_pos[1] - 0.5 - 0.025, cube_pos[2]); // Position below cube base.receiveShadow = true; @@ -88,14 +97,35 @@ export function CubeScene() { } function deleteCube(id: string) { - setCubes((prev) => prev.filter((c) => c.id !== id)); + // Remove cube mesh const mesh = meshMap.get(id); if (mesh) { scene.remove(mesh); mesh.geometry.dispose(); - (mesh.material as THREE.Material).dispose(); + // Dispose materials properly + if (Array.isArray(mesh.material)) { + mesh.material.forEach((material) => material.dispose()); + } else { + mesh.material.dispose(); + } meshMap.delete(id); } + + // Remove base mesh - THIS WAS MISSING! + const base = baseMap.get(id); + if (base) { + scene.remove(base); + base.geometry.dispose(); + // Dispose base materials properly + if (Array.isArray(base.material)) { + base.material.forEach((material) => material.dispose()); + } else { + base.material.dispose(); + } + baseMap.delete(id); + } + + setCubes((prev) => prev.filter((c) => c.id !== id)); } function toggleSelection(id: string) { @@ -137,6 +167,18 @@ export function CubeScene() { } } + function logMemoryUsage() { + if (renderer && renderer.info) { + console.log("Three.js Memory:", { + geometries: renderer.info.memory.geometries, + textures: renderer.info.memory.textures, + programs: renderer.info.programs?.length || 0, + calls: renderer.info.render.calls, + triangles: renderer.info.render.triangles, + }); + } + } + onMount(() => { // Scene setup scene = new THREE.Scene(); @@ -196,6 +238,11 @@ export function CubeScene() { floor.position.y = 0; // Keep at ground level for reference scene.add(floor); + // Shared geometries for cubes and bases + // This allows us to reuse the same geometry for all cubes and bases + sharedCubeGeometry = new THREE.BoxGeometry(1, 1, 1); + sharedBaseGeometry = new THREE.BoxGeometry(1.2, 0.05, 1.2); + // Basic OrbitControls implementation (simplified) let isDragging = false; let previousMousePosition = { x: 0, y: 0 }; @@ -288,11 +335,18 @@ export function CubeScene() { renderer.domElement.addEventListener("click", onClick); - // Animation loop const animate = () => { + if (!isAnimating) return; // Exit if component is unmounted + requestAnimationFrame(animate); + + frameCount++; renderer.render(scene, camera); + + // Uncomment for memory debugging: + if (frameCount % 60 === 0) logMemoryUsage(); // Log every 60 frames }; + isAnimating = true; animate(); // Handle window resize @@ -305,12 +359,18 @@ export function CubeScene() { // Cleanup function onCleanup(() => { + // Stop animation loop + isAnimating = false; renderer.domElement.removeEventListener("mousedown", onMouseDown); renderer.domElement.removeEventListener("mouseup", onMouseUp); renderer.domElement.removeEventListener("mousemove", onMouseMove); renderer.domElement.removeEventListener("wheel", onWheel); renderer.domElement.removeEventListener("click", onClick); window.removeEventListener("resize", handleResize); + + if (container) { + container.innerHTML = ""; + } }); }); @@ -322,9 +382,8 @@ export function CubeScene() { cubes().forEach((cube) => { if (!meshMap.has(cube.id)) { // Create cube mesh - const cubeGeometry = new THREE.BoxGeometry(1, 1, 1); const cubeMaterials = createCubeMaterials(); - const mesh = new THREE.Mesh(cubeGeometry, cubeMaterials); + const mesh = new THREE.Mesh(sharedCubeGeometry, cubeMaterials); mesh.castShadow = true; mesh.receiveShadow = true; mesh.position.set(...cube.position); @@ -356,9 +415,7 @@ export function CubeScene() { }); onCleanup(() => { - renderer?.dispose(); for (const mesh of meshMap.values()) { - mesh.geometry.dispose(); // Handle both single material and material array if (Array.isArray(mesh.material)) { mesh.material.forEach((material) => material.dispose()); @@ -367,8 +424,8 @@ export function CubeScene() { } } meshMap.clear(); + for (const mesh of baseMap.values()) { - mesh.geometry.dispose(); // Handle both single material and material array if (Array.isArray(mesh.material)) { mesh.material.forEach((material) => material.dispose()); @@ -377,6 +434,12 @@ export function CubeScene() { } } baseMap.clear(); + + // Dispose shared geometries + sharedCubeGeometry?.dispose(); + sharedBaseGeometry?.dispose(); + + renderer?.dispose(); }); return (