Files
ozone-tech_owl_prime/src/domain/cinematicCamera.test.ts
root a078d296e3 feat: cinematic camera playback for 3D demo
- Add cinematicCamera.ts with 9 camera modes
- Smooth camera transitions with lerp
- Camera follows item during movement
- Category-aware routing views (B/C/D)
- Viewport adaptation (desktop/laptop/mobile)
- Motion trail for movement feedback
- Auto camera toggle button
- 20 unit tests for camera logic
2026-07-09 15:53:50 +02:00

169 lines
5.2 KiB
TypeScript

/**
* Tests for cinematicCamera module.
*/
import { describe, it, expect } from 'vitest';
import {
getCameraModeForPhase,
getCameraConfig,
smoothCameraTransition,
isValidCameraConfig,
phaseHasCameraMode,
getInitialCameraConfig,
getAllCameraModes,
getViewportType,
lerpVector3,
lerp,
} from './cinematicCamera';
import { CASE_PHASES } from './continuousPlayback';
import type { Category } from './types';
describe('getCameraModeForPhase', () => {
it('returns a mode for every playback phase', () => {
for (const phaseConfig of CASE_PHASES) {
const mode = getCameraModeForPhase(phaseConfig.phase);
expect(mode).toBeTruthy();
expect(typeof mode).toBe('string');
}
});
it('spawn returns feedCloseup', () => {
expect(getCameraModeForPhase('spawn')).toBe('feedCloseup');
});
it('detection returns inspectionTop', () => {
expect(getCameraModeForPhase('detection')).toBe('inspectionTop');
});
it('routing returns chuteCloseup', () => {
expect(getCameraModeForPhase('routing')).toBe('chuteCloseup');
});
});
describe('getCameraConfig', () => {
it('returns valid config for all phases', () => {
for (const phaseConfig of CASE_PHASES) {
const config = getCameraConfig(phaseConfig.phase, 'B', null, 'desktop');
expect(isValidCameraConfig(config)).toBe(true);
}
});
it('camera position/target are finite numbers', () => {
const config = getCameraConfig('detection', 'B', [0, 0.8, 0], 'desktop');
expect(config.position.every(n => Number.isFinite(n))).toBe(true);
expect(config.target.every(n => Number.isFinite(n))).toBe(true);
});
it('fov within reasonable range (10-120)', () => {
for (const phaseConfig of CASE_PHASES) {
const config = getCameraConfig(phaseConfig.phase, 'C', null, 'desktop');
expect(config.fov).toBeGreaterThan(10);
expect(config.fov).toBeLessThan(120);
}
});
it('target category B/C/D maps to routing camera', () => {
const categories: Category[] = ['B', 'C', 'D'];
for (const cat of categories) {
const config = getCameraConfig('routing', cat, null, 'desktop');
expect(config.mode).toBe('chuteCloseup');
expect(isValidCameraConfig(config)).toBe(true);
}
});
it('adjusts for viewport type', () => {
const desktop = getCameraConfig('move_to_detection', 'B', null, 'desktop');
const mobile = getCameraConfig('move_to_detection', 'B', null, 'mobile');
// Mobile should have higher camera and wider FOV
expect(mobile.position[1]).toBeGreaterThan(desktop.position[1]);
expect(mobile.fov).toBeGreaterThan(desktop.fov);
});
});
describe('smoothCameraTransition', () => {
it('interpolates between configs', () => {
const current = getCameraConfig('spawn', 'B', null, 'desktop');
const target = getCameraConfig('detection', 'B', null, 'desktop');
const result = smoothCameraTransition(current, target, 0.5);
// Result should be between current and target
expect(result.position[0]).toBeGreaterThanOrEqual(
Math.min(current.position[0], target.position[0])
);
expect(result.position[0]).toBeLessThanOrEqual(
Math.max(current.position[0], target.position[0])
);
});
});
describe('isValidCameraConfig', () => {
it('returns true for valid config', () => {
const config = getInitialCameraConfig();
expect(isValidCameraConfig(config)).toBe(true);
});
it('returns false for invalid fov', () => {
const config = { ...getInitialCameraConfig(), fov: 5 };
expect(isValidCameraConfig(config)).toBe(false);
});
it('returns false for NaN position', () => {
const config = { ...getInitialCameraConfig(), position: [NaN, 0, 0] as [number, number, number] };
expect(isValidCameraConfig(config)).toBe(false);
});
});
describe('phaseHasCameraMode', () => {
it('returns true for all phases', () => {
for (const phaseConfig of CASE_PHASES) {
expect(phaseHasCameraMode(phaseConfig.phase)).toBe(true);
}
});
});
describe('getViewportType', () => {
it('desktop for width >= 1200', () => {
expect(getViewportType(1920)).toBe('desktop');
expect(getViewportType(1200)).toBe('desktop');
});
it('laptop for width 768-1199', () => {
expect(getViewportType(1024)).toBe('laptop');
expect(getViewportType(768)).toBe('laptop');
});
it('mobile for width < 768', () => {
expect(getViewportType(390)).toBe('mobile');
expect(getViewportType(767)).toBe('mobile');
});
});
describe('lerp utilities', () => {
it('lerp returns midpoint at t=0.5', () => {
expect(lerp(0, 10, 0.5)).toBe(5);
});
it('lerpVector3 works correctly', () => {
const a: [number, number, number] = [0, 0, 0];
const b: [number, number, number] = [10, 20, 30];
const result = lerpVector3(a, b, 0.5);
expect(result).toEqual([5, 10, 15]);
});
});
describe('getAllCameraModes', () => {
it('returns all defined modes', () => {
const modes = getAllCameraModes();
expect(modes).toContain('overview');
expect(modes).toContain('feedCloseup');
expect(modes).toContain('inspectionTop');
expect(modes).toContain('measurementSide');
expect(modes).toContain('routingWide');
expect(modes).toContain('chuteCloseup');
expect(modes).toContain('resultZone');
expect(modes.length).toBeGreaterThanOrEqual(8);
});
});