Files
frontend-miniapp/tests/unit/GuideFloorBasemap.spec.ts
lyf 540bb8627f
Some checks failed
CI / verify (push) Has been cancelled
同步移动端源码并修复小程序嵌入标题
2026-07-28 18:37:40 +08:00

336 lines
10 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest'
import {
GUIDE_FLOOR_BASEMAP_PROJECTION,
GUIDE_FLOOR_BASEMAP_PROJECTION_VERSION,
GUIDE_FLOOR_BASEMAP_SCHEMA_VERSION,
findGuideCompositeBasemap,
findGuideFloorBasemap,
getGuideFloorBasemapFrame,
loadGuideFloorBasemapManifest,
projectPointToGuideFloorBasemap,
resetGuideFloorBasemapManifestCacheForTests,
isCompatibleGuideFloorBasemapManifest,
isValidGuideFloorOrthographicProjection,
unprojectGuideFloorBasemapPoint,
type GuideCompositeBasemapEntry,
type GuideFloorBasemapEntry
} from '@/services/model/GuideFloorBasemap'
const yaw = 54 * Math.PI / 180
const screenRight = { x: Math.cos(yaw), z: -Math.sin(yaw) }
const screenDown = { x: Math.sin(yaw), z: Math.cos(yaw) }
const createBasemap = (): GuideFloorBasemapEntry => ({
floorId: 'L1',
floorCode: 'L1',
modelVersion: 'v1',
projection: {
type: 'orthographic-xz',
minX: -43.737275,
maxX: 43.737275,
minZ: -47.055327,
maxZ: 47.055327,
planeY: 6,
center: { x: 0, z: 0 },
width: 80,
height: 50,
screenRight,
screenDown
},
initialViewport: {
centerX: 0,
centerZ: 0,
visibleWorldSpan: 40,
maxVisibleWorldSpan: 64
},
image: {
path: '/static/guide-floor-basemaps/L1.v1.ortho-yaw54-v2.compact-floor-center-v1.webp',
width: 1600,
height: 1000,
format: 'webp'
}
})
const createCompositeBasemap = (): GuideCompositeBasemapEntry => ({
...createBasemap(),
assetId: 'route-asset-1',
role: 'SAME_GROUND_COMPOSITE',
floorId: 'EXTERIOR_L1',
floorCode: 'EXTERIOR_L1',
modelVersion: 'composite-v1',
modelUrl: '/app-api/composite.glb',
coverageFloorCodes: ['EXTERIOR', 'L1'],
coverageFloorIds: ['exterior-id', 'l1-id'],
image: {
...createBasemap().image,
path: '/static/guide-floor-basemaps/EXTERIOR_L1.composite-v1.ortho-yaw54-v2.compact-floor-center-v1.webp'
}
})
describe('GuideFloorBasemap projection', () => {
afterEach(() => {
resetGuideFloorBasemapManifestCacheForTests()
vi.unstubAllGlobals()
})
it('round-trips a GLB point through the orthographic WebP projection', () => {
const basemap = createBasemap()
const frame = getGuideFloorBasemapFrame(390, 844, 1600, 1000, 'cover')
const source = { x: 18, y: 0, z: -22 }
const projected = projectPointToGuideFloorBasemap(source, basemap, frame)
const restored = unprojectGuideFloorBasemapPoint(projected, basemap, frame)
expect(restored.x).toBeCloseTo(source.x, 6)
expect(restored.z).toBeCloseTo(source.z, 6)
})
it('maps projection-basis corners to the displayed image corners', () => {
const basemap = createBasemap()
const frame = getGuideFloorBasemapFrame(390, 844, 1600, 1000, 'contain')
const projection = basemap.projection!
const toWorld = (right: number, down: number) => ({
x: projection.center.x + right * projection.screenRight.x + down * projection.screenDown.x,
y: 999,
z: projection.center.z + right * projection.screenRight.z + down * projection.screenDown.z
})
const topLeft = projectPointToGuideFloorBasemap(
toWorld(-projection.width / 2, -projection.height / 2),
basemap,
frame
)
const bottomRight = projectPointToGuideFloorBasemap(
toWorld(projection.width / 2, projection.height / 2),
basemap,
frame
)
expect(topLeft.x).toBeCloseTo(frame.offsetX, 8)
expect(topLeft.y).toBeCloseTo(frame.offsetY, 8)
expect(bottomRight.x).toBeCloseTo(frame.offsetX + frame.width, 8)
expect(bottomRight.y).toBeCloseTo(frame.offsetY + frame.height, 8)
})
it('rejects a non-orthogonal projection basis', () => {
const projection = {
...createBasemap().projection!,
screenDown: { ...screenRight }
}
expect(isValidGuideFloorOrthographicProjection(projection)).toBe(false)
})
it('ignores POI height in the orthographic projection', () => {
const basemap = createBasemap()
const frame = getGuideFloorBasemapFrame(750, 1500, 750, 1500)
expect(projectPointToGuideFloorBasemap({ x: 5, y: -100, z: 8 }, basemap, frame))
.toEqual(projectPointToGuideFloorBasemap({ x: 5, y: 100, z: 8 }, basemap, frame))
})
it('revalidates the manifest instead of force-caching an obsolete schema', async () => {
const fetchMock = vi.fn(async () => ({
ok: true,
json: async () => ({
schemaVersion: 3,
render: {
projection: 'orthographic-xz',
projectionVersion: GUIDE_FLOOR_BASEMAP_PROJECTION_VERSION
},
floors: [],
routeAssets: []
})
}))
vi.stubGlobal('fetch', fetchMock)
await loadGuideFloorBasemapManifest()
expect(fetchMock).toHaveBeenCalledWith(
'/static/guide-floor-basemaps/manifest.json',
{ cache: 'no-cache' }
)
})
it('rejects an obsolete projection identity and can recover after cache reset', async () => {
const basemap = createBasemap()
const fetchMock = vi.fn()
.mockResolvedValueOnce({
ok: true,
json: async () => ({
schemaVersion: GUIDE_FLOOR_BASEMAP_SCHEMA_VERSION,
render: {
projection: 'orthographic-xz',
projectionVersion: 'oblique-v0'
},
floors: [basemap],
routeAssets: []
})
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({
schemaVersion: GUIDE_FLOOR_BASEMAP_SCHEMA_VERSION,
render: {
projection: 'orthographic-xz',
projectionVersion: GUIDE_FLOOR_BASEMAP_PROJECTION_VERSION
},
floors: [basemap],
routeAssets: []
})
})
vi.stubGlobal('fetch', fetchMock)
const obsolete = await loadGuideFloorBasemapManifest()
expect(obsolete).toBeNull()
expect(findGuideFloorBasemap({
schemaVersion: GUIDE_FLOOR_BASEMAP_SCHEMA_VERSION,
render: {
projection: 'orthographic-xz',
projectionVersion: 'oblique-v0'
},
floors: [basemap],
routeAssets: []
}, 'L1')).toBeNull()
const recovered = await loadGuideFloorBasemapManifest()
expect(findGuideFloorBasemap(recovered, 'L1')).toEqual(basemap)
expect(fetchMock).toHaveBeenCalledTimes(2)
})
it('rejects the previous ortho-v1 cache identity', () => {
expect(isCompatibleGuideFloorBasemapManifest({
schemaVersion: 3,
render: {
projection: GUIDE_FLOOR_BASEMAP_PROJECTION,
projectionVersion: 'ortho-v1'
},
floors: [createBasemap()],
routeAssets: []
})).toBe(false)
})
it('rejects a schema 2 oblique manifest even when it contains a legacy camera projection', async () => {
const basemap = createBasemap()
const legacyBasemap: GuideFloorBasemapEntry = {
...basemap,
projection: undefined,
cameraProjection: {
type: 'perspective',
position: { x: 0, y: 100, z: 100 },
target: { x: 0, y: 0, z: 0 },
up: { x: 0, y: 1, z: 0 },
fov: 45,
near: 0.1,
far: 1000,
aspect: 1.6
}
}
const legacyManifest = {
schemaVersion: 2,
floors: [legacyBasemap],
routeAssets: []
}
const fetchMock = vi.fn(async () => ({
ok: true,
json: async () => legacyManifest
}))
vi.stubGlobal('fetch', fetchMock)
expect(findGuideFloorBasemap(legacyManifest, 'L1')).toBeNull()
expect(await loadGuideFloorBasemapManifest()).toBeNull()
})
it('uses the sole valid floor basemap when the published GLB version is newer', () => {
const basemap = createBasemap()
const manifest = {
schemaVersion: GUIDE_FLOOR_BASEMAP_SCHEMA_VERSION,
render: {
projection: GUIDE_FLOOR_BASEMAP_PROJECTION,
projectionVersion: GUIDE_FLOOR_BASEMAP_PROJECTION_VERSION
},
floors: [basemap],
routeAssets: []
}
expect(findGuideFloorBasemap(manifest, 'L1', 'v2')).toEqual(basemap)
})
it('does not guess a basemap when a floor has multiple nonmatching versions', () => {
const older = createBasemap()
const newer = { ...createBasemap(), modelVersion: 'v2' }
const manifest = {
schemaVersion: GUIDE_FLOOR_BASEMAP_SCHEMA_VERSION,
render: {
projection: GUIDE_FLOOR_BASEMAP_PROJECTION,
projectionVersion: GUIDE_FLOOR_BASEMAP_PROJECTION_VERSION
},
floors: [older, newer],
routeAssets: []
}
expect(findGuideFloorBasemap(manifest, 'L1', 'v3')).toBeNull()
})
it('requires an orthographic floor entry under the schema 4 render identity', () => {
const basemap = createBasemap()
const manifest = {
schemaVersion: GUIDE_FLOOR_BASEMAP_SCHEMA_VERSION,
render: {
projection: GUIDE_FLOOR_BASEMAP_PROJECTION,
projectionVersion: GUIDE_FLOOR_BASEMAP_PROJECTION_VERSION
},
floors: [{
...basemap,
projection: undefined,
cameraProjection: {
type: 'perspective' as const,
position: { x: 0, y: 100, z: 100 },
target: { x: 0, y: 0, z: 0 },
up: { x: 0, y: 1, z: 0 },
fov: 45,
near: 0.1,
far: 1000,
aspect: 1.6
}
}],
routeAssets: []
}
expect(findGuideFloorBasemap(manifest, 'L1')).toBeNull()
})
it('selects only the strict composite whose coverage equals the unique route floor set', () => {
const composite = createCompositeBasemap()
const manifest = {
schemaVersion: GUIDE_FLOOR_BASEMAP_SCHEMA_VERSION,
render: {
projection: GUIDE_FLOOR_BASEMAP_PROJECTION,
projectionVersion: GUIDE_FLOOR_BASEMAP_PROJECTION_VERSION
},
floors: [createBasemap()],
routeAssets: [composite]
}
expect(findGuideCompositeBasemap(manifest, ['l1-id', 'exterior-id', 'l1-id']))
.toEqual(composite)
expect(findGuideCompositeBasemap(manifest, ['L1', 'EXTERIOR'])).toEqual(composite)
expect(findGuideCompositeBasemap(manifest, ['l1-id'])).toBeNull()
expect(findGuideCompositeBasemap(manifest, ['l1-id', 'exterior-id', 'L2'])).toBeNull()
})
it('rejects schema 3 manifests instead of silently treating an ordinary exterior image as composite', () => {
const manifest = {
schemaVersion: 3,
render: {
projection: GUIDE_FLOOR_BASEMAP_PROJECTION,
projectionVersion: GUIDE_FLOOR_BASEMAP_PROJECTION_VERSION
},
floors: [createBasemap()],
routeAssets: [createCompositeBasemap()]
}
expect(isCompatibleGuideFloorBasemapManifest(manifest)).toBe(false)
expect(findGuideCompositeBasemap(manifest, ['L1', 'EXTERIOR'])).toBeNull()
})
})