cubes: scene extend

This commit is contained in:
Johannes Kirschbauer
2025-07-15 17:04:10 +02:00
parent 45f7ebc0c9
commit 579885a6e2

View File

@@ -5,25 +5,27 @@ import {
onCleanup,
onMount,
createMemo,
on,
} from "solid-js";
import { render } from "solid-js/web";
import * as THREE from "three";
// Cube Data Model
interface CubeData {
id: string;
position: [number, number, number];
color: string;
}
export function CubeScene() {
let container: HTMLDivElement;
let scene: THREE.Scene;
let camera: THREE.PerspectiveCamera;
let renderer: THREE.WebGLRenderer;
// Create background scene
const bgScene = new THREE.Scene();
const bgCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
let raycaster: THREE.Raycaster;
const meshMap = new Map<string, THREE.Mesh>();
const baseMap = new Map<string, THREE.Mesh>(); // Map for cube bases
const positionMap = new Map<string, THREE.Vector3>();
let sharedCubeGeometry: THREE.BoxGeometry;
let sharedBaseGeometry: THREE.BoxGeometry;
@@ -36,6 +38,18 @@ export function CubeScene() {
let frameCount = 0;
const [ids, setIds] = createSignal<string[]>([]);
const [positionMode, setPositionMode] = createSignal<"grid" | "circle">(
"circle",
);
const [nextPosition, setNextPosition] = createSignal<THREE.Vector3 | null>(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 [selectedIds, setSelectedIds] = createSignal<Set<string>>(new Set());
const [deletingIds, setDeletingIds] = createSignal<Set<string>>(new Set());
const [creatingIds, setCreatingIds] = createSignal<Set<string>>(new Set());
@@ -53,65 +67,51 @@ export function CubeScene() {
const GRID_SIZE = 2;
const CUBE_SPACING = 2;
const BASE_SIZE = 1; // Height of the cube above the ground
const CUBE_SIZE = BASE_SIZE / 1.2; // Height of the cube above the ground
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 BASE_COLOR = 0x9cbcff;
const BASE_EMISSIVE = 0x0c0c0c;
// Calculate grid position for a cube index with floating effect
// function getGridPosition(index: number): [number, number, number] {
// const x =
// (index % GRID_SIZE) * CUBE_SPACING - (GRID_SIZE * CUBE_SPACING) / 2;
// const z =
// Math.floor(index / GRID_SIZE) * CUBE_SPACING -
// (GRID_SIZE * CUBE_SPACING) / 2;
// return [x, 0.5, z];
// }
// function getGridPosition(index: number): [number, number, number] {
// if (index === 0) return [0, 0.5, 0];
function getGridPosition(
id: string,
index: number,
total: number,
): [number, number, number] {
// TODO: Detect collision with other cubes
const pos = positionMap.get(id);
if (pos) {
return pos.toArray() as [number, number, number];
}
const nextPos = nextPosition();
if (!nextPos) {
// Use next position if available
throw new Error("Next position is not set");
}
// let x = 0, z = 0;
// let layer = 1;
// let value = 1;
// while (true) {
// // right
// for (let i = 0; i < layer; i++) {
// x += 1;
// if (value++ === index) return [x * CUBE_SPACING, 0.5, z * CUBE_SPACING];
// }
// // down
// for (let i = 0; i < layer; i++) {
// z += 1;
// if (value++ === index) return [x * CUBE_SPACING, 0.5, z * CUBE_SPACING];
// }
// layer++;
// // left
// for (let i = 0; i < layer; i++) {
// x -= 1;
// if (value++ === index) return [x * CUBE_SPACING, 0.5, z * CUBE_SPACING];
// }
// // up
// for (let i = 0; i < layer; i++) {
// z -= 1;
// if (value++ === index) return [x * CUBE_SPACING, 0.5, z * CUBE_SPACING];
// }
// layer++;
// if (layer > 100) {
// console.warn("Exceeded grid size, returning last position");
// // If we exceed the index, return the last position
// return [x * CUBE_SPACING, 0.5, z * CUBE_SPACING];
// }
// }
// }
const next = nextPos.toArray() as [number, number, number];
positionMap.set(id, new THREE.Vector3(...next));
return next;
}
// Circle IDEA:
// Need to talk with timo and W about this
function getCirclePosition(
_id: string,
index: number,
total: number,
): [number, number, number] {
const r = Math.sqrt(total) * CUBE_SPACING; // Radius based on total cubes
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, 0.5, z];
return [x, CUBE_Y, z];
}
// Reactive cubes memo - this recalculates whenever ids() changes
@@ -123,25 +123,32 @@ 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;
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 = getPosition(
id,
isDeleting ? -1 : activeIndex >= 0 ? activeIndex : index,
currentIds.length);
const targetPosition =
activeIndex >= 0
? getPosition(id, activeIndex, currentIds.length)
: getPosition(id, index, currentIds.length);
return {
id,
position: getCirclePosition(
isDeleting ? -1 : activeIndex >= 0 ? activeIndex : index,
currentIds.length,
),
// position: getGridPosition(isDeleting ? -1 : activeIndex >= 0 ? activeIndex : index),
position,
isDeleting,
isCreating,
// targetPosition: activeIndex >= 0 ? getGridPosition(activeIndex) : getGridPosition(index),
targetPosition:
activeIndex >= 0
? getCirclePosition(activeIndex, currentIds.length)
: getCirclePosition(index, currentIds.length),
targetPosition,
};
});
});
@@ -149,26 +156,24 @@ export function CubeScene() {
// Create multi-colored cube materials for different faces
function createCubeMaterials() {
const materials = [
new THREE.MeshBasicMaterial({ color: 0xb0c0c2 }), // Right face - medium
new THREE.MeshBasicMaterial({ color: 0x4d6a6b }), // Left face - dark shadow
new THREE.MeshLambertMaterial({ color: 0xb0c0cf, emissive: 0x303030 }), // Right face - medium
new THREE.MeshLambertMaterial({ color: 0xb0c0cf, emissive: 0x303030 }), // Left face - dark shadow
new THREE.MeshBasicMaterial({ color: 0xdce4e5 }), // Top face - light
new THREE.MeshBasicMaterial({ color: 0x4d6a6b }), // Bottom face - dark shadow
new THREE.MeshBasicMaterial({ color: 0xb0c0c2 }), // Front face - medium
new THREE.MeshBasicMaterial({ color: 0x4d6a6b }), // Back face - dark shadow
new THREE.MeshLambertMaterial({ color: 0xb0c0cf, emissive: 0x303030 }), // Bottom face - dark shadow
new THREE.MeshLambertMaterial({ color: 0xb0c0cf, emissive: 0x303030 }), // Front face - medium
new THREE.MeshLambertMaterial({ color: 0xb0c0cf, emissive: 0x303030 }), // Back face - dark shadow
];
return materials;
}
function createBaseMaterials() {
const materials = [
new THREE.MeshBasicMaterial({ color: 0xdce4e5 }), // Right face - medium
new THREE.MeshBasicMaterial({ color: 0xa4b3b5 }), // Left face - dark shadow
new THREE.MeshLambertMaterial({ color: 0xffffff, emissive: 0x303030 }), // Top face - light
new THREE.MeshBasicMaterial({ color: 0xa4b3b5 }), // Bottom face - dark shadow
new THREE.MeshBasicMaterial({ color: 0xdce4e5 }), // Front face - medium
new THREE.MeshBasicMaterial({ color: 0xa4b3b5 }), // Back face - dark shadow
];
return materials;
function createBaseMaterials(opacity: number) {
return new THREE.MeshLambertMaterial({
color: BASE_COLOR,
emissive: BASE_EMISSIVE,
flatShading: true,
transparent: true,
opacity,
});
}
// Animation helper function
@@ -319,11 +324,42 @@ export function CubeScene() {
animate();
}
function createCubeBase(cube_pos: [number, number, number]) {
const baseMaterials = createBaseMaterials();
const base = new THREE.Mesh(sharedBaseGeometry, baseMaterials);
// 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,
) {
const baseMaterial = createBaseMaterials(opacity);
const base = new THREE.Mesh(sharedBaseGeometry, baseMaterial);
// 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.position.set(cube_pos[0], BASE_HEIGHT / 2, cube_pos[2]); // Position Y=0 on the floor
base.receiveShadow = true;
return base;
}
@@ -397,22 +433,17 @@ export function CubeScene() {
function updateMeshColors() {
for (const [id, base] of baseMap.entries()) {
const selected = selectedIds().has(id);
const materials = base.material as THREE.Material[];
const material = base.material as THREE.MeshLambertMaterial;
if (selected) {
// When selected, make all faces red-ish but maintain the lighting difference
materials.forEach((material, index) => {
if (index === 2) {
(material as THREE.MeshBasicMaterial).color.set(0xff6666);
}
});
material.color.set(0xffffff);
material.emissive.set(0xff6666);
} else {
// Normal colors - restore original face colors
materials.forEach((material, index) => {
if (index === 2) {
(material as THREE.MeshBasicMaterial).color.set(0xffffff);
}
});
material.color.set(BASE_COLOR);
material.emissive.set(BASE_EMISSIVE);
}
}
}
@@ -429,12 +460,61 @@ export function CubeScene() {
}
}
const initialCameraPosition = { x: 2.8, y: 3.6, z: -2 };
const initialSphericalCameraPosition = new THREE.Spherical();
initialSphericalCameraPosition.setFromVector3(
new THREE.Vector3(
initialCameraPosition.x,
initialCameraPosition.y,
initialCameraPosition.z,
),
);
let initBase: THREE.Mesh;
const grid = new THREE.GridHelper(1000, 1000 / 1, 0xE1EDEF, 0xE1EDEF);
onMount(() => {
// Scene setup
scene = new THREE.Scene();
scene.fog = new THREE.Fog(0xffffff, 10, 50); //
// Transparent background
scene.background = null;
// 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 bgMaterial = new THREE.ShaderMaterial({
uniforms,
vertexShader: `
void main() {
gl_Position = vec4(position, 1.0);
}
`,
fragmentShader: `
uniform vec3 colorTop;
uniform vec3 colorBottom;
uniform vec2 resolution;
void main() {
float y = gl_FragCoord.y / resolution.y;
gl_FragColor = vec4(mix(colorBottom, colorTop, y), 1.0);
}
`,
depthTest: false,
depthWrite: false,
});
// Create fullscreen quad geometry
const bgGeometry = new THREE.PlaneGeometry(2, 2);
const bgMesh = new THREE.Mesh(bgGeometry, bgMaterial);
bgScene.add(bgMesh);
// Camera setup
camera = new THREE.PerspectiveCamera(
75,
@@ -442,7 +522,7 @@ export function CubeScene() {
0.1,
1000,
);
camera.position.set(11, 8, -11);
camera.position.setFromSpherical(initialSphericalCameraPosition);
camera.lookAt(0, 0, 0);
// Renderer setup
@@ -453,14 +533,20 @@ export function CubeScene() {
container.appendChild(renderer.domElement);
// Lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 1.0); // Bright
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); // Bright
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
// Position light at 30 degree angle (30 degrees from vertical)
// For 30 degree angle: tan(30°) = opposite/adjacent = x/y
// If y = 100, then x = 100 * tan(30°) = 100 * 0.577 = 57.7
directionalLight.position.set(57.7, 100, 57.7);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.9);
const lightPos = new THREE.Spherical(
100,
initialSphericalCameraPosition.phi,
initialSphericalCameraPosition.theta,
);
lightPos.theta = initialSphericalCameraPosition.theta - Math.PI / 2; // 90 degrees offset
directionalLight.position.setFromSpherical(lightPos);
// initialSphericalCameraPosition
directionalLight.castShadow = true;
// Configure shadow camera for hard, crisp shadows
@@ -477,31 +563,41 @@ export function CubeScene() {
scene.add(directionalLight);
// Floor/Ground - Make it invisible but keep it for reference
const floorGeometry = new THREE.PlaneGeometry(50, 50);
const floorGeometry = new THREE.PlaneGeometry(1000, 1000);
const floorMaterial = new THREE.MeshBasicMaterial({
color: 0xcccccc,
color: FLOOR_COLOR,
transparent: true,
opacity: 0, // Make completely invisible
visible: false, // Also hide it completely
opacity: 0.1, // Make completely invisible
// visible: false, // Also hide it completely
});
const 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);
// grid.material.opacity = 0.2;
// 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.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(1, 1, 1);
sharedBaseGeometry = new THREE.BoxGeometry(1.2, 0.05, 1.2);
sharedCubeGeometry = new THREE.BoxGeometry(CUBE_SIZE, CUBE_SIZE, CUBE_SIZE);
sharedBaseGeometry = new THREE.BoxGeometry(BASE_SIZE, BASE_HEIGHT, BASE_SIZE);
// Basic OrbitControls implementation (simplified)
let isDragging = false;
let previousMousePosition = { x: 0, y: 0 };
const spherical = new THREE.Spherical();
spherical.setFromVector3(camera.position);
// const spherical = new THREE.Spherical();
// spherical.setFromVector3(camera.position);
// Function to update camera info
const updateCameraInfo = () => {
const spherical = new THREE.Spherical();
spherical.setFromVector3(camera.position);
setCameraInfo({
position: {
x: Math.round(camera.position.x * 100) / 100,
@@ -529,28 +625,96 @@ export function CubeScene() {
};
const onMouseMove = (event: MouseEvent) => {
if (worldMode() === "create") {
if (isDragging) return;
const rect = renderer.domElement.getBoundingClientRect();
const mouse = new THREE.Vector2(
((event.clientX - rect.left) / rect.width) * 2 - 1,
-((event.clientY - rect.top) / rect.height) * 2 + 1,
);
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObject(floor);
if (intersects.length > 0) {
const point = intersects[0].point;
// Snap to grid
const snapped = new THREE.Vector3(
Math.round(point.x / GRID_SIZE) * GRID_SIZE,
CUBE_Y,
Math.round(point.z / GRID_SIZE) * GRID_SIZE,
);
if(!initBase) {
// Create initial base mesh if it doesn't exist
initBase = createCubeBase([snapped.x, 0.0, snapped.z], 0.5);
} else {
initBase.position.set(snapped.x, BASE_HEIGHT / 2, snapped.z);
}
scene.remove(initBase); // Remove any existing base mesh
scene.add(initBase);
setNextPosition(snapped); // Update next position for cube creation
}
// If in create mode, don't allow camera movement
return;
}
if (!isDragging) return;
const deltaX = event.clientX - previousMousePosition.x;
const deltaY = event.clientY - previousMousePosition.y;
// const deltaY = event.clientY - previousMousePosition.y;
if(positionMode() === "circle") {
const spherical = new THREE.Spherical();
spherical.setFromVector3(camera.position);
spherical.theta -= deltaX * 0.01;
// spherical.phi += deltaY * 0.01;
// spherical.phi = Math.max(0.1, Math.min(Math.PI - 0.1, spherical.phi));
spherical.theta -= deltaX * 0.01;
spherical.phi += deltaY * 0.01;
spherical.phi = Math.max(0.1, Math.min(Math.PI - 0.1, spherical.phi));
const lightPos = new THREE.Spherical();
lightPos.setFromVector3(directionalLight.position);
lightPos.theta = spherical.theta - Math.PI / 2; // 90 degrees offset
directionalLight.position.setFromSpherical(lightPos);
camera.position.setFromSpherical(spherical);
camera.lookAt(0, 0, 0);
directionalLight.lookAt(0, 0, 0);
camera.position.setFromSpherical(spherical);
camera.lookAt(0, 0, 0);
}
else {
const movementSpeed = 0.015;
// Get camera direction vectors
const cameraDirection = new THREE.Vector3();
camera.getWorldDirection(cameraDirection);
cameraDirection.y = 0; // Ignore vertical direction
const cameraRight = new THREE.Vector3();
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)
setBackedCameraPosition({
pos: camera.position.clone(),
dir: camera.getWorldDirection(new THREE.Vector3()).clone(),
});
}
updateCameraInfo();
previousMousePosition = { x: event.clientX, y: event.clientY };
};
const onWheel = (event: WheelEvent) => {
const spherical = new THREE.Spherical();
spherical.setFromVector3(camera.position);
event.preventDefault();
spherical.radius += event.deltaY * 0.01;
spherical.radius = Math.max(5, Math.min(50, spherical.radius));
spherical.radius = Math.max(3, Math.min(10, spherical.radius)); // Clamp radius between 5 and 50
camera.position.setFromSpherical(spherical);
camera.lookAt(0, 0, 0);
// camera.lookAt(0, 0, 0);
updateCameraInfo();
};
@@ -563,9 +727,19 @@ export function CubeScene() {
// Raycaster for clicking
raycaster = new THREE.Raycaster();
// Click handler for cube selection
// Click handler:
// - Select/deselects a cube in "view" mode
// - Creates a new cube in "create" mode
const onClick = (event: MouseEvent) => {
if (isDragging) return; // Don't select if we were dragging
if(worldMode() === "create") {
if (initBase) {
scene.remove(initBase); // Remove the base mesh after adding cube
setWorldMode("view");
addCube();
}
return;
}
const rect = renderer.domElement.getBoundingClientRect();
const mouse = new THREE.Vector2(
@@ -581,9 +755,13 @@ export function CubeScene() {
if (intersects.length > 0) {
const id = intersects[0].object.userData.id;
toggleSelection(id);
} else {
setSelectedIds(new Set<string>()); // Clear selection if clicked outside cubes
}
};
const currentCubes = cubes();
renderer.domElement.addEventListener("click", onClick);
const animate = () => {
@@ -592,10 +770,13 @@ export function CubeScene() {
requestAnimationFrame(animate);
frameCount++;
renderer.autoClear = false;
renderer.render(bgScene, bgCamera); // Render background scene
renderer.render(scene, camera);
// Uncomment for memory debugging:
if (frameCount % 60 === 0) logMemoryUsage(); // Log every 60 frames
if (frameCount % 300 === 0) logMemoryUsage(); // Log every 60 frames
};
isAnimating = true;
animate();
@@ -619,12 +800,60 @@ export function CubeScene() {
renderer.domElement.removeEventListener("click", onClick);
window.removeEventListener("resize", handleResize);
if(initBase){
initBase.geometry.dispose();
// @ts-ignore: Not sure why this is needed
initBase.material.dispose();
}
if (container) {
container.innerHTML = "";
}
});
});
createEffect(() => {
if (!container) return;
if (worldMode() === "create") {
// Show the plus button when in create mode
container.style.cursor = "crosshair";
} else {
container.style.cursor = "pointer";
}
});
createEffect(
// Fly back and forth between circle and grid positions
// ? Do we want to do this.
// We could shift the center of the circle to the camera look at position
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");
});
}
}
})
);
// Effect to manage cube meshes - this runs whenever cubes() changes
createEffect(() => {
const currentCubes = cubes();
@@ -632,6 +861,8 @@ export function CubeScene() {
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);
@@ -649,7 +880,7 @@ export function CubeScene() {
meshMap.set(cube.id, mesh);
// Create new base mesh
const base = createCubeBase(cube.position);
const base = createCubeBase(cube.position, 1);
base.userData.id = cube.id;
scene.add(base);
baseMap.set(cube.id, base);
@@ -681,7 +912,7 @@ export function CubeScene() {
if (existingBase) {
animateToPosition(existingBase, [
target[0],
target[1] - 0.5 - 0.025,
target[1] - 0.5 - 0.025, // TODO: Refactor this DRY
target[2],
]);
}
@@ -798,16 +1029,57 @@ export function CubeScene() {
renderer?.dispose();
});
const onHover = (inside: boolean) => (event: MouseEvent) => {
// Hover over the plus button, shows a preview of the base mesh
const currentCubes = cubes();
if (currentCubes.length > 0) {
return;
}
if (!initBase) {
// Create initial base mesh if it doesn't exist
initBase = createCubeBase([0, 0.5, 0], 0.5);
}
if (inside) {
scene.add(initBase);
} else {
scene.remove(initBase);
}
};
const onAddClick = (event: MouseEvent) => {
setPositionMode("grid");
setWorldMode("create");
};
return (
<div>
<div style={{ "margin-bottom": "10px" }}>
<button onClick={addCube}>Add Cube</button>
<button
onClick={onAddClick}
onMouseEnter={onHover(true)}
onMouseLeave={onHover(false)}
>
Add Cube
</button>
<button onClick={() => deleteSelectedCubes(selectedIds())}>
Delete Selected
</button>
<button onClick={() => setPositionMode("grid")}>
Grid Positioning
</button>
<button onClick={() => setPositionMode("circle")}>
Circle Positioning
</button>
<span style={{ "margin-left": "10px" }}>
Selected: {selectedIds().size} cubes | Total: {ids().length} cubes
</span>
<span>
{" | "}
World Mode: {worldMode()}
{" | "}
Position Mode: {positionMode()}
</span>
</div>
{/* Camera Information Display */}
@@ -839,9 +1111,8 @@ export function CubeScene() {
ref={(el) => (container = el)}
style={{
width: "100%",
height: "1000px",
border: "1px solid #ccc",
cursor: "grab",
height: "90vh",
cursor: "pointer",
}}
/>
</div>