t50-pipe-detail.mjs 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  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 { mountPipeEnhancement } from '../../../src/modules/cockpit/services/threePipeEnhancement.js'
  5. const source = { code: 'hollow-pipe', industry: 'WATER_SUPPLY', diameter: 400, material: 'PE', positions: [[110, 28], [110.001, 28]] }
  6. const original = JSON.stringify(source)
  7. const plan = planPipeEnhancement({ selected: { kind: 'pipe', code: source.code }, visiblePipes: [source] })
  8. class Canvas extends EventTarget {}
  9. const host = { clientWidth: 1000, clientHeight: 600,
  10. ownerDocument: { createElement: () => new Canvas() }, children: [],
  11. appendChild(node) { node.parentNode = this; this.children.push(node) },
  12. removeChild(node) { this.children.splice(this.children.indexOf(node), 1); node.parentNode = null } }
  13. let scene
  14. const viewer = mountPipeEnhancement(host, plan, { rendererFactory: ({ canvas }) => ({
  15. domElement: canvas, setPixelRatio() {}, setSize() {}, render(value) { scene = value; scene.updateMatrixWorld(true) }, dispose() {},
  16. }) })
  17. const root = scene.children.find((n) => n.name === `pipe-enhancement:${source.code}`)
  18. const meshes = []
  19. root.traverse((node) => { if (node.isMesh) meshes.push(node) })
  20. const bounds = new THREE.Box3().setFromObject(root)
  21. const ray = new THREE.Raycaster(new THREE.Vector3(bounds.min.x - 1, 0, 0), new THREE.Vector3(1, 0, 0))
  22. let failures = 0
  23. let passes = 0
  24. function check(name, action) {
  25. try { action(); passes += 1; console.log(`PASS ${name}`) }
  26. catch (error) { failures += 1; console.error(`FAIL ${name}: ${error.message}`) }
  27. }
  28. check('轴向穿过中空管腔,不被端盖封死', () => {
  29. assert.equal(ray.intersectObject(root, true).length, 0)
  30. ray.set(new THREE.Vector3(bounds.getCenter(new THREE.Vector3()).x, 2, 0), new THREE.Vector3(0, -1, 0))
  31. assert.ok(ray.intersectObject(root, true).length > 0, '管壁须有实体表面')
  32. })
  33. check('端部有法兰与一圈螺栓,沿管轴附着', () => {
  34. const flanges = meshes.filter((m) => m.name === 'pipe-flange')
  35. const bolts = meshes.filter((m) => m.name === 'pipe-bolt')
  36. assert.equal(flanges.length, 2)
  37. assert.equal(bolts.length, 16)
  38. for (const flange of flanges) {
  39. const p = flange.getWorldPosition(new THREE.Vector3())
  40. assert.ok(Math.abs(p.y) < 1e-7 && Math.abs(p.z) < 1e-7)
  41. }
  42. })
  43. check('有分节接缝,没有独立定位环', () => {
  44. assert.ok(meshes.some((m) => m.name === 'pipe-seam'))
  45. assert.ok(!meshes.some((m) => m.geometry.type === 'TorusGeometry'), '旧中心定位环必须移除')
  46. })
  47. check('管体保持供水行业色,不改源路径与示意说明', () => {
  48. const body = meshes.find((m) => m.name === 'pipe-wall')
  49. assert.ok(body)
  50. assert.equal(body.material.color.getHexString(), '168bff')
  51. assert.equal(JSON.stringify(source), original)
  52. assert.match(plan.disclaimer, /不代表工程竣工模型/)
  53. })
  54. function trackResources(model) {
  55. const resources = new Map()
  56. model.traverse((node) => {
  57. for (const resource of [node.geometry, ...[].concat(node.material || [])].filter(Boolean)) {
  58. if (resources.has(resource)) continue
  59. resources.set(resource, 0)
  60. resource.addEventListener('dispose', () => resources.set(resource, resources.get(resource) + 1))
  61. }
  62. })
  63. return () => {
  64. assert.ok(resources.size > 0)
  65. for (const count of resources.values()) assert.ok(count > 0, '每个几何与材质都须释放')
  66. }
  67. }
  68. const assertInitialReleased = trackResources(root)
  69. viewer.sync({ ...plan, object: { ...plan.object, code: 'replacement' } })
  70. check('换选移除旧模型并释放全部几何与材质', () => {
  71. assert.equal(root.parent, null)
  72. assertInitialReleased()
  73. })
  74. check('短管段保持真实长径比例,不为可见性拉长或加粗', () => {
  75. const tiny = { ...plan, object: { ...plan.object, path: [[110, 28], [110.00001, 28]], position: [110, 28] } }
  76. viewer.sync(tiny)
  77. const next = scene.children.find((n) => n.name === `pipe-enhancement:${source.code}`)
  78. const box = new THREE.Box3().setFromObject(next)
  79. const size = box.getSize(new THREE.Vector3())
  80. assert.ok(size.x > 0.98 && size.x < 1.02)
  81. assert.ok(size.y > 0.4 && size.y < 0.6)
  82. assert.ok(box.getCenter(new THREE.Vector3()).length() < 1e-6)
  83. })
  84. check('纵向管段法兰随路径定向,轴向管腔不封死', () => {
  85. viewer.sync({ ...plan, object: { ...plan.object, path: [[110, 28], [110, 28.001]] } })
  86. const next = scene.children.find((n) => n.name === `pipe-enhancement:${source.code}`)
  87. const box = new THREE.Box3().setFromObject(next)
  88. ray.set(new THREE.Vector3(0, 0, box.min.z - 1), new THREE.Vector3(0, 0, 1))
  89. assert.equal(ray.intersectObject(next, true).length, 0)
  90. })
  91. check('折线共享拐角不堆叠法兰,仅真实两端有法兰', () => {
  92. viewer.sync({ ...plan, object: { ...plan.object, path: [[110, 28], [110.001, 28], [110.001, 28.001]] } })
  93. const next = scene.children.find((n) => n.name === `pipe-enhancement:${source.code}`)
  94. const flanges = []
  95. next.traverse((n) => { if (n.name === 'pipe-flange') flanges.push(n) })
  96. assert.equal(flanges.length, 2, '折点不能额外生成一对法兰')
  97. })
  98. check('关闭释放当前模型全部几何材质、移除画布并可重复销毁', () => {
  99. const currentRoot = scene.children.find((n) => n.name === `pipe-enhancement:${source.code}`)
  100. const assertCurrentReleased = trackResources(currentRoot)
  101. viewer.destroy()
  102. assertCurrentReleased()
  103. viewer.destroy()
  104. assert.equal(host.children.length, 0)
  105. assert.equal(viewer.getState().status, 'destroyed')
  106. })
  107. console.log(`T50 pipe detail: ${passes} passed, ${failures} failed`)
  108. if (failures) process.exitCode = 1