cubes: scene extend
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user