Files
frontend-miniapp/tests/unit/GuideTopView.spec.ts
lyf 9ea1bfab71
Some checks failed
CI / verify (push) Has been cancelled
同步 sgs-frontend-mobile 源码
2026-07-27 11:26:01 +08:00

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)
})
})