import type { CameraPresetId, CameraSnapshot, Vec3, ViewerSceneBounds } from '../types/viewer'; export interface CameraPresetDefinition { id: CameraPresetId; label: string; ariaLabel: string; direction: Vec3; up: Vec3; distanceMultiplier: number; } export const DEFAULT_SCENE_BOUNDS: ViewerSceneBounds = { center: [0, 0.85, 0], radius: 4.6, min: [-2.5, -1.25, -2.25], max: [2.5, 3.25, 2.25] }; export const CAMERA_PRESET_ORDER: CameraPresetId[] = [ 'isometric', 'front', 'back', 'left', 'right', 'top' ]; export const CAMERA_PRESETS: Record = { isometric: { id: 'isometric', label: 'Iso', ariaLabel: 'Set camera to isometric view', direction: [0.78, 0.52, 0.62], up: [0, 1, 0], distanceMultiplier: 2.15 }, front: { id: 'front', label: 'Front', ariaLabel: 'Set camera to front view', direction: [0, 0.12, 1], up: [0, 1, 0], distanceMultiplier: 2.05 }, back: { id: 'back', label: 'Back', ariaLabel: 'Set camera to back view', direction: [0, 0.12, -1], up: [0, 1, 0], distanceMultiplier: 2.05 }, left: { id: 'left', label: 'Left', ariaLabel: 'Set camera to left side view', direction: [-1, 0.12, 0], up: [0, 1, 0], distanceMultiplier: 2.05 }, right: { id: 'right', label: 'Right', ariaLabel: 'Set camera to right side view', direction: [1, 0.12, 0], up: [0, 1, 0], distanceMultiplier: 2.05 }, top: { id: 'top', label: 'Top', ariaLabel: 'Set camera to top view', direction: [0, 1, 0.001], up: [0, 0, -1], distanceMultiplier: 2.2 } }; export function normaliseVec3(vector: Vec3): Vec3 { const [x, y, z] = vector; const length = Math.hypot(x, y, z) || 1; return [x / length, y / length, z / length]; } export function addVec3(a: Vec3, b: Vec3): Vec3 { return [a[0] + b[0], a[1] + b[1], a[2] + b[2]]; } export function multiplyVec3(vector: Vec3, scalar: number): Vec3 { return [vector[0] * scalar, vector[1] * scalar, vector[2] * scalar]; } export function createCameraSnapshotForPreset( presetId: CameraPresetId, bounds: ViewerSceneBounds = DEFAULT_SCENE_BOUNDS ): CameraSnapshot { const preset = CAMERA_PRESETS[presetId]; const direction = normaliseVec3(preset.direction); const radius = Math.max(1.8, Number.isFinite(bounds.radius) ? bounds.radius : DEFAULT_SCENE_BOUNDS.radius); const distance = radius * preset.distanceMultiplier; return { position: addVec3(bounds.center, multiplyVec3(direction, distance)), target: bounds.center, up: preset.up, fov: 42 }; } export const DEFAULT_CAMERA_SNAPSHOT: CameraSnapshot = createCameraSnapshotForPreset('isometric'); export function coerceCameraSnapshot( snapshot: Partial | undefined, fallback: CameraSnapshot = DEFAULT_CAMERA_SNAPSHOT ): CameraSnapshot { if (!snapshot) { return fallback; } return { position: coerceVec3(snapshot.position, fallback.position), target: coerceVec3(snapshot.target, fallback.target), up: snapshot.up ? coerceVec3(snapshot.up, fallback.up ?? [0, 1, 0]) : fallback.up, zoom: coerceFiniteNumber(snapshot.zoom, fallback.zoom), fov: coerceFiniteNumber(snapshot.fov, fallback.fov) }; } function coerceVec3(value: unknown, fallback: Vec3): Vec3 { if (!Array.isArray(value) || value.length !== 3) { return fallback; } const tuple = value.map((entry) => Number(entry)); if (tuple.some((entry) => !Number.isFinite(entry))) { return fallback; } return [tuple[0], tuple[1], tuple[2]]; } function coerceFiniteNumber(value: unknown, fallback: number | undefined): number | undefined { if (value === undefined || value === null || value === '') { return fallback; } const numeric = Number(value); return Number.isFinite(numeric) ? numeric : fallback; }