96 lines
2.9 KiB
TypeScript
96 lines
2.9 KiB
TypeScript
import { useMemo } from 'react';
|
|
import { useXRPlanes, XRPlaneModel } from '@react-three/xr';
|
|
import { XRSpace } from '@react-three/xr';
|
|
import * as THREE from 'three';
|
|
|
|
interface XRPlaneOverlayProps {
|
|
showPlanes: boolean;
|
|
highlightNearest?: THREE.Vector3 | null;
|
|
}
|
|
|
|
/**
|
|
* Renders semi-transparent overlays on all detected WebXR planes.
|
|
* Walls are tinted blue, floors/ceilings green, others grey.
|
|
*/
|
|
export function XRPlaneOverlay({ showPlanes, highlightNearest }: XRPlaneOverlayProps) {
|
|
const allPlanes = useXRPlanes();
|
|
|
|
if (!showPlanes || allPlanes.length === 0) return null;
|
|
|
|
return (
|
|
<>
|
|
{allPlanes.map((plane, i) => {
|
|
const label = plane.semanticLabel ?? '';
|
|
const isHorizontal = plane.orientation === 'horizontal';
|
|
const isVertical = plane.orientation === 'vertical';
|
|
|
|
// Color by type
|
|
let color = '#64748b'; // default grey
|
|
if (label === 'floor' || label === 'table' || (isHorizontal && label !== 'ceiling')) {
|
|
color = '#22c55e'; // green for floor/table/horizontal
|
|
} else if (label === 'wall' || isVertical) {
|
|
color = '#3b82f6'; // blue for walls
|
|
} else if (label === 'ceiling') {
|
|
color = '#a855f7'; // purple for ceiling
|
|
}
|
|
|
|
return (
|
|
<XRSpace key={i} space={plane.planeSpace}>
|
|
<XRPlaneModel plane={plane}>
|
|
<meshBasicMaterial
|
|
color={color}
|
|
transparent
|
|
opacity={0.15}
|
|
side={THREE.DoubleSide}
|
|
depthWrite={false}
|
|
/>
|
|
</XRPlaneModel>
|
|
</XRSpace>
|
|
);
|
|
})}
|
|
</>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Hook to find the nearest detected plane to a given point.
|
|
* Returns the plane's center position and orientation quaternion.
|
|
*/
|
|
export function useNearestPlane(
|
|
targetPoint: THREE.Vector3 | null,
|
|
maxDistance = 0.5
|
|
): { position: THREE.Vector3; quaternion: THREE.Quaternion; plane: XRPlane } | null {
|
|
const allPlanes = useXRPlanes();
|
|
|
|
return useMemo(() => {
|
|
if (!targetPoint || allPlanes.length === 0) return null;
|
|
|
|
let nearest: { position: THREE.Vector3; quaternion: THREE.Quaternion; plane: XRPlane } | null = null;
|
|
let minDist = maxDistance;
|
|
|
|
for (const plane of allPlanes) {
|
|
// Use the plane's polygon center as approximation
|
|
const polygon = plane.polygon;
|
|
if (!polygon || polygon.length === 0) continue;
|
|
|
|
const center = new THREE.Vector3();
|
|
for (const pt of polygon) {
|
|
center.add(new THREE.Vector3(pt.x, pt.y, pt.z));
|
|
}
|
|
center.divideScalar(polygon.length);
|
|
|
|
const dist = center.distanceTo(targetPoint);
|
|
if (dist < minDist) {
|
|
minDist = dist;
|
|
nearest = {
|
|
position: center.clone(),
|
|
quaternion: new THREE.Quaternion(),
|
|
plane,
|
|
};
|
|
}
|
|
}
|
|
|
|
return nearest;
|
|
}, [targetPoint, allPlanes, maxDistance]);
|
|
}
|