Changes
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Suspense, useRef, useMemo, useEffect } from 'react';
|
||||
import { Canvas, useFrame } from '@react-three/fiber';
|
||||
import { OrbitControls, useGLTF, Grid, Html } from '@react-three/drei';
|
||||
import { Suspense, useRef, useMemo, useEffect, useCallback } from 'react';
|
||||
import { Canvas, useThree, useFrame } from '@react-three/fiber';
|
||||
import { OrbitControls, useGLTF, Grid, Html, Line } from '@react-three/drei';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import * as THREE from 'three';
|
||||
import { useModelStore } from '@/stores/useModelStore';
|
||||
@@ -37,15 +37,12 @@ function GLBModel({ url }: { url: string }) {
|
||||
return { size, center };
|
||||
}, [scene]);
|
||||
|
||||
// Apply opacity, wireframe, and checklist color feedback
|
||||
useEffect(() => {
|
||||
// Determine overall inspection color
|
||||
const hasRejected = checklist.some(i => i.status === 'rejected');
|
||||
const allApproved = checklist.every(i => i.status === 'approved');
|
||||
|
||||
scene.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
// Clone material to avoid shared mutations
|
||||
if (Array.isArray(child.material)) {
|
||||
child.material = child.material.map(m => m.clone());
|
||||
} else {
|
||||
@@ -58,13 +55,12 @@ function GLBModel({ url }: { url: string }) {
|
||||
mat.opacity = opacity;
|
||||
mat.wireframe = renderMode === 'wireframe';
|
||||
mat.needsUpdate = true;
|
||||
// Color feedback
|
||||
if (hasRejected) {
|
||||
mat.color.setHSL(0, 0.7, 0.5); // red
|
||||
mat.color.setHSL(0, 0.7, 0.5);
|
||||
} else if (allApproved) {
|
||||
mat.color.setHSL(0.38, 0.7, 0.45); // green
|
||||
mat.color.setHSL(0.38, 0.7, 0.45);
|
||||
} else {
|
||||
mat.color.set(0x8899aa); // default steel
|
||||
mat.color.set(0x8899aa);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -89,6 +85,109 @@ function GLBModel({ url }: { url: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
/** Sphere marker at a 3D point */
|
||||
function PointMarker({ position, color = '#e8a838' }: { position: [number, number, number]; color?: string }) {
|
||||
return (
|
||||
<mesh position={position}>
|
||||
<sphereGeometry args={[0.003, 16, 16]} />
|
||||
<meshBasicMaterial color={color} />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
/** Renders all measurements and pending points */
|
||||
function MeasurementOverlay() {
|
||||
const measurements = useModelStore((s) => s.measurements);
|
||||
const measurePoints = useModelStore((s) => s.measurePoints);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Pending first point */}
|
||||
{measurePoints.length === 1 && (
|
||||
<PointMarker position={[measurePoints[0].x, measurePoints[0].y, measurePoints[0].z]} color="#e8a838" />
|
||||
)}
|
||||
|
||||
{/* Completed measurements */}
|
||||
{measurements.map((m) => {
|
||||
const a: [number, number, number] = [m.pointA.x, m.pointA.y, m.pointA.z];
|
||||
const b: [number, number, number] = [m.pointB.x, m.pointB.y, m.pointB.z];
|
||||
const mid: [number, number, number] = [
|
||||
(a[0] + b[0]) / 2,
|
||||
(a[1] + b[1]) / 2,
|
||||
(a[2] + b[2]) / 2,
|
||||
];
|
||||
|
||||
return (
|
||||
<group key={m.id}>
|
||||
<PointMarker position={a} color="#22c55e" />
|
||||
<PointMarker position={b} color="#22c55e" />
|
||||
<Line
|
||||
points={[a, b]}
|
||||
color="#22c55e"
|
||||
lineWidth={2}
|
||||
/>
|
||||
<Html position={mid} center distanceFactor={1} style={{ pointerEvents: 'none' }}>
|
||||
<div className="rounded bg-card/95 border border-success/50 px-2 py-0.5 shadow-lg backdrop-blur-sm">
|
||||
<span className="font-mono text-[11px] font-bold text-success whitespace-nowrap">
|
||||
{m.distanceMM.toFixed(1)} mm
|
||||
</span>
|
||||
</div>
|
||||
</Html>
|
||||
</group>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/** Raycasting click handler for measurement mode */
|
||||
function MeasureClickHandler() {
|
||||
const { camera, scene, gl } = useThree();
|
||||
const measureMode = useModelStore((s) => s.measureMode);
|
||||
const addMeasurePoint = useModelStore((s) => s.addMeasurePoint);
|
||||
const raycaster = useMemo(() => new THREE.Raycaster(), []);
|
||||
const mouse = useMemo(() => new THREE.Vector2(), []);
|
||||
|
||||
const handleClick = useCallback((event: MouseEvent) => {
|
||||
if (!measureMode) return;
|
||||
|
||||
const rect = gl.domElement.getBoundingClientRect();
|
||||
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
|
||||
raycaster.setFromCamera(mouse, camera);
|
||||
const intersects = raycaster.intersectObjects(scene.children, true);
|
||||
|
||||
// Filter out measurement markers and grid
|
||||
const hit = intersects.find(i => {
|
||||
const obj = i.object;
|
||||
if (obj instanceof THREE.GridHelper) return false;
|
||||
if (obj instanceof THREE.Mesh) {
|
||||
if (obj.geometry instanceof THREE.SphereGeometry) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (hit) {
|
||||
addMeasurePoint({ x: hit.point.x, y: hit.point.y, z: hit.point.z });
|
||||
}
|
||||
}, [measureMode, addMeasurePoint, camera, scene, gl, raycaster, mouse]);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = gl.domElement;
|
||||
canvas.addEventListener('click', handleClick);
|
||||
return () => canvas.removeEventListener('click', handleClick);
|
||||
}, [gl, handleClick]);
|
||||
|
||||
// Change cursor when in measure mode
|
||||
useEffect(() => {
|
||||
gl.domElement.style.cursor = measureMode ? 'crosshair' : 'grab';
|
||||
return () => { gl.domElement.style.cursor = 'grab'; };
|
||||
}, [measureMode, gl]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function ModelViewerCanvas({ url }: ModelViewerProps) {
|
||||
return (
|
||||
<Canvas
|
||||
@@ -97,9 +196,7 @@ export function ModelViewerCanvas({ url }: ModelViewerProps) {
|
||||
frameloop="demand"
|
||||
className="!bg-background"
|
||||
onCreated={({ gl, invalidate }) => {
|
||||
// Target 72fps for Quest 3 compatibility
|
||||
gl.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
|
||||
// Continuous render for smooth interaction
|
||||
const loop = () => { invalidate(); requestAnimationFrame(loop); };
|
||||
loop();
|
||||
}}
|
||||
@@ -112,6 +209,9 @@ export function ModelViewerCanvas({ url }: ModelViewerProps) {
|
||||
<GLBModel url={url} />
|
||||
</Suspense>
|
||||
|
||||
<MeasurementOverlay />
|
||||
<MeasureClickHandler />
|
||||
|
||||
<Grid
|
||||
infiniteGrid
|
||||
cellSize={0.01}
|
||||
|
||||
Reference in New Issue
Block a user