164 lines
4.8 KiB
TypeScript
164 lines
4.8 KiB
TypeScript
import { useEffect, useRef } from "react";
|
|
import * as THREE from 'three';
|
|
import { useInteraction, type InteractionMouseMoveEventArgs } from "../helpers/hooks/useInteration";
|
|
import { db } from "../backend/db";
|
|
import { HitTestFactory, type HitTest } from "../helpers/hitTest";
|
|
import { model } from "../model/model";
|
|
import { Point3dHelper } from "../helpers/point3dHelper";
|
|
import { SceneHelper } from "../helpers/sceneHelper";
|
|
|
|
export type ThreeViewEventArgs = {
|
|
camera: THREE.Camera,
|
|
scene: THREE.Scene,
|
|
}
|
|
|
|
export type ThreeViewTickEventArgs = ThreeViewEventArgs & {
|
|
deltaTime: number,
|
|
absoluteTime: number,
|
|
}
|
|
|
|
export type ThreeViewMouseMoveEventArgs = ThreeViewEventArgs & {
|
|
hitTest: HitTest,
|
|
}
|
|
|
|
export type ThreeViewProps = {
|
|
sceneHelper: SceneHelper,
|
|
onTick?: (event: ThreeViewTickEventArgs) => void,
|
|
onMouseMove?: (event: ThreeViewMouseMoveEventArgs) => void,
|
|
onDispose: (event: ThreeViewEventArgs) => void,
|
|
}
|
|
|
|
function setupScene(size: { w: number, h: number }): { scene: THREE.Scene, camera: THREE.PerspectiveCamera } {
|
|
// --- Scene & Camera ---
|
|
const scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0x0a0a12);
|
|
|
|
const camera = new THREE.PerspectiveCamera(55, size.w / size.h, 0.1, 100);
|
|
camera.position.set(4, 3, 6);
|
|
|
|
// --- Lights ---
|
|
scene.add(new THREE.AmbientLight(0xffffff, 0.3));
|
|
|
|
const dir = new THREE.DirectionalLight(0xffffff, 1.2);
|
|
dir.position.set(5, 8, 5);
|
|
dir.castShadow = true;
|
|
scene.add(dir);
|
|
|
|
const pt = new THREE.PointLight(0x5588ff, 1.5, 20);
|
|
pt.position.set(-3, 2, -3);
|
|
scene.add(pt);
|
|
|
|
// --- Floor & Grid ---
|
|
const plane = new THREE.Mesh(
|
|
new THREE.PlaneGeometry(14, 14),
|
|
new THREE.MeshStandardMaterial({ color: 0x080810 })
|
|
);
|
|
plane.rotation.x = -Math.PI / 2;
|
|
plane.receiveShadow = true;
|
|
// scene.add(plane);
|
|
// scene.add(new THREE.GridHelper(14, 14, 0x222233, 0x111122));
|
|
|
|
return { scene, camera };
|
|
}
|
|
|
|
export const ThreeView = function (props: ThreeViewProps) {
|
|
|
|
const viewportRef = useRef<HTMLDivElement | null>(null);
|
|
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
|
|
|
|
let handleHover: (e: InteractionMouseMoveEventArgs) => void;
|
|
|
|
useEffect(() => {
|
|
|
|
model.initFromBlob(db.saveToBlob());
|
|
|
|
const container = viewportRef.current!;
|
|
const W = container.clientWidth;
|
|
const H = container.clientHeight;
|
|
|
|
// --- Renderer ---
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
renderer.setSize(W, H);
|
|
renderer.shadowMap.enabled = true;
|
|
container.appendChild(renderer.domElement);
|
|
|
|
canvasRef.current = renderer.domElement;
|
|
|
|
const { scene, camera } = setupScene({ w: W, h: H });
|
|
cameraRef.current = camera;
|
|
|
|
props.sceneHelper.initialize(scene);
|
|
|
|
const handleWindowResize = () => {
|
|
const w = container.clientWidth;
|
|
const h = container.clientHeight;
|
|
camera.aspect = w / h;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(w, h);
|
|
};
|
|
window.addEventListener("resize", handleWindowResize);
|
|
|
|
handleHover = (e: InteractionMouseMoveEventArgs) => {
|
|
const hitTest = HitTestFactory.hitTest(
|
|
props.sceneHelper,
|
|
new THREE.Vector2(e.position.x, e.position.y),
|
|
camera,
|
|
{ tolerancePixels: 3, cameraPixelSize: e.pixelSize }
|
|
);
|
|
props.onMouseMove?.({
|
|
camera,
|
|
scene,
|
|
hitTest,
|
|
});
|
|
};
|
|
|
|
// --- Animation loop ---
|
|
let lastTime = performance.now();
|
|
let animId: number;
|
|
function animate(time: DOMHighResTimeStamp) {
|
|
animId = requestAnimationFrame(animate);
|
|
const deltaTime = lastTime ? time - lastTime : 0;
|
|
lastTime = time;
|
|
props.onTick?.({
|
|
camera,
|
|
scene,
|
|
deltaTime,
|
|
absoluteTime: time,
|
|
});
|
|
renderer.render(scene, camera);
|
|
}
|
|
animId = requestAnimationFrame(animate);
|
|
|
|
// --- Cleanup ---
|
|
return () => {
|
|
if (animId)
|
|
cancelAnimationFrame(animId);
|
|
|
|
container.removeChild(renderer.domElement);
|
|
|
|
window.removeEventListener("resize", handleWindowResize);
|
|
|
|
renderer.dispose();
|
|
|
|
props.onDispose({
|
|
camera,
|
|
scene,
|
|
});
|
|
|
|
props.sceneHelper.dispose();
|
|
};
|
|
}, []);
|
|
|
|
useInteraction(canvasRef, cameraRef, {
|
|
onMouseMove: (e) => handleHover?.(e),
|
|
});
|
|
|
|
return (
|
|
<div id="viewport" ref={viewportRef}>
|
|
|
|
</div>
|
|
)
|
|
}
|