export interface ViewportOrbitState { yaw: number; pitch: number; distance: number; target: [number, number, number]; } export interface ViewportCameraState extends ViewportOrbitState { position: [number, number, number]; } export type ViewportView = 'USER' | 'FRONT' | 'BACK' | 'RIGHT' | 'LEFT' | 'TOP' | 'BOTTOM'; export type ViewportNavigationCommand = ViewportView | 'PROJECTION' | 'FRAME_ALL' | 'FRAME_SELECTED' | 'ZOOM_IN' | 'ZOOM_OUT'; export interface ViewportNavigationCamera { position: [number,number,number]; quaternion: [number,number,number,number]; target: [number,number,number]; projection:'PERSPECTIVE'|'ORTHOGRAPHIC'; fov:number;aspect:number;near:number;far:number;zoom:number;orthoHeight:number; } export interface NativeViewportState { offset: [number,number,number]; rotation: [number,number,number,number]; distance: number; lens: number; near: number; far: number; projection: 'PERSPECTIVE'|'ORTHOGRAPHIC'|'CAMERA'; locked?: boolean; } export interface BlenderViewportCamera { position: [number,number,number]; target: [number,number,number]; orientation: [number,number,number,number]; projection: 'PERSPECTIVE'|'ORTHOGRAPHIC'; fov:number; near:number; far:number; zoom:number; orthoHeight:number; } export const VIEWPORT_DEFAULT_ORBIT: Readonly = Object.freeze({ yaw: -Math.PI / 4, pitch: 0.55, distance: 7, target: [0, 0, 0] as [number, number, number], }); export const VIEWPORT_ORBIT_ROTATE_SENSITIVITY = 0.008; export const VIEWPORT_ORBIT_ZOOM_SENSITIVITY = 0.001; export const VIEWPORT_ORBIT_MIN_DISTANCE = 0.2; export const VIEWPORT_ORBIT_MAX_DISTANCE = 500; export function orbitPosition(state: Pick): [number, number, number] { const horizontal = state.distance * Math.cos(state.pitch); return [ state.target[0] + horizontal * Math.cos(state.yaw), state.target[1] + horizontal * Math.sin(state.yaw), state.target[2] + state.distance * Math.sin(state.pitch), ]; } export function cameraState(state: ViewportOrbitState): ViewportCameraState { return { ...state, target: [...state.target] as [number, number, number], position: orbitPosition(state) }; } export function orbitStateFromPosition(position: readonly number[], target: readonly number[] = [0, 0, 0]): ViewportOrbitState { const dx = position[0] - target[0]; const dy = position[1] - target[1]; const dz = position[2] - target[2]; const distance = Math.max(VIEWPORT_ORBIT_MIN_DISTANCE, Math.hypot(dx, dy, dz)); return { yaw: Math.atan2(dy, dx), pitch: Math.asin(Math.max(-1, Math.min(1, dz / distance))), distance, target: [target[0], target[1], target[2]], }; } export function applyOrbitDelta(state: ViewportOrbitState, deltaX: number, deltaY: number, zoom: number): ViewportOrbitState { return { ...state, yaw: state.yaw - deltaX * VIEWPORT_ORBIT_ROTATE_SENSITIVITY, pitch: Math.max(-1.45, Math.min(1.45, state.pitch + deltaY * VIEWPORT_ORBIT_ROTATE_SENSITIVITY)), distance: Math.max(VIEWPORT_ORBIT_MIN_DISTANCE, Math.min(VIEWPORT_ORBIT_MAX_DISTANCE, state.distance * Math.exp(zoom * VIEWPORT_ORBIT_ZOOM_SENSITIVITY))), target: [...state.target] as [number, number, number], }; }