"use client"; import { useRef, useState, useCallback, useMemo, Suspense, useEffect } from "react"; import { Canvas, useFrame, useThree } from "@react-three/fiber"; import { OrbitControls, PerspectiveCamera, Html, useHelper } from "@react-three/drei"; import type { OrbitControls as OrbitControlsImpl } from "three-stdlib"; import * as THREE from "three"; import { cn } from "@/lib/utils"; // 角度配置常量 const AZIMUTH_OPTIONS = [ { angle: 0, label: "正前方", value: "front" }, { angle: 45, label: "前右", value: "front-right" }, { angle: 90, label: "右侧", value: "right" }, { angle: 135, label: "后右", value: "back-right" }, { angle: 180, label: "正后方", value: "back" }, { angle: 225, label: "后左", value: "back-left" }, { angle: 270, label: "左侧", value: "left" }, { angle: 315, label: "前左", value: "front-left" }, ]; const ELEVATION_OPTIONS = [ { angle: -30, label: "仰视", value: "low" }, { angle: 0, label: "平视", value: "eye-level" }, { angle: 30, label: "俯视", value: "elevated" }, { angle: 60, label: "高角度", value: "high" }, ]; const DISTANCE_OPTIONS = [ { factor: 0.6, label: "特写", value: "close" }, { factor: 1.0, label: "中景", value: "medium" }, { factor: 1.8, label: "远景", value: "wide" }, ]; interface AngleController3DProps { onChange?: (angles: { azimuth: string; elevation: string; distance: string; azimuthDegree: number; elevationDegree: number; distanceFactor: number; }) => void; initialAzimuth?: number; initialElevation?: number; initialDistance?: number; showObject?: boolean; className?: string; } // 3D 场景中的相机指示器 - 简约现代风格 function CameraIndicator({ position }: { position: THREE.Vector3 }) { // 计算相机朝向中心的旋转 const lookAtCenter = useMemo(() => { const quaternion = new THREE.Quaternion(); const up = new THREE.Vector3(0, 1, 0); const matrix = new THREE.Matrix4(); matrix.lookAt(position, new THREE.Vector3(0, 0, 0), up); quaternion.setFromRotationMatrix(matrix); return quaternion; }, [position]); return ( {/* 相机主体 - 哑光黑/深灰 */} {/* 相机顶部 */} {/* 镜头筒 - 金属质感 */} {/* 镜头玻璃 - 蓝色反光 */} {/* 视野指示 - 极简线条 */} {/* 连接线 */} {[ [-0.3, -0.2, 1.0], [0.3, -0.2, 1.0], [0.3, 0.2, 1.0], [-0.3, 0.2, 1.0] ].map((point, i) => ( ))} ); } // 方位角刻度圆环 - 更加隐形 function AzimuthRing() { return ( {/* 圆环 */} {/* 方位角标签 */} {AZIMUTH_OPTIONS.map((opt) => { const rad = (opt.angle * Math.PI) / 180; const x = Math.sin(rad) * 2.3; const z = Math.cos(rad) * 2.3; return (
{opt.label}
); })}
); } // 目标物体 - 极简风格 function TargetObject() { return ( {/* 极简几何体代表目标 */} {/* 底部阴影 */} {/* 网格地面 */} ); } // 3D 控制场景 function Scene({ azimuth, elevation, distance, onAngleChange, showObject, }: { azimuth: number; elevation: number; distance: number; onAngleChange: (azimuth: number, elevation: number) => void; showObject: boolean; }) { const controlsRef = useRef(null); const ghostCameraRef = useRef(null); const mainCameraRef = useRef(null); // 计算目标拍摄位置(CameraIndicator的位置) const targetPosition = useMemo(() => { const azimuthRad = (azimuth * Math.PI) / 180; const elevationRad = (elevation * Math.PI) / 180; const radius = 2 * distance; const x = radius * Math.cos(elevationRad) * Math.sin(azimuthRad); const y = radius * Math.sin(elevationRad); const z = radius * Math.cos(elevationRad) * Math.cos(azimuthRad); return new THREE.Vector3(x, y, z); }, [azimuth, elevation, distance]); // 同步 Ghost Camera 位置到 state // 当 state 改变时,更新 Ghost Camera 位置 useEffect(() => { if (ghostCameraRef.current) { ghostCameraRef.current.position.copy(targetPosition); ghostCameraRef.current.lookAt(0, 0, 0); } }, [targetPosition]); // 监听控制器变化 (用户拖拽) const handleControlsChange = useCallback(() => { if (ghostCameraRef.current) { const camera = ghostCameraRef.current; const spherical = new THREE.Spherical(); spherical.setFromVector3(camera.position); // 将球面坐标转换为我们的角度系统 let newAzimuth = THREE.MathUtils.radToDeg(spherical.theta); if (newAzimuth < 0) newAzimuth += 360; const newElevation = 90 - THREE.MathUtils.radToDeg(spherical.phi); // 只有当角度变化超过阈值时才触发更新,避免浮点数抖动 if (Math.abs(newAzimuth - azimuth) > 0.5 || Math.abs(newElevation - elevation) > 0.5) { onAngleChange(Math.round(newAzimuth), Math.round(newElevation)); } } }, [onAngleChange, azimuth, elevation]); return ( <> {/* 主相机:上帝视角,固定位置 */} c.lookAt(0, 0, 0)} /> {/* 幽灵相机:用于 OrbitControls 交互,不可见 */} {/* 控制器绑定到幽灵相机 */} {/* 灯光 */} {/* 场景内容 */} {showObject && } {/* 拍摄相机模型(在目标位置渲染) */} {/* 坐标轴辅助 */} ); } // 主组件 export function AngleController3D({ onChange, initialAzimuth = 45, initialElevation = 0, initialDistance = 1.0, showObject = true, className, }: AngleController3DProps) { const [azimuth, setAzimuth] = useState(initialAzimuth); const [elevation, setElevation] = useState(initialElevation); const [distance, setDistance] = useState(initialDistance); // 将角度映射到最近的预设值 const getClosestAzimuth = useCallback((deg: number) => { let minDiff = 360; let closest = AZIMUTH_OPTIONS[0]; for (const opt of AZIMUTH_OPTIONS) { const diff = Math.min(Math.abs(deg - opt.angle), 360 - Math.abs(deg - opt.angle)); if (diff < minDiff) { minDiff = diff; closest = opt; } } return closest; }, []); const getClosestElevation = useCallback((deg: number) => { let minDiff = 180; let closest = ELEVATION_OPTIONS[0]; for (const opt of ELEVATION_OPTIONS) { const diff = Math.abs(deg - opt.angle); if (diff < minDiff) { minDiff = diff; closest = opt; } } return closest; }, []); const getClosestDistance = useCallback((factor: number) => { let minDiff = 10; let closest = DISTANCE_OPTIONS[0]; for (const opt of DISTANCE_OPTIONS) { const diff = Math.abs(factor - opt.factor); if (diff < minDiff) { minDiff = diff; closest = opt; } } return closest; }, []); // 处理角度变化 const handleAngleChange = useCallback( (newAzimuth: number, newElevation: number) => { setAzimuth(newAzimuth); setElevation(newElevation); const closestAzimuth = getClosestAzimuth(newAzimuth); const closestElevation = getClosestElevation(newElevation); const closestDistance = getClosestDistance(distance); onChange?.({ azimuth: closestAzimuth.value, elevation: closestElevation.value, distance: closestDistance.value, azimuthDegree: newAzimuth, elevationDegree: newElevation, distanceFactor: distance, }); }, [distance, getClosestAzimuth, getClosestElevation, getClosestDistance, onChange] ); // 处理距离变化 const handleDistanceChange = useCallback( (newDistance: number) => { setDistance(newDistance); const closestAzimuth = getClosestAzimuth(azimuth); const closestElevation = getClosestElevation(elevation); const closestDistance = getClosestDistance(newDistance); onChange?.({ azimuth: closestAzimuth.value, elevation: closestElevation.value, distance: closestDistance.value, azimuthDegree: azimuth, elevationDegree: elevation, distanceFactor: newDistance, }); }, [azimuth, elevation, getClosestAzimuth, getClosestElevation, getClosestDistance, onChange] ); // 快速选择预设角度 const handlePresetClick = useCallback( (presetAzimuth: number, presetElevation: number) => { setAzimuth(presetAzimuth); setElevation(presetElevation); const closestAzimuth = getClosestAzimuth(presetAzimuth); const closestElevation = getClosestElevation(presetElevation); const closestDistance = getClosestDistance(distance); onChange?.({ azimuth: closestAzimuth.value, elevation: closestElevation.value, distance: closestDistance.value, azimuthDegree: presetAzimuth, elevationDegree: presetElevation, distanceFactor: distance, }); }, [distance, getClosestAzimuth, getClosestElevation, getClosestDistance, onChange] ); const currentAzimuth = getClosestAzimuth(azimuth); const currentElevation = getClosestElevation(elevation); const currentDistance = getClosestDistance(distance); return (
{/* 3D 视图 */}
{/* 当前角度显示 - 悬浮卡片风格 */}
方位: {currentAzimuth.label} ({azimuth}°) 仰角: {currentElevation.label} ({elevation}°)
{/* 操作提示 */}
🖱️ 拖拽旋转相机位置
{/* 快速预设按钮 */}
快速选择
{[ { label: "正前", azimuth: 0, elevation: 0 }, { label: "前3/4", azimuth: 45, elevation: 0 }, { label: "侧面", azimuth: 90, elevation: 0 }, { label: "后3/4", azimuth: 135, elevation: 0 }, { label: "正后", azimuth: 180, elevation: 0 }, { label: "俯视", azimuth: 0, elevation: 60 }, { label: "仰视", azimuth: 0, elevation: -30 }, { label: "鸟瞰", azimuth: 45, elevation: 60 }, ].map((preset) => ( ))}
{/* 距离控制 */}
镜头距离
{DISTANCE_OPTIONS.map((opt) => ( ))}
); } export default AngleController3D;