t47-readability-anchor.mjs 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. import assert from 'node:assert/strict'
  2. import { readFileSync } from 'node:fs'
  3. import vm from 'node:vm'
  4. import { resolveDetailAnchor, detailAnchorLocation } from '../../../src/modules/cockpit/domain/detailAnchor.js'
  5. import { solvePointAnchor } from '../../../src/modules/cockpit/domain/pointAnchor.js'
  6. const root = new URL('../../../src/modules/cockpit/', import.meta.url)
  7. const read = (path) => readFileSync(new URL(path, root), 'utf8')
  8. const engine = read('services/cesiumEngine.js')
  9. const fn = (name) => {
  10. const match = engine.match(new RegExp(`(?:export )?function ${name}\\([^]*?\\n\\}`))
  11. assert.ok(match, `${name} exists`)
  12. return match[0].replace(/^export /, '')
  13. }
  14. const property = (value) => ({ getValue: () => value })
  15. const core = {
  16. id: 'device|MP-001|EQ-A', show: true,
  17. properties: { deviceCode: property('EQ-A') },
  18. billboard: { scale: 1, color: { alpha: 1 }, rotation: 0 },
  19. }
  20. core.billboard.pixelOffset = property({ x: 42, y: -26 })
  21. const halo = {
  22. id: 'fx|MP-001|EQ-A|ring0', show: true,
  23. properties: { effectFx: property(true), effectFxFor: property('EQ-A'), effectFxStatus: property('WARNING'), effectFxKind: property('ring'), effectFxLayer: property(0) },
  24. billboard: { scale: 1, color: { alpha: 1 }, rotation: 0 },
  25. }
  26. const source = { entities: { values: [core, halo] } }
  27. let renderRequests = 0
  28. const viewer = { scene: { requestRender() { renderRequests += 1 } } }
  29. const context = vm.createContext({
  30. deviceEffectState: { viewer, plan: { animated: [{ code: 'EQ-A', effect: 'blink' }] }, selectedCode: '', tick: 0 },
  31. deviceSources: new Map([[viewer, source]]),
  32. baseStyleOf: () => ({ scale: 0.75, alpha: 0.45 }),
  33. DEVICE_EFFECT: { SPIN: 'spin', BLINK: 'blink' },
  34. Color: { WHITE: { withAlpha: (alpha) => ({ alpha }) } },
  35. blinkStrengthAt: () => 0.25, pulseStrengthAt: () => 0,
  36. spinAngleAt: () => 1, glowStrengthAt: () => 0, glowScaleAt: () => 1,
  37. ringFrameAt: () => ({ rings: [{ scale: 2, alpha: 0.8 }] }), Date,
  38. Cartesian2: class { constructor(x, y) { this.x = x; this.y = y } },
  39. })
  40. vm.runInContext(`${fn('animatedByCode')}\n${fn('writeEffectFrame')}\nwriteEffectFrame()`, context)
  41. let failures = 0
  42. const check = (name, action) => {
  43. try { action(); console.log(`PASS ${name}`) }
  44. catch (error) { failures += 1; console.error(`FAIL ${name}: ${String(error.message).split('\n')[0]}`) }
  45. }
  46. check('center icon remains opaque at the dimmest blink phase', () => assert.equal(core.billboard.color.alpha, 1))
  47. check('center icon never shrinks below its normal size', () => assert.ok(core.billboard.scale >= 1))
  48. check('blink applies to the outer halo while center stays opaque', () => assert.equal(halo.billboard.color.alpha, 0.2))
  49. check('halo follows the expanded device pixelOffset', () => assert.equal(halo.billboard.pixelOffset, core.billboard.pixelOffset))
  50. check('active effects request a real render', () => assert.ok(renderRequests > 0))
  51. check('halo disappears with its device', () => {
  52. core.show = false
  53. vm.runInContext('writeEffectFrame()', context)
  54. assert.equal(halo.show, false)
  55. core.show = true
  56. })
  57. const objects = [{ kind: 'monitoring-point', code: 'MP-001', coordinate: { renderable: true } }]
  58. const devices = [
  59. { code: 'EQ-A', pointCode: 'MP-001', coordinate: { renderable: true } },
  60. { code: 'EQ-B', pointCode: 'MP-001', coordinate: { renderable: true } },
  61. { code: 'EQ-X', pointCode: 'MP-001', coordinate: { renderable: false, validated: true, reason: 'UNVERIFIED_COORDINATE' } },
  62. ]
  63. const target = (level, deviceCode = '') => resolveDetailAnchor({ selection: { level, pointCode: 'MP-001', deviceCode }, objects, devices })
  64. check('equipment A anchors to A while keeping point ownership', () => {
  65. assert.equal(target('equipment', 'EQ-A').code, 'EQ-A')
  66. assert.equal(target('equipment', 'EQ-A').pointCode, 'MP-001')
  67. assert.equal(target('equipment', 'EQ-A').kind, 'monitoring-device')
  68. })
  69. check('equipment A to B changes anchor identity', () => assert.equal(target('equipment', 'EQ-B').code, 'EQ-B'))
  70. check('back to point summary restores point anchor', () => assert.equal(target('point', 'EQ-B').code, 'MP-001'))
  71. check('no-location equipment uses its own verdict', () => assert.equal(detailAnchorLocation(target('equipment', 'EQ-X')).reasonText, '坐标未经校验'))
  72. check('missing equipment does not borrow its parent location', () => assert.equal(detailAnchorLocation(target('equipment', 'EQ-missing')).present, false))
  73. check('unassociated equipment anchors to itself', () => {
  74. const standalone = { kind: 'unassociated-equipment', code: 'EQ-U' }
  75. assert.equal(resolveDetailAnchor({ selection: { level: 'equipment', deviceCode: 'EQ-U' }, objects: [standalone] }).object, standalone)
  76. })
  77. check('closed detail has no anchor', () => assert.equal(target('none', 'EQ-A'), null))
  78. check('shell consumes anchor policy and includes line movement in its update key', () => {
  79. assert.ok(/resolveDetailAnchor\(/.test(read('CockpitShell.vue')))
  80. assert.ok(/JSON.stringify\(layout.line\)/.test(read('CockpitShell.vue')))
  81. })
  82. // Execute the production projection with a small Cesium public-API double.
  83. const entity = {
  84. isShowing: true, show: true, position: property({ longitude: 1, latitude: 2 }),
  85. properties: { deviceCode: property('EQ-A'), monitoringPointCode: property('MP-001') },
  86. billboard: { pixelOffset: property({ x: 42, y: -26 }), scale: property(1.6), width: property(14), height: property(14) },
  87. }
  88. const projectionViewer = {
  89. clock: { currentTime: 0 }, canvas: { getBoundingClientRect: () => ({ left: 12, top: 102 }) },
  90. scene: { globe: { ellipsoid: { cartesianToCartographic: (p) => p }, getHeight: () => 123 } },
  91. camera: { positionWC: {}, directionWC: {} },
  92. }
  93. function Cartesian3() {}
  94. Cartesian3.fromRadians = (longitude, latitude, height) => ({ longitude, latitude, height })
  95. Cartesian3.subtract = () => ({})
  96. Cartesian3.dot = () => 1
  97. const projectionContext = vm.createContext({
  98. Cartesian3,
  99. EllipsoidalOccluder: class { isPointVisible() { return true } },
  100. SceneTransforms: { worldToWindowCoordinates: (_scene, position) => { assert.equal(position.height, 123); return { x: 500, y: 320 } } },
  101. deviceSources: new Map([[projectionViewer, { show: true, entities: { values: [entity] } }]]),
  102. viewer: projectionViewer,
  103. })
  104. vm.runInContext(`${fn('findDeviceMarker')}\n${fn('projectMarkerEntity')}\n${fn('projectDeviceMarker')}`, projectionContext)
  105. check('device anchor includes terrain, fan pixelOffset and selected icon size', () => {
  106. const at = vm.runInContext("projectDeviceMarker(viewer, 'EQ-A', 'MP-001')", projectionContext)
  107. assert.equal(at.x, 554)
  108. assert.equal(at.y, 396)
  109. assert.equal(at.iconSize, 22.400000000000002)
  110. const layout = solvePointAnchor({ anchor: at, canvas: { left: 12, top: 102, right: 1650, bottom: 842 }, iconSize: at.iconSize })
  111. assert.ok(layout.rect.left > at.x + at.iconSize / 2)
  112. assert.equal(layout.line.from.x, at.x + at.iconSize / 2)
  113. })
  114. check('hidden fan device has no fake parent anchor', () => {
  115. entity.show = false
  116. assert.equal(vm.runInContext("projectDeviceMarker(viewer, 'EQ-A', 'MP-001')", projectionContext), null)
  117. })
  118. check('effects off cancels the loop without hiding the center icon', () => {
  119. const cancelled = []
  120. context.window = { cancelAnimationFrame: (id) => cancelled.push(id) }
  121. context.clearRingEntities = () => {}
  122. context.deviceEffectState.raf = 17
  123. vm.runInContext(`${fn('applyDeviceEffects')}\napplyDeviceEffects(deviceEffectState.viewer, { enabled: false, animated: [] })`, context)
  124. assert.deepEqual(cancelled, [17])
  125. assert.equal(context.deviceEffectState.raf, 0)
  126. assert.equal(core.billboard.color.alpha, 1)
  127. })
  128. check('wrong parent identity never resolves another device marker', () => {
  129. entity.show = true
  130. assert.equal(vm.runInContext("projectDeviceMarker(viewer, 'EQ-A', 'MP-other')", projectionContext), null)
  131. })
  132. check('pending device does not pretend to have a known location failure', () => {
  133. assert.equal(detailAnchorLocation({ code: 'EQ-pending', object: { placement: 'pending' } }).present, false)
  134. })
  135. console.log(`T47 readability/anchor: ${failures ? 'FAILED' : 'passed'}`)
  136. process.exitCode = failures ? 1 : 0