UI/cubes: group logic to add more meshed

This commit is contained in:
Johannes Kirschbauer
2025-07-16 11:03:40 +02:00
parent 579885a6e2
commit 8c7e93c92e

View File

@@ -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<string, THREE.Mesh>();
const baseMap = new Map<string, THREE.Mesh>(); // Map for cube bases
const groupMap = new Map<string, THREE.Group>();
const positionMap = new Map<string, THREE.Vector3>();
let sharedCubeGeometry: THREE.BoxGeometry;
@@ -41,14 +77,15 @@ export function CubeScene() {
const [positionMode, setPositionMode] = createSignal<"grid" | "circle">(
"circle",
);
const [nextPosition, setNextPosition] = createSignal<THREE.Vector3 | null>(null);
const [nextBasePosition, 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 [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());
@@ -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<string>()); // 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<string>()); // 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 (
<div>
<div style="width: 100%; height: 100%; position: relative;">
<div style={{ "margin-bottom": "10px" }}>
<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>
@@ -1082,6 +1009,15 @@ export function CubeScene() {
</span>
</div>
<div
ref={(el) => (container = el)}
style={{
width: "100%",
height: "80vh",
cursor: "pointer",
}}
/>
{/* Camera Information Display */}
<div
style={{
@@ -1094,6 +1030,33 @@ export function CubeScene() {
border: "1px solid #ddd",
}}
>
<div
style={{
position: "absolute",
bottom: "70px",
left: "0",
display: "flex",
"flex-direction": "row",
width: "100%",
}}
>
<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>
</div>
<div>
<strong>Camera Info:</strong>
</div>
@@ -1106,15 +1069,6 @@ export function CubeScene() {
{cameraInfo().spherical.theta}, φ={cameraInfo().spherical.phi}
</div>
</div>
<div
ref={(el) => (container = el)}
style={{
width: "100%",
height: "90vh",
cursor: "pointer",
}}
/>
</div>
);
}