import { describe, expect, it } from 'vitest'; import { Box3, MathUtils, PerspectiveCamera, Vector3 } from 'three'; import { applyPerspectiveCameraFit, computePerspectiveCameraFit, createCameraFitBasis, getBoxCorners, isFiniteBox, type PerspectiveCameraFitResult, } from './fitCameraToBounds'; describe('fitCameraToBounds', () => { it('identifies finite, empty, and invalid bounds', () => { expect(isFiniteBox(new Box3(new Vector3(0, 0, 0), new Vector3(0, 0, 0)))).toBe( true, ); expect(isFiniteBox(new Box3())).toBe(false); expect( isFiniteBox(new Box3(new Vector3(0, 0, 0), new Vector3(Number.NaN, 1, 1))), ).toBe(false); }); it('builds an orthonormal basis even when preferred up is parallel to the view direction', () => { const basis = createCameraFitBasis([0, 1, 0], [0, 1, 0]); expect(basis.viewDirection.length()).toBeCloseTo(1); expect(basis.right.length()).toBeCloseTo(1); expect(basis.up.length()).toBeCloseTo(1); expect(Math.abs(basis.up.dot(basis.viewDirection))).toBeLessThan(1e-6); expect(Math.abs(basis.right.dot(basis.viewDirection))).toBeLessThan(1e-6); expect(Math.abs(basis.right.dot(basis.up))).toBeLessThan(1e-6); }); it('frames every box corner for a direct front view', () => { const box = new Box3(new Vector3(-2, -1, -0.5), new Vector3(2, 1, 0.5)); const fov = 50; const aspect = 16 / 9; const fit = computePerspectiveCameraFit(box, fov, aspect, { direction: [0, 0, 1], margin: 1.08, }); expect(fit.constrainedByMaxDistance).toBe(false); expectBoxCornersToFit(box, fit, fov, aspect); }); it('requires more distance for narrow viewports when fitting wide machines', () => { const box = new Box3(new Vector3(-3, -0.5, -0.5), new Vector3(3, 0.5, 0.5)); const wideFit = computePerspectiveCameraFit(box, 60, 2, { direction: [0, 0, 1], margin: 1.1, }); const narrowFit = computePerspectiveCameraFit(box, 60, 0.5, { direction: [0, 0, 1], margin: 1.1, }); expect(narrowFit.distance).toBeGreaterThan(wideFit.distance); expectBoxCornersToFit(box, wideFit, 60, 2); expectBoxCornersToFit(box, narrowFit, 60, 0.5); }); it('returns a finite fallback fit for empty bounds', () => { const fit = computePerspectiveCameraFit(new Box3(), 60, 1, { fallbackRadius: 2, direction: [0, 0, 1], }); expect(fit.target.toArray()).toEqual([0, 0, 0]); expect(fit.size.toArray()).toEqual([4, 4, 4]); expect(fit.distance).toBeGreaterThan(0); expect(fit.near).toBeGreaterThan(0); expect(fit.far).toBeGreaterThan(fit.near); expect(Number.isFinite(fit.position.x)).toBe(true); expect(Number.isFinite(fit.position.y)).toBe(true); expect(Number.isFinite(fit.position.z)).toBe(true); }); it('reports when maxDistance prevents a full unconstrained fit', () => { const box = new Box3(new Vector3(-10, -1, -1), new Vector3(10, 1, 1)); const fit = computePerspectiveCameraFit(box, 45, 1, { direction: [0, 0, 1], maxDistance: 2, }); expect(fit.constrainedByMaxDistance).toBe(true); expect(fit.distance).toBe(2); expect(fit.requiredDistance).toBeGreaterThan(fit.distance); }); it('applies a fit pose to a PerspectiveCamera', () => { const camera = new PerspectiveCamera(45, 1, 0.1, 10); const box = new Box3(new Vector3(-1, -1, -1), new Vector3(1, 1, 1)); const fit = applyPerspectiveCameraFit(camera, box, { direction: [0, 0, 1], margin: 1.05, }); expect(camera.position.x).toBeCloseTo(fit.position.x); expect(camera.position.y).toBeCloseTo(fit.position.y); expect(camera.position.z).toBeCloseTo(fit.position.z); expect(camera.near).toBe(fit.near); expect(camera.far).toBe(fit.far); expect(camera.far).toBeGreaterThan(camera.near); const cameraForward = new Vector3(); camera.getWorldDirection(cameraForward); const expectedForward = fit.target.clone().sub(camera.position).normalize(); expect(cameraForward.angleTo(expectedForward)).toBeLessThan(1e-5); }); }); function expectBoxCornersToFit( box: Box3, fit: PerspectiveCameraFitResult, fovDegrees: number, aspect: number, ): void { const tanHalfVertical = Math.tan(MathUtils.degToRad(fovDegrees) / 2); const tanHalfHorizontal = tanHalfVertical * aspect; for (const corner of getBoxCorners(box)) { const relative = corner.clone().sub(fit.target); const depth = fit.distance - relative.dot(fit.basis.viewDirection); expect(depth).toBeGreaterThan(0); const normalizedX = Math.abs(relative.dot(fit.basis.right)) / (depth * tanHalfHorizontal); const normalizedY = Math.abs(relative.dot(fit.basis.up)) / (depth * tanHalfVertical); expect(normalizedX).toBeLessThanOrEqual(1 + 1e-6); expect(normalizedY).toBeLessThanOrEqual(1 + 1e-6); } }