t45-device-camera-height.mjs 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. import assert from 'node:assert/strict'
  2. import { readFileSync } from 'node:fs'
  3. import vm from 'node:vm'
  4. import { createCameraControl } from '../../../src/modules/cockpit/services/cameraControl.js'
  5. function fixture() {
  6. let now = 0
  7. let frame = null
  8. const applied = []
  9. const control = createCameraControl({ engine: {
  10. now: () => now,
  11. requestFrame: (callback) => { frame = callback },
  12. applyView: (pivot, hpr) => applied.push({ pivot: { ...pivot }, hpr: { ...hpr } }),
  13. pickGroundAtCenter: () => null,
  14. viewport: () => ({ width: 1680, height: 900 }),
  15. fovY: () => Math.PI / 3,
  16. } })
  17. const settle = () => {
  18. for (let n = 0; frame && n < 500; n += 1) {
  19. const callback = frame; frame = null; now += 16; callback(now)
  20. }
  21. assert.equal(control.getState().settled, true)
  22. }
  23. control.applyViewPreset({ lon: 110.4, lat: 28.44, height: 320 }, { heading: 0, pitch: -55, range: 150 }, { immediate: true })
  24. return { control, settle, applied }
  25. }
  26. let failed = 0
  27. function check(name, fn) {
  28. try { fn(); console.log(`PASS ${name}`) }
  29. catch (error) { failed += 1; console.error(`FAIL ${name}: ${error.message}`) }
  30. }
  31. check('pan then select device preserves sampled ground Z and stays above terrain', () => {
  32. const { control, settle, applied } = fixture()
  33. control.beginDrag({ button: 0, x: 500, y: 300 })
  34. control.moveDrag({ x: 560, y: 320 })
  35. control.endDrag(); settle()
  36. const before = control.getState().hpr
  37. const device = { lon: 110.4001, lat: 28.4401, height: 345 }
  38. control.setFocus(device); settle()
  39. const state = control.getState()
  40. assert.equal(state.pivot.height, device.height, 'device ground height must not be replaced by ellipsoid zero')
  41. assert.equal(state.focus.height, device.height)
  42. assert.deepEqual(state.hpr, before)
  43. const last = applied.at(-1)
  44. const cameraHeight = last.pivot.height + last.hpr.range * Math.sin(-last.hpr.pitch * Math.PI / 180)
  45. assert.ok(cameraHeight > device.height + 10, `camera ${cameraHeight} must clear ground ${device.height}`)
  46. assert.ok(applied.every((view) => view.pivot.height >= 320), 'flight must not interpolate down to zero')
  47. })
  48. check('same XY device selection still updates newly sampled Z', () => {
  49. const { control, settle } = fixture()
  50. control.setFocus({ lon: 110.4, lat: 28.44, height: 360 }); settle()
  51. assert.equal(control.getState().pivot.height, 360)
  52. })
  53. check('orbit drag pending selection preserves device ground height', () => {
  54. const { control, settle } = fixture()
  55. control.beginDrag({ button: 2, x: 500, y: 300 })
  56. control.setFocus({ lon: 110.401, lat: 28.441, height: 350 })
  57. control.endDrag(); settle()
  58. assert.equal(control.getState().pivot.height, 350)
  59. })
  60. const service = readFileSync(new URL('../../../src/modules/cockpit/services/mapService.js', import.meta.url), 'utf8')
  61. const extract = (name, async = false) => service.match(new RegExp(`export ${async ? 'async ' : ''}function ${name}\\([^]*?\\n\\}`))[0].replace(/^export /, '')
  62. const { control, settle } = fixture()
  63. control.beginDrag({ button: 0, x: 500, y: 300 })
  64. control.moveDrag({ x: 580, y: 350 })
  65. control.endDrag(); settle()
  66. const pending = []
  67. const record = { cameraControl: control, viewer: {}, engine: {
  68. sampleTerrainHeight: () => new Promise((resolve) => pending.push(resolve)),
  69. groundHeightAt: () => 345,
  70. } }
  71. const context = vm.createContext({
  72. instance: record, pointLocateToken: 0,
  73. isRenderable: (object) => object.coordinate?.renderable === true,
  74. isStaleLocate: (mine, current) => mine !== current,
  75. TERRAIN_STATE: { NONE: 'none' }, terrainState: 'ready',
  76. })
  77. for (const name of ['cancelPointLocate', 'setCameraFocus']) vm.runInContext(extract(name), context)
  78. vm.runInContext(extract('locateMonitoringPoint', true), context)
  79. context.deviceA = { longitude: 110.4001, latitude: 28.4401, coordinate: { renderable: true } }
  80. context.deviceB = { longitude: 110.4002, latitude: 28.4402, coordinate: { renderable: true } }
  81. const first = vm.runInContext("locateMonitoringPoint(deviceA, { tier: 'device', centerOnly: true })", context)
  82. pending.shift()(345)
  83. await first; settle()
  84. check('service terrain sampling survives real setCameraFocus and controller', () => {
  85. assert.equal(control.getState().pivot.height, 345)
  86. assert.ok(Math.abs(control.getState().pivot.lon - context.deviceA.longitude) < 1e-9)
  87. })
  88. const stale = vm.runInContext("locateMonitoringPoint(deviceA, { tier: 'device', centerOnly: true })", context)
  89. const latest = vm.runInContext("locateMonitoringPoint(deviceB, { tier: 'device', centerOnly: true })", context)
  90. const resolveStale = pending.shift()
  91. const resolveLatest = pending.shift()
  92. resolveLatest(370); await latest
  93. resolveStale(100); await stale
  94. settle()
  95. check('late terrain response cannot move camera down to previously selected device', () => {
  96. assert.ok(Math.abs(control.getState().pivot.lon - context.deviceB.longitude) < 1e-9)
  97. assert.equal(control.getState().pivot.height, 370)
  98. })
  99. console.log(`T45 device camera height: ${failed ? 'FAILED' : 'passed'}`)
  100. process.exitCode = failed ? 1 : 0