t55-detail-usability.mjs 8.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. import assert from 'node:assert/strict'
  2. import * as THREE from 'three'
  3. import { planPipeEnhancement } from '../../../src/modules/cockpit/domain/pipeEnhancement.js'
  4. import { planEnhancementDevices } from '../../../src/modules/cockpit/domain/enhancementDevices.js'
  5. import { planPipeModels } from '../../../src/modules/cockpit/domain/pipeModels.js'
  6. import { mountPipeEnhancement } from '../../../src/modules/cockpit/services/threePipeEnhancement.js'
  7. import { pipeSurface } from '../../../src/modules/cockpit/domain/pipePhysical.js'
  8. import { pipeSurfaceMaterial } from '../../../src/modules/cockpit/services/pipeSurfaceMaterial.js'
  9. const row = { code: 'P', industry: 'WATER_SUPPLY', diameter: 400, material: 'PE', positions: [[110, 28], [110.001, 28]] }
  10. const next = { ...row, code: 'N', material: '钢', positions: [[110, 28.00015], [110.001, 28.00015]] }
  11. const plan = planPipeEnhancement({ selected: { kind: 'pipe', code: 'P' }, visiblePipes: [row, next] })
  12. function fixture(plan, options = {}) {
  13. const canvas = new EventTarget()
  14. canvas.getBoundingClientRect = () => ({ left: 0, top: 0, width: 900, height: 500 })
  15. const host = { clientWidth: 900, clientHeight: 500, appendChild(n) { n.parentNode = this }, removeChild(n) { n.parentNode = null } }
  16. let scene, camera
  17. const viewer = mountPipeEnhancement(host, plan, { ...options, rendererFactory: () => ({ domElement: canvas, setSize() {}, dispose() {}, render(s, c) { scene = s; camera = c; s.updateMatrixWorld(true); c.updateMatrixWorld() } }) })
  18. return { viewer, canvas, scene: () => scene, camera: () => camera }
  19. }
  20. function event(canvas, type, props = {}) { const e = new Event(type, { cancelable: true }); Object.assign(e, props); canvas.dispatchEvent(e) }
  21. let failed = 0, passed = 0
  22. function check(name, action) { try { action(); passed++; console.log(`PASS ${name}`) } catch (error) { failed++; console.error(`FAIL ${name}: ${error.message}`) } }
  23. check('主图与详情恢复8倍显示直径但保留0.4米源径', () => {
  24. const model = planPipeModels({ pipes: [row] }).instances[0]
  25. assert.equal(model.diameterM, 0.4)
  26. assert.equal(model.displayDiameterM, 3.2)
  27. const f = fixture(plan)
  28. try { const wall = f.scene().getObjectByName('pipe-wall'); wall.geometry.computeBoundingBox(); assert.ok(Math.abs(wall.geometry.boundingBox.max.x - 1.6) < 1e-6) }
  29. finally { f.viewer.destroy() }
  30. })
  31. check('基础设施模式的管道详情仍可查看真实附近设备,设备位于放大管壁之外', () => {
  32. const devices = planEnhancementDevices(plan, [{ code: 'D', name: '压力设备', industry: 'WATER_SUPPLY', coordinate: { renderable: true }, longitude: 110.0005, latitude: 28 }], { mode: 'infrastructure' })
  33. assert.equal(devices.devices.length, 1)
  34. const f = fixture({ ...plan, ...devices })
  35. try { const device = f.scene().getObjectByName('device:D'); const pipe = f.scene().getObjectByName('pipe-wall'); const world = device.getWorldPosition(new THREE.Vector3()); const box = new THREE.Box3().setFromObject(pipe); assert.ok(world.y > box.max.y + 0.15) }
  36. finally { f.viewer.destroy() }
  37. })
  38. check('100米管段可缩放到厘米级,近处同像素拖拽速度随视距降低', () => {
  39. const f = fixture(plan)
  40. try {
  41. const pan = () => { const before = f.camera().position.clone(); event(f.canvas, 'pointerdown', { button: 0, pointerId: 1, clientX: -100, clientY: -100 }); event(f.canvas, 'pointermove', { pointerId: 1, clientX: -90, clientY: -100 }); event(f.canvas, 'pointerup', { pointerId: 1 }); return f.camera().position.distanceTo(before) }
  42. const farPan = pan()
  43. // Fresh viewer, so pan offset is not mistaken for the camera's orbit range.
  44. f.viewer.destroy()
  45. const near = fixture(plan)
  46. try { for (let i = 0; i < 18; i++) event(near.canvas, 'wheel', { deltaY: -1000, clientX: -100, clientY: -100 }); assert.ok(near.camera().position.distanceTo(new THREE.Box3().setFromObject(near.scene().getObjectByName('pipe-enhancement:P')).getCenter(new THREE.Vector3())) < 0.1, '最小视距不能随整个场景半径放大到米级'); const before = near.camera().position.clone(); event(near.canvas, 'pointerdown', { button: 0, pointerId: 1, clientX: -100, clientY: -100 }); event(near.canvas, 'pointermove', { pointerId: 1, clientX: -90, clientY: -100 }); event(near.canvas, 'pointerup', { pointerId: 1 }); assert.ok(near.camera().position.distanceTo(before) < farPan / 100) }
  47. finally { near.viewer.destroy() }
  48. } finally { f.viewer.destroy() }
  49. })
  50. check('真实射线可点选邻近管道身份,拖拽不触发', () => {
  51. let selected
  52. const f = fixture(plan, { onObjectPick: (hit) => { selected = hit } })
  53. try {
  54. const mesh = f.scene().getObjectByName('neighbor-pipe-wall')
  55. const point = mesh.getWorldPosition(new THREE.Vector3()).project(f.camera())
  56. const x = (point.x + 1) * 450, y = (1 - point.y) * 250
  57. event(f.canvas, 'pointerdown', { button: 0, pointerId: 1, clientX: x, clientY: y }); event(f.canvas, 'pointerup', { button: 0, pointerId: 1, clientX: x, clientY: y })
  58. assert.equal(selected?.code, 'N'); assert.equal(selected?.objectKind, 'pipe')
  59. selected = null
  60. event(f.canvas, 'pointerdown', { button: 0, pointerId: 1, clientX: x, clientY: y }); event(f.canvas, 'pointermove', { pointerId: 1, clientX: x + 40, clientY: y }); event(f.canvas, 'pointerup', { button: 0, pointerId: 1, clientX: x + 40, clientY: y }); assert.equal(selected, null)
  61. } finally { f.viewer.destroy() }
  62. })
  63. check('材质有不同表面纹理/凹凸,钢与PE及混凝土不能仅靠微小反射差异', () => {
  64. const values = ['钢', 'PE', '混凝土'].map((s) => pipeSurfaceMaterial('#168bff', pipeSurface(s)))
  65. try { assert.ok(values.every((m) => m.bumpMap || m.normalMap)); assert.notEqual(values[0].bumpMap, values[1].bumpMap); assert.notEqual(values[1].bumpScale, values[2].bumpScale) }
  66. finally { for (const m of values) { m.bumpMap?.dispose(); m.dispose() } }
  67. })
  68. check('新设备渐进返回不挪动已聚焦设备,且可从清单聚焦模型', () => {
  69. const device = { code: 'D', industry: 'WATER_SUPPLY', coordinate: { renderable: true }, longitude: 110.0005, latitude: 28 }
  70. const first = planEnhancementDevices(plan, [device])
  71. const second = planEnhancementDevices(plan, [device, { ...device, code: 'later' }])
  72. assert.deepEqual(first.devices[0].position, second.devices.find((d) => d.code === 'D').position)
  73. const f = fixture({ ...plan, ...first })
  74. try { assert.equal(f.viewer.focusDevice('D'), true); const before = f.camera().position.clone(); f.viewer.sync({ ...plan, ...second }); assert.ok(f.camera().position.distanceTo(before) < 1e-6); assert.equal(f.viewer.focusDevice('not-loaded'), false) }
  75. finally { f.viewer.destroy() }
  76. })
  77. check('细管设备不被8倍相邻粗管包住', () => {
  78. const small = { ...row, code: 'small', diameter: 50, networkId: 'small-id' }
  79. const large = { ...row, code: 'large', positions: row.positions.map((p) => [p[0], p[1] + 0.5 / 111320]) }
  80. const local = planPipeEnhancement({ selected: { kind: 'pipe', code: 'small' }, visiblePipes: [small, large] })
  81. const layout = planEnhancementDevices(local, [{ code: 'D', industry: 'WATER_SUPPLY', networkId: 'small-id' }])
  82. const f = fixture({ ...local, ...layout })
  83. try { const device = f.scene().getObjectByName('device:D'); const pipe = f.scene().getObjectByName('neighbor-pipe-wall'); const deviceBox = new THREE.Box3().setFromObject(device.getObjectByName('device-body')); const pipeBox = new THREE.Box3().setFromObject(pipe); assert.ok(deviceBox.min.y > pipeBox.max.y) }
  84. finally { f.viewer.destroy() }
  85. })
  86. check('偏心邻管滚轮沿鼠标射线接近、朝向不变', () => {
  87. const f = fixture(plan)
  88. try { const pipe = f.scene().getObjectByName('neighbor-pipe-wall'); const point = pipe.localToWorld(new THREE.Vector3(0, 20, 0)); const before = f.camera().position.clone(); const orientation = f.camera().quaternion.clone(); const projected = point.clone().project(f.camera()); event(f.canvas, 'wheel', { deltaY: -120, clientX: (projected.x + 1) * 450, clientY: (1 - projected.y) * 250 }); const movement = f.camera().position.clone().sub(before).normalize(); const expected = point.sub(before).normalize(); assert.ok(movement.dot(expected) > 0.9999); assert.ok(f.camera().quaternion.angleTo(orientation) < 1e-6) }
  89. finally { f.viewer.destroy() }
  90. })
  91. console.log(`T55 local usability: ${passed} passed, ${failed} failed`)
  92. if (failed) process.exitCode = 1