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(null); const canvasRef = useRef(null); const cameraRef = useRef(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 (
) }