CAD/client/src/components/ThreeVIew.tsx

187 lines
5.6 KiB
TypeScript

import { useEffect, useRef } from "react";
import * as THREE from 'three';
import { useInteraction, type InteractionMouseEventArgs } from "../helpers/hooks/useInteration";
import { db } from "../backend/db";
import { HitTestFactory, type HitTest } from "../helpers/hitTest";
import { model } from "../model/model";
import { SceneHelper } from "../helpers/sceneHelper";
export type ThreeViewEventArgs = {
camera: THREE.Camera,
scene: THREE.Scene,
}
export type ThreeViewTickEventArgs = ThreeViewEventArgs & {
deltaTime: number,
absoluteTime: number,
}
export type ThreeViewMouseEventArgs = ThreeViewEventArgs & {
hitTest: HitTest,
}
export type ThreeViewProps = {
sceneHelper: SceneHelper,
onTick?: (event: ThreeViewTickEventArgs) => void,
onClick?: (event: ThreeViewMouseEventArgs) => void,
onMouseMove?: (event: ThreeViewMouseEventArgs) => 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 handleClick: (e: InteractionMouseEventArgs) => void;
let handleHover: (e: InteractionMouseEventArgs) => 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, camera);
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: InteractionMouseEventArgs) => {
const ht = props.sceneHelper.hitTest(
e.position,
e.screenSize,
);
console.log(JSON.stringify(ht.map((h) => h.object.userData)));
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,
});
};
handleClick = (e: InteractionMouseEventArgs) => {
const hitTest = HitTestFactory.hitTest(
props.sceneHelper,
new THREE.Vector2(e.position.x, e.position.y),
camera,
{ tolerancePixels: 3, cameraPixelSize: e.pixelSize }
);
props.onClick?.({
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),
onMouseClick: (e) => handleClick?.(e),
});
return (
<div id="viewport" ref={viewportRef}>
</div>
)
}