Back to home page

EIC code displayed by LXR

 
 

    


Warning, /firebird/packages/firebird-ng/display/src/lib/services/camera-framing.spec.ts is written in an unsupported language. File is not indexed.

0001 /**
0002  * Camera framing: what no pack pins derives from the loaded geometry's
0003  * bounding sphere (orbit limits; the home view while the camera has not
0004  * moved), and what a pack pins (a home pose and limits) stays exactly as
0005  * pinned. Presets come from the registry; face views keep the orbit
0006  * target and distance.
0007  */
0008 import { TestBed } from '@angular/core/testing';
0009 import { BoxGeometry, Group, Mesh } from 'three';
0010 import { ThreeService } from './three.service';
0011 import { PerfService } from './perf.service';
0012 import { RenderViewImpl } from './render-view';
0013 import { withCameraLimits, withCameraPreset, type CameraPreset } from '@dexvis/firebird-ng/api';
0014 import { withStandardCameraPresets } from '@dexvis/firebird-ng';
0015 
0016 /** The private members these specs set up instead of a WebGPU init. */
0017 type ThreeInternals = {
0018   views: RenderViewImpl[];
0019   sceneGeometry: Group;
0020   initialized: boolean;
0021   pinnedHomePose: () => RenderViewImpl['startPose'] | undefined;
0022   cameraLimits: { minDistance: number; maxDistance: number };
0023   scheduleGeometryBvh: () => void;
0024 };
0025 
0026 /** A pack's pinned home view: a top view from 7 m with x up. */
0027 const PINNED_HOME: CameraPreset = { name: 'home', position: [0, 7000, 0], target: [0, 0, 0], up: [1, 0, 0] };
0028 
0029 /** A ThreeService with a real main view over a fake renderer, and a 2 m cube as geometry. */
0030 function setup(providers: unknown[]) {
0031   TestBed.configureTestingModule({
0032     providers: [ThreeService, { provide: PerfService, useValue: { updateStats: () => undefined } }, ...providers as never[]],
0033   });
0034   const service = TestBed.inject(ThreeService);
0035   const internals = service as unknown as ThreeInternals;
0036   const renderer = { domElement: document.createElement('canvas') } as unknown as ConstructorParameters<typeof RenderViewImpl>[0];
0037   const main = new RenderViewImpl(renderer, {
0038     name: 'main',
0039     container: document.createElement('div'),
0040     startPose: internals.pinnedHomePose(),
0041     distanceLimits: internals.cameraLimits,
0042   });
0043   internals.views = [main];
0044   internals.sceneGeometry = new Group();
0045   internals.initialized = true;
0046   internals.scheduleGeometryBvh = () => undefined;
0047   const loadCube = () => {
0048     internals.sceneGeometry.add(new Mesh(new BoxGeometry(2000, 2000, 2000)));
0049     internals.sceneGeometry.updateMatrixWorld(true);
0050     service.geometryChanged();
0051   };
0052   return { service, main, loadCube };
0053 }
0054 
0055 describe('camera framing', () => {
0056   const cubeRadius = Math.sqrt(3) * 1000;
0057 
0058   it('without pins: derives the limits from the geometry and frames it from the home direction', () => {
0059     const { main, loadCube } = setup([...withStandardCameraPresets().providers]);
0060     expect(main.isAtStartPose()).toBe(true);
0061     loadCube();
0062 
0063     expect(main.controls.minDistance).toBeCloseTo(0.05 * cubeRadius);
0064     expect(main.controls.maxDistance).toBeCloseTo(5 * cubeRadius);
0065     expect(main.camera.far).toBeCloseTo(5.5 * cubeRadius);
0066     // Top view, the sphere fitting the 60 degree field of view: r / sin(30)
0067     expect(main.camera.position.y).toBeCloseTo(2 * cubeRadius);
0068     expect(main.controls.target.length()).toBeCloseTo(0);
0069     expect(main.camera.up.toArray()).toEqual([1, 0, 0]);
0070   });
0071 
0072   it('without pins: leaves a camera the user already moved where it is', () => {
0073     const { main, loadCube } = setup([...withStandardCameraPresets().providers]);
0074     main.camera.position.set(100, 200, 300);
0075     loadCube();
0076     expect(main.camera.position.toArray()).toEqual([100, 200, 300]);
0077     expect(main.controls.maxDistance).toBeCloseTo(5 * cubeRadius);
0078   });
0079 
0080   it('with a pinned home and limits: starts at the pinned home and keeps pose and limits after a load', () => {
0081     const { main, loadCube } = setup([
0082       ...withStandardCameraPresets().providers,
0083       ...withCameraPreset(PINNED_HOME).providers,
0084       ...withCameraLimits({ minDistance: 750, maxDistance: 75000 }).providers,
0085     ]);
0086     // Rounded: OrbitControls leaves sub-nanometer noise on the exact pose
0087     const roundedPosition = () => main.camera.position.toArray().map(value => Math.round(value) + 0);
0088     expect(roundedPosition()).toEqual([0, 7000, 0]);
0089     expect(main.camera.far).toBe(82500);
0090     loadCube();
0091     expect(roundedPosition()).toEqual([0, 7000, 0]);
0092     expect([main.controls.minDistance, main.controls.maxDistance, main.camera.far]).toEqual([750, 75000, 82500]);
0093   });
0094 
0095   it('a face preset keeps the orbit target and distance; a fixed pose sets all three', () => {
0096     const { service, main } = setup([...withStandardCameraPresets().providers]);
0097     main.controls.target.set(0, 0, 1000);
0098     main.camera.position.set(0, 3000, 1000);
0099     service.applyCameraPreset({ name: 'front', direction: [-1, 0, 0], up: [0, 1, 0] });
0100     expect(main.camera.position.toArray().map(value => Math.round(value) + 0)).toEqual([-3000, 0, 1000]);
0101     expect(main.camera.up.toArray()).toEqual([0, 1, 0]);
0102 
0103     service.applyCameraPreset({ name: 'downstream', position: [8000, 7500, 40000], target: [0, 0, 30000], up: [0, 1, 0] });
0104     expect(main.controls.target.toArray()).toEqual([0, 0, 30000]);
0105     expect(main.camera.position.toArray().map(value => Math.round(value) + 0)).toEqual([8000, 7500, 40000]);
0106   });
0107 });