t22-pipeline-rendering.mjs 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import assert from 'node:assert/strict'
  2. import { readFileSync } from 'node:fs'
  3. import vm from 'node:vm'
  4. import * as Cesium from 'cesium'
  5. import { toPipeNetwork, filterPipeNetwork } from '../../../src/modules/cockpit/domain/pipeNetwork.js'
  6. import { planModeLayers } from '../../../src/modules/cockpit/domain/modeLayerPlan.js'
  7. import { planViewMode } from '../../../src/modules/cockpit/domain/viewModes.js'
  8. import { syncPipeLayer, clearPipeLayer } from '../../../src/modules/cockpit/services/cesiumPipeLayer.js'
  9. // Offline only: real domain functions and Cesium entities; GPU picking returns controlled hits.
  10. // No network, server, browser, file mutations, or in-place mutation testing.
  11. const source = readFileSync(new URL('../../../src/modules/cockpit/services/cesiumEngine.js', import.meta.url), 'utf8')
  12. const code = source.replace(/^import\s+[^]*?from\s+['"][^'"\n]+['"][ \t]*$/gm, '')
  13. .replace(/^import\s+['"][^'"\n]+['"][ \t]*$/gm, '')
  14. .replace(/^export /gm, '')
  15. const context = vm.createContext({ ...Cesium, CesiumMath: Cesium.Math })
  16. vm.runInContext(`${code}\nglobalThis.engine = { pickPointMarkerAt, pickDeviceMarkerAt };`, context)
  17. const engine = context.engine
  18. let failed = 0
  19. let passed = 0
  20. async function check(name, action) {
  21. try { await action(); passed += 1; console.log(`PASS ${name}`) }
  22. catch (error) { failed += 1; console.error(`FAIL ${name}: ${error.message}`) }
  23. }
  24. const path = [[110.39, 28.44], [110.395, 28.445], [110.40, 28.45]]
  25. function feature(coordinates = path, properties = {}) {
  26. return { type: 'Feature', id: 'P-1', geometry: { type: 'LineString', coordinates }, properties: {
  27. code: 'P-1', industry: 'GAS', diameter: '400', material: '钢',
  28. coordinateValidation: { renderable: true }, ...properties,
  29. } }
  30. }
  31. const network = toPipeNetwork({ features: [feature()] })
  32. await check('完整路径、源管径和可选材质原样映射', () => {
  33. assert.deepEqual(network.pipes[0].positions, path)
  34. assert.equal(network.pipes[0].diameter, 400)
  35. assert.equal(network.pipes[0].material, '钢')
  36. const missing = toPipeNetwork({ features: [feature(path, { diameter: null, material: null })] }).pipes[0]
  37. assert.equal(missing.diameter, null)
  38. assert.equal(missing.material, '')
  39. })
  40. await check('任一坏顶点整条不上图,保留清单,不跨点重连', () => {
  41. for (const bad of [null, [], ['110.395', 28.445], [NaN, 28.445], [110.395, Infinity]]) {
  42. const result = toPipeNetwork({ features: [feature([path[0], bad, path[2]])] })
  43. assert.equal(result.pipes.length, 0)
  44. assert.equal(result.blocked.length, 1)
  45. assert.equal(result.blocked[0].code, 'P-1')
  46. assert.equal(result.blocked[0].reasonText, '无地图位置')
  47. }
  48. })
  49. await check('后端未准入即不上图,不用合法结构替代后端校核', () => {
  50. const result = toPipeNetwork({ features: [feature(path, { coordinateValidation: { renderable: false, reason: 'UNVERIFIED_COORDINATE' } })] })
  51. assert.equal(result.pipes.length, 0)
  52. assert.equal(result.blocked[0].reasonText, '坐标未经校验')
  53. })
  54. await check('行业筛选同样裁剪无地图位置清单,不修改原始网络', () => {
  55. const all = toPipeNetwork({ features: [feature(), feature(path, { code: 'W', industry: 'WATER_SUPPLY' }), feature([path[0]], { code: 'X', industry: 'WATER_SUPPLY' })] })
  56. assert.deepEqual(filterPipeNetwork(all, { industry: 'gas' }).pipes.map((p) => p.code), ['P-1'])
  57. assert.equal(filterPipeNetwork(all, { industry: 'gas' }).blocked.length, 0)
  58. const water = filterPipeNetwork(all, { industry: 'water' })
  59. assert.deepEqual(water.pipes.map((p) => p.code), ['W'])
  60. assert.equal(water.blocked.length, 1)
  61. assert.equal(filterPipeNetwork(all, { industry: 'all' }).pipes.length, 2)
  62. assert.equal(all.blocked.length, 1)
  63. })
  64. await check('四种模式均保留管线,引导态隐藏,切视图不裁管线', () => {
  65. for (const mode of ['overview', 'infrastructure', 'monitoring', 'warningResponse']) {
  66. const input = { mode, entered: true, objects: [], facilities: [], pipelines: network.pipes }
  67. assert.equal(planModeLayers(input).pipelines, network.pipes)
  68. assert.equal(planModeLayers({ ...input, entered: false }).pipelines.length, 0)
  69. }
  70. for (const mode of ['ground', 'pipeline']) {
  71. const plan = planViewMode(mode)
  72. assert.ok(plan.terrainAlpha > 0 && plan.tilesetAlpha > 0)
  73. assert.equal(plan.terrainAlpha < 1, mode === 'pipeline')
  74. assert.equal(plan.tilesetAlpha < 1, mode === 'pipeline')
  75. }
  76. })
  77. const pipes = new Cesium.CustomDataSource('cockpit-pipelines')
  78. const devices = new Cesium.CustomDataSource('cockpit-associated-device-markers')
  79. const pipe = pipes.entities.add({ id: 'pipe:P-1', properties: { kind: 'pipe', code: 'P-1' } })
  80. const component = pipes.entities.add({ id: 'component:C-1', properties: { kind: 'component', code: 'C-1' } })
  81. const point = new Cesium.Entity({ id: 'point:M', properties: { pointCode: 'M', pointKind: 'monitoring-point', pointStatus: 'NORMAL' } })
  82. const device = new Cesium.Entity({ id: 'device:D', properties: { deviceCode: 'D', monitoringPointCode: 'M', deviceStatus: 'NORMAL' } })
  83. const model = { kind: 'pipe-model-batch', key: 'batch', instances: ['pipe:P-1'] }
  84. const building = { id: 'building' }
  85. let hits = []
  86. let drills = 0
  87. let pickedPosition = null
  88. const viewer = {
  89. dataSources: { getByName: (name) => name === pipes.name ? [pipes] : name === devices.name ? [devices] : [] },
  90. canvas: { getBoundingClientRect: () => ({ left: 20, top: 30 }) },
  91. scene: {
  92. pick(position) { pickedPosition = position; return hits[0] },
  93. drillPick() { drills += 1; return hits },
  94. },
  95. }
  96. await check('普通点选不触发多层拾取,窗口坐标转画布坐标', () => {
  97. hits = [{ id: point }]; drills = 0
  98. assert.equal(engine.pickPointMarkerAt(viewer, 120, 230).code, 'M')
  99. assert.equal(drills, 0)
  100. assert.equal(pickedPosition.x, 100)
  101. assert.equal(pickedPosition.y, 200)
  102. })
  103. await check('管线、构件、模型都不能吞掉监测点', () => {
  104. for (const foreground of [pipe, component, model]) {
  105. hits = [{ id: foreground }, { id: point }]
  106. assert.equal(engine.pickPointMarkerAt(viewer, 120, 230).code, 'M')
  107. }
  108. })
  109. await check('管线遮挡设备时仍回设备身份与父点位', () => {
  110. hits = [{ id: model }, { id: pipe }, { id: device }]
  111. const hit = engine.pickDeviceMarkerAt(viewer, 120, 230)
  112. assert.equal(hit.code, 'D')
  113. assert.equal(hit.pointCode, 'M')
  114. })
  115. await check('不穿透建筑/其它对象,也不把单独管线当监测点或设备', () => {
  116. for (const list of [[{ id: building }, { id: point }], [{ id: pipe }, { id: building }, { id: point }], [{ id: model }]]) {
  117. hits = list
  118. assert.equal(engine.pickPointMarkerAt(viewer, 120, 230), null)
  119. assert.equal(engine.pickDeviceMarkerAt(viewer, 120, 230), null)
  120. }
  121. })
  122. await check('名字相似但不属于管线源的对象不能被穿透', () => {
  123. hits = [{ id: new Cesium.Entity({ id: 'pipe:foreign' }) }, { id: point }]
  124. assert.equal(engine.pickPointMarkerAt(viewer, 120, 230), null)
  125. })
  126. await check('无多层拾取能力时不伪造命中', () => {
  127. hits = [{ id: pipe }, { id: point }]
  128. const limited = { ...viewer, scene: { pick: viewer.scene.pick } }
  129. assert.equal(engine.pickPointMarkerAt(limited, 120, 230), null)
  130. })
  131. await check('真实Cesium折线写入且相同输入不重赋值,筛空后移除', async () => {
  132. const target = { dataSources: new Cesium.DataSourceCollection() }
  133. const first = await syncPipeLayer(target, network)
  134. assert.equal(first.pipes, 1)
  135. const entity = target.dataSources.getByName(pipes.name)[0].entities.getById('pipe:P-1')
  136. assert.equal(entity.polyline.positions.getValue().length, path.length)
  137. const positions = entity.polyline.positions
  138. const second = await syncPipeLayer(target, network)
  139. assert.equal(second.unchanged, 1)
  140. assert.equal(entity.polyline.positions, positions)
  141. const empty = await syncPipeLayer(target, { pipes: [], components: [] })
  142. assert.equal(empty.removed, 1)
  143. await clearPipeLayer(target)
  144. })
  145. console.log(`T22: ${passed} passed, ${failed} failed`)
  146. process.exitCode = failed ? 1 : 0