115 lines
4.4 KiB
TypeScript
115 lines
4.4 KiB
TypeScript
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)
|
|
})
|
|
})
|