Changes
This commit is contained in:
@@ -4,6 +4,7 @@ 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';
|
||||
import { findNearestVertex, detectHoleAtFace, findNearestEdgeSegment } from './SmartMeasure';
|
||||
|
||||
interface ModelViewerProps {
|
||||
url: string;
|
||||
@@ -140,13 +141,43 @@ function PointMarker({ position, color = '#e8a838' }: { position: [number, numbe
|
||||
);
|
||||
}
|
||||
|
||||
/** Renders all measurements and pending points */
|
||||
/** Snap ring indicator */
|
||||
function SnapRing({ position }: { position: [number, number, number] }) {
|
||||
return (
|
||||
<mesh position={position} rotation={[-Math.PI / 2, 0, 0]}>
|
||||
<ringGeometry args={[0.003, 0.005, 24]} />
|
||||
<meshBasicMaterial color="#eab308" side={THREE.DoubleSide} />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
/** Renders all measurements, snap point, and hover info */
|
||||
function MeasurementOverlay() {
|
||||
const measurements = useModelStore((s) => s.measurements);
|
||||
const measurePoints = useModelStore((s) => s.measurePoints);
|
||||
const snapPoint = useModelStore((s) => s.snapPoint);
|
||||
const hoverInfo = useModelStore((s) => s.hoverInfo);
|
||||
const measureMode = useModelStore((s) => s.measureMode);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Snap indicator */}
|
||||
{measureMode && snapPoint && (
|
||||
<SnapRing position={[snapPoint.x, snapPoint.y, snapPoint.z]} />
|
||||
)}
|
||||
|
||||
{/* Hover auto-detect tooltip */}
|
||||
{hoverInfo && (
|
||||
<Html position={hoverInfo.position} center distanceFactor={1} style={{ pointerEvents: 'none' }}>
|
||||
<div className="rounded bg-card/95 border border-primary/50 px-2 py-0.5 shadow-lg backdrop-blur-sm">
|
||||
<span className="font-mono text-[11px] font-bold text-primary whitespace-nowrap">
|
||||
{hoverInfo.type === 'hole' ? '⌀ ' : ''}
|
||||
{hoverInfo.value.toFixed(1)} mm
|
||||
</span>
|
||||
</div>
|
||||
</Html>
|
||||
)}
|
||||
|
||||
{/* Pending first point */}
|
||||
{measurePoints.length === 1 && (
|
||||
<PointMarker position={[measurePoints[0].x, measurePoints[0].y, measurePoints[0].z]} color="#e8a838" />
|
||||
@@ -185,17 +216,180 @@ function MeasurementOverlay() {
|
||||
);
|
||||
}
|
||||
|
||||
/** Raycasting click handler for measurement mode */
|
||||
/** Smart vertex snap on pointer move */
|
||||
function SmartSnapHandler() {
|
||||
const { camera, scene, gl } = useThree();
|
||||
const measureMode = useModelStore((s) => s.measureMode);
|
||||
const setSnapPoint = useModelStore((s) => s.setSnapPoint);
|
||||
const raycaster = useMemo(() => new THREE.Raycaster(), []);
|
||||
const mouse = useMemo(() => new THREE.Vector2(), []);
|
||||
const mouseRef = useRef({ x: 0, y: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
const onMove = (e: MouseEvent) => {
|
||||
const rect = gl.domElement.getBoundingClientRect();
|
||||
mouseRef.current.x = e.clientX - rect.left;
|
||||
mouseRef.current.y = e.clientY - rect.top;
|
||||
mouse.x = (mouseRef.current.x / rect.width) * 2 - 1;
|
||||
mouse.y = -(mouseRef.current.y / rect.height) * 2 + 1;
|
||||
};
|
||||
gl.domElement.addEventListener('pointermove', onMove);
|
||||
return () => gl.domElement.removeEventListener('pointermove', onMove);
|
||||
}, [gl, mouse]);
|
||||
|
||||
useFrame(() => {
|
||||
if (!measureMode) {
|
||||
setSnapPoint(null);
|
||||
return;
|
||||
}
|
||||
|
||||
raycaster.setFromCamera(mouse, camera);
|
||||
const intersects = raycaster.intersectObjects(scene.children, true);
|
||||
const hit = intersects.find(i => {
|
||||
const obj = i.object;
|
||||
if (obj instanceof THREE.GridHelper) return false;
|
||||
if (obj instanceof THREE.Mesh && obj.geometry instanceof THREE.SphereGeometry) return false;
|
||||
if (obj instanceof THREE.Mesh && obj.geometry instanceof THREE.RingGeometry) return false;
|
||||
if (obj.userData.__edgeLine) return false;
|
||||
return obj instanceof THREE.Mesh;
|
||||
});
|
||||
|
||||
if (hit && hit.object instanceof THREE.Mesh) {
|
||||
const canvas = gl.domElement;
|
||||
const snap = findNearestVertex(
|
||||
hit.object, hit.point, camera,
|
||||
{ width: canvas.clientWidth, height: canvas.clientHeight },
|
||||
10
|
||||
);
|
||||
if (snap) {
|
||||
setSnapPoint({ x: snap.x, y: snap.y, z: snap.z });
|
||||
} else {
|
||||
setSnapPoint(null);
|
||||
}
|
||||
} else {
|
||||
setSnapPoint(null);
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Hover detector for auto-detect hole diameter and edge length */
|
||||
function HoverDetector() {
|
||||
const { camera, scene, gl } = useThree();
|
||||
const measureMode = useModelStore((s) => s.measureMode);
|
||||
const setHoverInfo = useModelStore((s) => s.setHoverInfo);
|
||||
const raycaster = useMemo(() => new THREE.Raycaster(), []);
|
||||
const mouse = useMemo(() => new THREE.Vector2(), []);
|
||||
const hoverTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const lastHitKey = useRef('');
|
||||
|
||||
useEffect(() => {
|
||||
if (measureMode) {
|
||||
setHoverInfo(null);
|
||||
if (hoverTimer.current) clearTimeout(hoverTimer.current);
|
||||
return;
|
||||
}
|
||||
|
||||
const onMove = (e: MouseEvent) => {
|
||||
const rect = gl.domElement.getBoundingClientRect();
|
||||
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
|
||||
raycaster.setFromCamera(mouse, camera);
|
||||
const intersects = raycaster.intersectObjects(scene.children, true);
|
||||
const hit = intersects.find(i => {
|
||||
const obj = i.object;
|
||||
if (obj instanceof THREE.GridHelper) return false;
|
||||
if (obj instanceof THREE.Mesh && obj.geometry instanceof THREE.SphereGeometry) return false;
|
||||
if (obj instanceof THREE.Mesh && obj.geometry instanceof THREE.RingGeometry) return false;
|
||||
if (obj.userData.__edgeLine) return false;
|
||||
return obj instanceof THREE.Mesh;
|
||||
});
|
||||
|
||||
if (!hit || !(hit.object instanceof THREE.Mesh)) {
|
||||
lastHitKey.current = '';
|
||||
setHoverInfo(null);
|
||||
if (hoverTimer.current) clearTimeout(hoverTimer.current);
|
||||
return;
|
||||
}
|
||||
|
||||
// Stability check – same approximate position for debounce
|
||||
const key = `${hit.point.x.toFixed(3)},${hit.point.y.toFixed(3)},${hit.point.z.toFixed(3)}`;
|
||||
if (key === lastHitKey.current) return; // timer already running
|
||||
lastHitKey.current = key;
|
||||
setHoverInfo(null);
|
||||
|
||||
if (hoverTimer.current) clearTimeout(hoverTimer.current);
|
||||
hoverTimer.current = setTimeout(() => {
|
||||
if (!(hit.object instanceof THREE.Mesh)) return;
|
||||
const canvas = gl.domElement;
|
||||
const canvasSize = { width: canvas.clientWidth, height: canvas.clientHeight };
|
||||
|
||||
// Try hole detection first
|
||||
if (hit.faceIndex !== undefined) {
|
||||
const hole = detectHoleAtFace(hit.object, hit.faceIndex);
|
||||
if (hole) {
|
||||
setHoverInfo({
|
||||
type: 'hole',
|
||||
position: [hole.center.x, hole.center.y, hole.center.z],
|
||||
value: hole.diameterMM,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Try edge detection
|
||||
const edge = findNearestEdgeSegment(hit.object, hit.point, camera, canvasSize, 15);
|
||||
if (edge) {
|
||||
setHoverInfo({
|
||||
type: 'edge',
|
||||
position: [edge.midpoint.x, edge.midpoint.y, edge.midpoint.z],
|
||||
value: edge.lengthMM,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setHoverInfo(null);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const onLeave = () => {
|
||||
lastHitKey.current = '';
|
||||
setHoverInfo(null);
|
||||
if (hoverTimer.current) clearTimeout(hoverTimer.current);
|
||||
};
|
||||
|
||||
gl.domElement.addEventListener('pointermove', onMove);
|
||||
gl.domElement.addEventListener('pointerleave', onLeave);
|
||||
return () => {
|
||||
gl.domElement.removeEventListener('pointermove', onMove);
|
||||
gl.domElement.removeEventListener('pointerleave', onLeave);
|
||||
if (hoverTimer.current) clearTimeout(hoverTimer.current);
|
||||
};
|
||||
}, [measureMode, camera, scene, gl, raycaster, mouse, setHoverInfo]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Raycasting click handler for measurement mode – uses snap point when available */
|
||||
function MeasureClickHandler() {
|
||||
const { camera, scene, gl } = useThree();
|
||||
const measureMode = useModelStore((s) => s.measureMode);
|
||||
const addMeasurePoint = useModelStore((s) => s.addMeasurePoint);
|
||||
const snapPoint = useModelStore((s) => s.snapPoint);
|
||||
const raycaster = useMemo(() => new THREE.Raycaster(), []);
|
||||
const mouse = useMemo(() => new THREE.Vector2(), []);
|
||||
|
||||
const handleClick = useCallback((event: MouseEvent) => {
|
||||
if (!measureMode) return;
|
||||
|
||||
// Use snap point if available
|
||||
if (snapPoint) {
|
||||
addMeasurePoint({ x: snapPoint.x, y: snapPoint.y, z: snapPoint.z });
|
||||
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;
|
||||
@@ -203,12 +397,12 @@ function MeasureClickHandler() {
|
||||
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;
|
||||
if (obj.geometry instanceof THREE.RingGeometry) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@@ -216,7 +410,7 @@ function MeasureClickHandler() {
|
||||
if (hit) {
|
||||
addMeasurePoint({ x: hit.point.x, y: hit.point.y, z: hit.point.z });
|
||||
}
|
||||
}, [measureMode, addMeasurePoint, camera, scene, gl, raycaster, mouse]);
|
||||
}, [measureMode, addMeasurePoint, snapPoint, camera, scene, gl, raycaster, mouse]);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = gl.domElement;
|
||||
@@ -274,6 +468,8 @@ export function ModelViewerCanvas({ url }: ModelViewerProps) {
|
||||
|
||||
<MeasurementOverlay />
|
||||
<MeasureClickHandler />
|
||||
<SmartSnapHandler />
|
||||
<HoverDetector />
|
||||
|
||||
<GridLayer />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user