From 8c7e93c92e73d2bdc50abc7f31efda19bb1e3d71 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Wed, 16 Jul 2025 11:03:40 +0200 Subject: [PATCH] UI/cubes: group logic to add more meshed --- pkgs/clan-app/ui/src/scene/cubes.tsx | 524 ++++++++++++--------------- 1 file changed, 239 insertions(+), 285 deletions(-) diff --git a/pkgs/clan-app/ui/src/scene/cubes.tsx b/pkgs/clan-app/ui/src/scene/cubes.tsx index 04e7ed24b..fe14f9a19 100644 --- a/pkgs/clan-app/ui/src/scene/cubes.tsx +++ b/pkgs/clan-app/ui/src/scene/cubes.tsx @@ -1,4 +1,3 @@ -// Working SolidJS + Three.js cube scene with reactive positioning import { createSignal, createEffect, @@ -7,15 +6,53 @@ import { createMemo, on, } from "solid-js"; -import { render } from "solid-js/web"; import * as THREE from "three"; +function garbageCollectGroup(group: THREE.Group) { + for (const child of group.children) { + if (child instanceof THREE.Mesh) { + child.geometry.dispose(); + if (Array.isArray(child.material)) { + child.material.forEach((material) => material.dispose()); + } else { + child.material.dispose(); + } + } else { + console.warn("Unknown child type in group:", child); + } + } + group.clear(); // Clear the group +} + +function getFloorPosition( + camera: THREE.PerspectiveCamera, + floor: THREE.Object3D, +): [number, number, number] { + const cameraPosition = camera.position.clone(); + + // Get camera's direction + const direction = new THREE.Vector3(); + camera.getWorldDirection(direction); + + // Define floor plane (XZ-plane at y=0) + const floorPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); // Normal = up, constant = 0 + + // Create ray from camera + const ray = new THREE.Ray(cameraPosition, direction); + + // Get intersection point + const intersection = new THREE.Vector3(); + ray.intersectPlane(floorPlane, intersection); + + return intersection.toArray() as [number, number, number]; +} export function CubeScene() { let container: HTMLDivElement; let scene: THREE.Scene; let camera: THREE.PerspectiveCamera; let renderer: THREE.WebGLRenderer; + let floor: THREE.Mesh; // Create background scene const bgScene = new THREE.Scene(); @@ -23,8 +60,7 @@ export function CubeScene() { let raycaster: THREE.Raycaster; - const meshMap = new Map(); - const baseMap = new Map(); // Map for cube bases + const groupMap = new Map(); const positionMap = new Map(); let sharedCubeGeometry: THREE.BoxGeometry; @@ -41,14 +77,15 @@ export function CubeScene() { const [positionMode, setPositionMode] = createSignal<"grid" | "circle">( "circle", ); - const [nextPosition, setNextPosition] = createSignal(null); + const [nextBasePosition, setNextPosition] = + createSignal(null); const [worldMode, setWorldMode] = createSignal<"view" | "create">("view"); // Backed camera position for restoring after switching mode - const [ backedCameraPosition, setBackedCameraPosition ] = createSignal<{ - pos: THREE.Vector3, - dir: THREE.Vector3 - }>() + const [backedCameraPosition, setBackedCameraPosition] = createSignal<{ + pos: THREE.Vector3; + dir: THREE.Vector3; + }>(); const [selectedIds, setSelectedIds] = createSignal>(new Set()); const [deletingIds, setDeletingIds] = createSignal>(new Set()); @@ -72,13 +109,11 @@ export function CubeScene() { const BASE_HEIGHT = 0.05; // Height of the cube above the ground const CUBE_Y = 0 + CUBE_SIZE / 2 + BASE_HEIGHT / 2; // Y position of the cube above the ground - const FLOOR_COLOR = 0xCDD8D9; + const FLOOR_COLOR = 0xcdd8d9; const BASE_COLOR = 0x9cbcff; const BASE_EMISSIVE = 0x0c0c0c; - - // Calculate grid position for a cube index with floating effect function getGridPosition( id: string, index: number, @@ -89,7 +124,7 @@ export function CubeScene() { if (pos) { return pos.toArray() as [number, number, number]; } - const nextPos = nextPosition(); + const nextPos = nextBasePosition(); if (!nextPos) { // Use next position if available throw new Error("Next position is not set"); @@ -102,17 +137,15 @@ export function CubeScene() { // Circle IDEA: // Need to talk with timo and W about this - function getCirclePosition( - _id: string, - index: number, - total: number, - ): [number, number, number] { - const r = total === 1 ? 0 : Math.sqrt(total) * CUBE_SPACING; // Radius based on total cubes - const x = Math.cos((index / total) * 2 * Math.PI) * r; - const z = Math.sin((index / total) * 2 * Math.PI) * r; - // Position cubes at y = 0.5 to float above the ground - return [x, CUBE_Y, z]; - } + const getCirclePosition = + (center: [number, number, number]) => + (_id: string, index: number, total: number): [number, number, number] => { + const r = total === 1 ? 0 : Math.sqrt(total) * CUBE_SPACING; // Radius based on total cubes + const x = Math.cos((index / total) * 2 * Math.PI) * r + center[0]; + const z = Math.sin((index / total) * 2 * Math.PI) * r + center[2]; + // Position cubes at y = 0.5 to float above the ground + return [x, CUBE_Y, z]; + }; // Reactive cubes memo - this recalculates whenever ids() changes const cubes = createMemo(() => { @@ -123,25 +156,30 @@ export function CubeScene() { // Include both active and deleting cubes for smooth transitions const allIds = [...new Set([...currentIds, ...Array.from(deleting)])]; - const getPosition = - positionMode() === "grid" ? getGridPosition : getCirclePosition; + let cameraTarget = [0, 0, 0] as [number, number, number]; + if (camera && floor) { + cameraTarget = getFloorPosition(camera, floor); + } + const getCubePosition = + positionMode() === "grid" + ? getGridPosition + : getCirclePosition(cameraTarget); - console.log("creating", creating); return allIds.map((id, index) => { const isDeleting = deleting.has(id); const isCreating = creating.has(id); const activeIndex = currentIds.indexOf(id); + const position = getCubePosition( + id, + isDeleting ? -1 : activeIndex >= 0 ? activeIndex : index, + currentIds.length, + ); - const position = getPosition( - id, - isDeleting ? -1 : activeIndex >= 0 ? activeIndex : index, - currentIds.length); - - const targetPosition = - activeIndex >= 0 - ? getPosition(id, activeIndex, currentIds.length) - : getPosition(id, index, currentIds.length); + const targetPosition = + activeIndex >= 0 + ? getCubePosition(id, activeIndex, currentIds.length) + : getCubePosition(id, index, currentIds.length); return { id, @@ -178,11 +216,11 @@ export function CubeScene() { // Animation helper function function animateToPosition( - mesh: THREE.Mesh, + thing: THREE.Object3D, targetPosition: [number, number, number], duration: number = ANIMATION_DURATION, ) { - const startPosition = mesh.position.clone(); + const startPosition = thing.position.clone(); const endPosition = new THREE.Vector3(...targetPosition); const startTime = Date.now(); @@ -193,7 +231,7 @@ export function CubeScene() { // Smooth easing function const easeProgress = 1 - Math.pow(1 - progress, 3); - mesh.position.lerpVectors(startPosition, endPosition, easeProgress); + thing.position.lerpVectors(startPosition, endPosition, easeProgress); if (progress < 1) { requestAnimationFrame(animate); @@ -270,16 +308,10 @@ export function CubeScene() { } // Delete animation helper - function animateDelete( - mesh: THREE.Mesh, - baseMesh: THREE.Mesh, - onComplete: () => void, - ) { + function animateDelete(group: THREE.Group, onComplete: () => void) { const startTime = Date.now(); - const startScale = mesh.scale.clone(); - const startOpacity = Array.isArray(mesh.material) - ? (mesh.material[0] as THREE.MeshBasicMaterial).opacity - : (mesh.material as THREE.MeshBasicMaterial).opacity; + // const startScale = group.scale.clone(); + // const startOpacity = 1; function animate() { const elapsed = Date.now() - startTime; @@ -288,31 +320,9 @@ export function CubeScene() { // Smooth easing function const easeProgress = 1 - Math.pow(1 - progress, 3); const scale = 1 - easeProgress; - const opacity = startOpacity * (1 - easeProgress); + // const opacity = startOpacity * (1 - easeProgress); - mesh.scale.setScalar(scale); - baseMesh.scale.setScalar(scale); - - // Update opacity for all materials - if (Array.isArray(mesh.material)) { - mesh.material.forEach((material) => { - (material as THREE.MeshBasicMaterial).opacity = opacity; - material.transparent = true; - }); - } else { - (mesh.material as THREE.MeshBasicMaterial).opacity = opacity; - mesh.material.transparent = true; - } - - if (Array.isArray(baseMesh.material)) { - baseMesh.material.forEach((material) => { - (material as THREE.MeshBasicMaterial).opacity = opacity; - material.transparent = true; - }); - } else { - (baseMesh.material as THREE.MeshBasicMaterial).opacity = opacity; - baseMesh.material.transparent = true; - } + group.scale.setScalar(scale); if (progress >= 1) { onComplete(); @@ -324,34 +334,6 @@ export function CubeScene() { animate(); } - // Helper for camera transition animation - function animateCameraToPosition( - targetPosition: THREE.Vector3, - target: THREE.Vector3, - onComplete: () => void, - ) { - const startPosition = camera.position.clone(); - const startTime = Date.now(); - - function animate() { - const elapsed = Date.now() - startTime; - const progress = Math.min(elapsed / ANIMATION_DURATION, 1); - - // Smooth easing function - const easeProgress = 1 - Math.pow(1 - progress, 3); - camera.position.lerpVectors(startPosition, targetPosition, easeProgress); - camera.lookAt(target); - - if (progress < 1) { - requestAnimationFrame(animate); - } else { - onComplete(); - } - } - - animate(); - } - function createCubeBase( cube_pos: [number, number, number], opacity: number = 1, @@ -389,33 +371,40 @@ export function CubeScene() { // Add to deleting set to start animation setDeletingIds(selectedSet); + console.log("Deleting cubes:", selectedSet); // Start delete animations selectedSet.forEach((id) => { - const mesh = meshMap.get(id); - const base = baseMap.get(id); + const group = groupMap.get(id); - if (mesh && base) { - animateDelete(mesh, base, () => { + if (group) { + groupMap.delete(id); // Remove from group map + + const base = group.children.find((child) => child.name === "base"); + const cube = group.children.find((child) => child.name === "cube"); + + if (!base || !cube) { + console.warn(`DELETE: Base mesh not found for id: ${id}`); + return; + } + + animateDelete(group, () => { // Remove from deleting set when animation completes setDeletingIds((prev) => { const next = new Set(prev); next.delete(id); return next; }); + setSelectedIds(new Set()); // Clear selection after deletion + garbageCollectGroup(group); // Clean up geometries and materials + setIds((prev) => prev.filter((existingId) => existingId !== id)); + + console.log("Done deleting", id, ids()); }); + } else { + console.warn(`DELETE: Group not found for id: ${id}`); } }); - - // Remove from ids after a short delay to allow animation to start - setTimeout(() => { - setIds((prev) => prev.filter((id) => !selectedSet.has(id))); - setSelectedIds(new Set()); // Clear selection after deletion - }, 50); - } - - function deleteCube(id: string) { - deleteSelectedCubes(new Set([id])); } function toggleSelection(id: string) { @@ -431,8 +420,15 @@ export function CubeScene() { } function updateMeshColors() { - for (const [id, base] of baseMap.entries()) { + for (const [id, group] of groupMap.entries()) { const selected = selectedIds().has(id); + const base = group.children.find((child) => child.name === "base"); + + if (!base || !(base instanceof THREE.Mesh)) { + // console.warn(`UPDATE COLORS: Base mesh not found for id: ${id}`); + continue; + } + const material = base.material as THREE.MeshLambertMaterial; if (selected) { @@ -443,7 +439,6 @@ export function CubeScene() { // Normal colors - restore original face colors material.color.set(BASE_COLOR); material.emissive.set(BASE_EMISSIVE); - } } } @@ -472,7 +467,7 @@ export function CubeScene() { let initBase: THREE.Mesh; - const grid = new THREE.GridHelper(1000, 1000 / 1, 0xE1EDEF, 0xE1EDEF); + const grid = new THREE.GridHelper(1000, 1000 / 1, 0xe1edef, 0xe1edef); onMount(() => { // Scene setup @@ -483,10 +478,12 @@ export function CubeScene() { // Create a fullscreen quad with a gradient shader // TODO: Recalculate gradient depending on container size - const uniforms = { - colorTop: { value: new THREE.Color('##E6EAEA') }, // Top color - colorBottom: { value: new THREE.Color('#C5D1D2') }, // Bottom color - resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) } + const uniforms = { + colorTop: { value: new THREE.Color("#E6EAEA") }, // Top color + colorBottom: { value: new THREE.Color("#C5D1D2") }, // Bottom color + resolution: { + value: new THREE.Vector2(window.innerWidth, window.innerHeight), + }, }; const bgMaterial = new THREE.ShaderMaterial({ @@ -570,7 +567,7 @@ export function CubeScene() { opacity: 0.1, // Make completely invisible // visible: false, // Also hide it completely }); - const floor = new THREE.Mesh(floorGeometry, floorMaterial); + floor = new THREE.Mesh(floorGeometry, floorMaterial); floor.rotation.x = -Math.PI / 2; floor.position.y = 0; // Keep at ground level for reference scene.add(floor); @@ -579,14 +576,18 @@ export function CubeScene() { // grid.material.transparent = true; grid.position.y = 0.001; // Slightly above the floor to avoid z-fighting // grid.rotation.x = -Math.PI / 2; - grid.position.x = 0.5 + grid.position.x = 0.5; grid.position.z = 0.5; scene.add(grid); // Shared geometries for cubes and bases // This allows us to reuse the same geometry for all cubes and bases sharedCubeGeometry = new THREE.BoxGeometry(CUBE_SIZE, CUBE_SIZE, CUBE_SIZE); - sharedBaseGeometry = new THREE.BoxGeometry(BASE_SIZE, BASE_HEIGHT, BASE_SIZE); + sharedBaseGeometry = new THREE.BoxGeometry( + BASE_SIZE, + BASE_HEIGHT, + BASE_SIZE, + ); // Basic OrbitControls implementation (simplified) let isDragging = false; @@ -643,12 +644,15 @@ export function CubeScene() { // Snap to grid const snapped = new THREE.Vector3( Math.round(point.x / GRID_SIZE) * GRID_SIZE, - CUBE_Y, + BASE_HEIGHT / 2, Math.round(point.z / GRID_SIZE) * GRID_SIZE, ); - if(!initBase) { + if (!initBase) { // Create initial base mesh if it doesn't exist - initBase = createCubeBase([snapped.x, 0.0, snapped.z], 0.5); + initBase = createCubeBase( + [snapped.x, BASE_HEIGHT / 2, snapped.z], + 0.5, + ); } else { initBase.position.set(snapped.x, BASE_HEIGHT / 2, snapped.z); } @@ -665,7 +669,7 @@ export function CubeScene() { const deltaX = event.clientX - previousMousePosition.x; const deltaY = event.clientY - previousMousePosition.y; // const deltaY = event.clientY - previousMousePosition.y; - if(positionMode() === "circle") { + if (positionMode() === "circle") { const spherical = new THREE.Spherical(); spherical.setFromVector3(camera.position); spherical.theta -= deltaX * 0.01; @@ -681,8 +685,7 @@ export function CubeScene() { camera.position.setFromSpherical(spherical); camera.lookAt(0, 0, 0); - } - else { + } else { const movementSpeed = 0.015; // Get camera direction vectors @@ -694,8 +697,11 @@ export function CubeScene() { cameraRight.crossVectors(camera.up, cameraDirection).normalize(); // Get right vector // Move camera based on mouse deltas - camera.position.addScaledVector(cameraRight, deltaX * movementSpeed); // horizontal drag - camera.position.addScaledVector(cameraDirection, deltaY * movementSpeed); // vertical drag (forward/back) + camera.position.addScaledVector(cameraRight, deltaX * movementSpeed); // horizontal drag + camera.position.addScaledVector( + cameraDirection, + deltaY * movementSpeed, + ); // vertical drag (forward/back) setBackedCameraPosition({ pos: camera.position.clone(), @@ -731,7 +737,7 @@ export function CubeScene() { // - Select/deselects a cube in "view" mode // - Creates a new cube in "create" mode const onClick = (event: MouseEvent) => { - if(worldMode() === "create") { + if (worldMode() === "create") { if (initBase) { scene.remove(initBase); // Remove the base mesh after adding cube setWorldMode("view"); @@ -740,7 +746,6 @@ export function CubeScene() { return; } - const rect = renderer.domElement.getBoundingClientRect(); const mouse = new THREE.Vector2( ((event.clientX - rect.left) / rect.width) * 2 - 1, @@ -749,7 +754,7 @@ export function CubeScene() { raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects( - Array.from(meshMap.values()), + Array.from(groupMap.values()), ); if (intersects.length > 0) { @@ -760,8 +765,6 @@ export function CubeScene() { } }; - const currentCubes = cubes(); - renderer.domElement.addEventListener("click", onClick); const animate = () => { @@ -800,7 +803,7 @@ export function CubeScene() { renderer.domElement.removeEventListener("click", onClick); window.removeEventListener("resize", handleResize); - if(initBase){ + if (initBase) { initBase.geometry.dispose(); // @ts-ignore: Not sure why this is needed initBase.material.dispose(); @@ -813,7 +816,6 @@ export function CubeScene() { }); createEffect(() => { - if (!container) return; if (worldMode() === "create") { // Show the plus button when in create mode @@ -829,64 +831,61 @@ export function CubeScene() { on(positionMode, (mode) => { if (mode === "circle") { grid.visible = false; // Hide grid when in circle mode - animateCameraToPosition( - new THREE.Vector3( - initialCameraPosition.x, - initialCameraPosition.y, - initialCameraPosition.z - ), - new THREE.Vector3(0, 0, 0), - () => { - console.log("Camera animation to circle position complete"); - } - ); } else if (mode === "grid") { grid.visible = true; // Show grid when in grid mode - const backup = backedCameraPosition(); // This won't be tracked reactively now - if (backup) { - const target = backup.pos.clone().add(backup.dir); - animateCameraToPosition(backup.pos, target, () => { - console.log("Camera animation to grid position complete"); - }); - } } - }) + }), ); + function createCube( + gridPosition: [number, number], + userData: { id: string; [key: string]: any }, + ) { + // Creates a cube, base, and other visuals + // Groups them together in the scene + const cubeMaterials = createCubeMaterials(); + const cubeMesh = new THREE.Mesh(sharedCubeGeometry, cubeMaterials); + cubeMesh.castShadow = true; + cubeMesh.receiveShadow = true; + cubeMesh.userData = userData; + cubeMesh.name = "cube"; // Name for easy identification + cubeMesh.position.set(0, CUBE_Y, 0); + + const baseMesh = createCubeBase([0, BASE_HEIGHT / 2, 0]); + baseMesh.name = "base"; // Name for easy identification + + // TODO: Destroy Group in onCleanup + const group = new THREE.Group(); + group.add(cubeMesh); + group.add(baseMesh); + group.position.set(gridPosition[0], 0, gridPosition[1]); // Position on the grid + + group.userData.id = userData.id; + return group; + } // Effect to manage cube meshes - this runs whenever cubes() changes createEffect(() => { const currentCubes = cubes(); - const existing = new Set(meshMap.keys()); + const existing = new Set(groupMap.keys()); const deleting = deletingIds(); const creating = creatingIds(); - console.log("Current cubes:", currentCubes); - // Update existing cubes and create new ones currentCubes.forEach((cube) => { - const existingMesh = meshMap.get(cube.id); - const existingBase = baseMap.get(cube.id); + const existingGroup = groupMap.get(cube.id); - if (!existingMesh) { - // Create new cube mesh - const cubeMaterials = createCubeMaterials(); - const mesh = new THREE.Mesh(sharedCubeGeometry, cubeMaterials); - mesh.castShadow = true; - mesh.receiveShadow = true; - mesh.position.set(...cube.position); - mesh.userData.id = cube.id; - scene.add(mesh); - meshMap.set(cube.id, mesh); - - // Create new base mesh - const base = createCubeBase(cube.position, 1); - base.userData.id = cube.id; - scene.add(base); - baseMap.set(cube.id, base); + if (!existingGroup) { + const group = createCube([cube.position[0], cube.position[2]], { + id: cube.id, + }); + scene.add(group); + groupMap.set(cube.id, group); // Start create animation if this cube is being created if (creating.has(cube.id)) { + const mesh = group.children[0] as THREE.Mesh; + const base = group.children[1] as THREE.Mesh; animateCreate(mesh, base, () => { // Animation complete callback - could add additional logic here }); @@ -894,28 +893,19 @@ export function CubeScene() { } else if (!deleting.has(cube.id)) { // Only animate position if not being deleted const targetPosition = cube.targetPosition || cube.position; - const currentPosition = existingMesh.position.toArray() as [ + const currentPosition = existingGroup.position.toArray() as [ number, number, number, ]; const target = targetPosition; - // Check if position actually changed if ( Math.abs(currentPosition[0] - target[0]) > 0.01 || Math.abs(currentPosition[1] - target[1]) > 0.01 || Math.abs(currentPosition[2] - target[2]) > 0.01 ) { - animateToPosition(existingMesh, target); - - if (existingBase) { - animateToPosition(existingBase, [ - target[0], - target[1] - 0.5 - 0.025, // TODO: Refactor this DRY - target[2], - ]); - } + animateToPosition(existingGroup, target); } } @@ -925,32 +915,9 @@ export function CubeScene() { // Remove cubes that are no longer in the state and not being deleted existing.forEach((id) => { if (!deleting.has(id)) { - // Remove cube mesh - const mesh = meshMap.get(id); - if (mesh) { - scene.remove(mesh); - mesh.geometry.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 - 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); + const group = groupMap.get(id); + if (group) { + garbageCollectGroup(group); } } }); @@ -966,31 +933,21 @@ export function CubeScene() { // Clean up cubes that finished their delete animation deleting.forEach((id) => { if (!currentIds.includes(id)) { - // Check if this cube has finished its animation - const mesh = meshMap.get(id); - if (mesh && mesh.scale.x <= 0.01) { - // Remove cube mesh - scene.remove(mesh); - mesh.geometry.dispose(); - if (Array.isArray(mesh.material)) { - mesh.material.forEach((material) => material.dispose()); - } else { - mesh.material.dispose(); - } - meshMap.delete(id); - - // Remove base mesh - const base = baseMap.get(id); - if (base) { - scene.remove(base); - base.geometry.dispose(); - if (Array.isArray(base.material)) { - base.material.forEach((material) => material.dispose()); - } else { - base.material.dispose(); + const group = groupMap.get(id); + if (group) { + scene.remove(group); + group.children.forEach((child) => { + // Child is finished with its destroy animation + if (child instanceof THREE.Mesh && child.scale.x <= 0.01) { + child.geometry.dispose(); + if (Array.isArray(child.material)) { + child.material.forEach((material) => material.dispose()); + } else { + child.material.dispose(); + } } - baseMap.delete(id); - } + }); + groupMap.delete(id); } } }); @@ -1002,25 +959,11 @@ export function CubeScene() { }); onCleanup(() => { - for (const mesh of meshMap.values()) { - // Handle both single material and material array - if (Array.isArray(mesh.material)) { - mesh.material.forEach((material) => material.dispose()); - } else { - mesh.material.dispose(); - } + for (const group of groupMap.values()) { + garbageCollectGroup(group); + scene.remove(group); } - meshMap.clear(); - - for (const mesh of baseMap.values()) { - // Handle both single material and material array - if (Array.isArray(mesh.material)) { - mesh.material.forEach((material) => material.dispose()); - } else { - mesh.material.dispose(); - } - } - baseMap.clear(); + groupMap.clear(); // Dispose shared geometries sharedCubeGeometry?.dispose(); @@ -1038,7 +981,7 @@ export function CubeScene() { if (!initBase) { // Create initial base mesh if it doesn't exist - initBase = createCubeBase([0, 0.5, 0], 0.5); + initBase = createCubeBase([0, BASE_HEIGHT / 2, 0], 0.5); } if (inside) { scene.add(initBase); @@ -1053,24 +996,8 @@ export function CubeScene() { }; return ( -
+
- - - - Selected: {selectedIds().size} cubes | Total: {ids().length} cubes @@ -1082,6 +1009,15 @@ export function CubeScene() {
+
(container = el)} + style={{ + width: "100%", + height: "80vh", + cursor: "pointer", + }} + /> + {/* Camera Information Display */}
+
+ + + + +
Camera Info:
@@ -1106,15 +1069,6 @@ export function CubeScene() { {cameraInfo().spherical.theta}, φ={cameraInfo().spherical.phi}
- -
(container = el)} - style={{ - width: "100%", - height: "90vh", - cursor: "pointer", - }} - />
); }