UI/cubes: group logic to add more meshed
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user