This commit is contained in:
114
tests/unit/GuideTopView.spec.ts
Normal file
114
tests/unit/GuideTopView.spec.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import * as THREE from 'three'
|
||||
import {
|
||||
createGuideObliqueReturnPose,
|
||||
createGuideTopViewPose,
|
||||
getGuideTopViewScreenBasis,
|
||||
getPerspectiveDistanceForHorizontalSpan,
|
||||
getPerspectiveHorizontalVisibleSpan
|
||||
} from '@/domain/guideTopView'
|
||||
|
||||
const createObliqueCamera = () => {
|
||||
const yaw = THREE.MathUtils.degToRad(54)
|
||||
const elevation = THREE.MathUtils.degToRad(42)
|
||||
const horizontal = Math.cos(elevation)
|
||||
const target = new THREE.Vector3(29.0598, -61.8768, 23.9424)
|
||||
const direction = new THREE.Vector3(
|
||||
Math.sin(yaw) * horizontal,
|
||||
Math.sin(elevation),
|
||||
Math.cos(yaw) * horizontal
|
||||
).normalize()
|
||||
const camera = new THREE.PerspectiveCamera(42, 390 / 844, 0.1, 20_000)
|
||||
camera.position.copy(target).addScaledVector(direction, 720)
|
||||
camera.up.set(0, 1, 0)
|
||||
camera.lookAt(target)
|
||||
camera.updateProjectionMatrix()
|
||||
camera.updateMatrixWorld()
|
||||
return { camera, target }
|
||||
}
|
||||
|
||||
describe('GuideTopView', () => {
|
||||
it('derives the top-view basis from the current camera screen-right direction', () => {
|
||||
const { camera } = createObliqueCamera()
|
||||
const basis = getGuideTopViewScreenBasis(camera.quaternion)
|
||||
const yaw = THREE.MathUtils.degToRad(54)
|
||||
|
||||
expect(basis.screenRight.x).toBeCloseTo(Math.cos(yaw), 8)
|
||||
expect(basis.screenRight.y).toBeCloseTo(0, 8)
|
||||
expect(basis.screenRight.z).toBeCloseTo(-Math.sin(yaw), 8)
|
||||
expect(basis.screenDown.x).toBeCloseTo(Math.sin(yaw), 8)
|
||||
expect(basis.screenDown.z).toBeCloseTo(Math.cos(yaw), 8)
|
||||
expect(basis.screenRight.dot(basis.screenDown)).toBeCloseTo(0, 8)
|
||||
})
|
||||
|
||||
it('keeps target, horizontal scale, and screen direction when entering top view', () => {
|
||||
const { camera, target } = createObliqueCamera()
|
||||
const originalDistance = camera.position.distanceTo(target)
|
||||
const originalSpan = getPerspectiveHorizontalVisibleSpan(
|
||||
originalDistance,
|
||||
camera.fov,
|
||||
camera.aspect
|
||||
)
|
||||
const pose = createGuideTopViewPose({
|
||||
position: camera.position,
|
||||
target,
|
||||
quaternion: camera.quaternion,
|
||||
fov: camera.fov,
|
||||
aspect: camera.aspect
|
||||
})
|
||||
|
||||
expect(pose.target.distanceTo(target)).toBeLessThan(1e-9)
|
||||
expect(pose.position.x).toBeCloseTo(target.x, 9)
|
||||
expect(pose.position.z).toBeCloseTo(target.z, 9)
|
||||
expect(pose.position.y - target.y).toBeCloseTo(originalDistance, 9)
|
||||
expect(pose.visibleWorldSpan).toBeCloseTo(originalSpan, 9)
|
||||
expect(getPerspectiveDistanceForHorizontalSpan(
|
||||
pose.visibleWorldSpan,
|
||||
camera.fov,
|
||||
camera.aspect
|
||||
)).toBeCloseTo(originalDistance, 9)
|
||||
|
||||
const topCamera = camera.clone()
|
||||
topCamera.position.copy(pose.position)
|
||||
topCamera.up.copy(pose.up)
|
||||
topCamera.lookAt(pose.target)
|
||||
topCamera.updateMatrixWorld()
|
||||
const left = target.clone().addScaledVector(pose.screenRight, -20)
|
||||
const right = target.clone().addScaledVector(pose.screenRight, 20)
|
||||
const obliqueWidth = right.clone().project(camera).x - left.clone().project(camera).x
|
||||
const topWidth = right.clone().project(topCamera).x - left.clone().project(topCamera).x
|
||||
expect(topWidth).toBeCloseTo(obliqueWidth, 8)
|
||||
})
|
||||
|
||||
it('restores the exact oblique pose when the top viewport did not move', () => {
|
||||
const { camera, target } = createObliqueCamera()
|
||||
const result = createGuideObliqueReturnPose({
|
||||
originalPosition: camera.position,
|
||||
originalTarget: target,
|
||||
currentTarget: target.clone(),
|
||||
currentDistance: camera.position.distanceTo(target)
|
||||
})
|
||||
|
||||
expect(result.viewportChanged).toBe(false)
|
||||
expect(result.position.distanceTo(camera.position)).toBeLessThan(1e-9)
|
||||
expect(result.target.distanceTo(target)).toBeLessThan(1e-9)
|
||||
})
|
||||
|
||||
it('preserves a changed world center and scale when returning to oblique view', () => {
|
||||
const { camera, target } = createObliqueCamera()
|
||||
const currentTarget = target.clone().add(new THREE.Vector3(18, 0, -11))
|
||||
const result = createGuideObliqueReturnPose({
|
||||
originalPosition: camera.position,
|
||||
originalTarget: target,
|
||||
currentTarget,
|
||||
currentDistance: 540
|
||||
})
|
||||
const originalDirection = camera.position.clone().sub(target).normalize()
|
||||
const restoredDirection = result.position.clone().sub(result.target).normalize()
|
||||
|
||||
expect(result.viewportChanged).toBe(true)
|
||||
expect(result.target.distanceTo(currentTarget)).toBeLessThan(1e-9)
|
||||
expect(result.position.distanceTo(result.target)).toBeCloseTo(540, 9)
|
||||
expect(restoredDirection.distanceTo(originalDirection)).toBeLessThan(1e-9)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user