import assert from 'node:assert/strict' import * as THREE from 'three' import { planPipeEnhancement } from '../../../src/modules/cockpit/domain/pipeEnhancement.js' import { mountPipeEnhancement } from '../../../src/modules/cockpit/services/threePipeEnhancement.js' const source = { code: 'hollow-pipe', industry: 'WATER_SUPPLY', diameter: 400, material: 'PE', positions: [[110, 28], [110.001, 28]] } const original = JSON.stringify(source) const plan = planPipeEnhancement({ selected: { kind: 'pipe', code: source.code }, visiblePipes: [source] }) class Canvas extends EventTarget {} const host = { clientWidth: 1000, clientHeight: 600, ownerDocument: { createElement: () => new Canvas() }, children: [], appendChild(node) { node.parentNode = this; this.children.push(node) }, removeChild(node) { this.children.splice(this.children.indexOf(node), 1); node.parentNode = null } } let scene const viewer = mountPipeEnhancement(host, plan, { rendererFactory: ({ canvas }) => ({ domElement: canvas, setPixelRatio() {}, setSize() {}, render(value) { scene = value; scene.updateMatrixWorld(true) }, dispose() {}, }) }) const root = scene.children.find((n) => n.name === `pipe-enhancement:${source.code}`) const meshes = [] root.traverse((node) => { if (node.isMesh) meshes.push(node) }) const bounds = new THREE.Box3().setFromObject(root) const ray = new THREE.Raycaster(new THREE.Vector3(bounds.min.x - 1, 0, 0), new THREE.Vector3(1, 0, 0)) let failures = 0 let passes = 0 function check(name, action) { try { action(); passes += 1; console.log(`PASS ${name}`) } catch (error) { failures += 1; console.error(`FAIL ${name}: ${error.message}`) } } check('轴向穿过中空管腔,不被端盖封死', () => { assert.equal(ray.intersectObject(root, true).length, 0) ray.set(new THREE.Vector3(bounds.getCenter(new THREE.Vector3()).x, 2, 0), new THREE.Vector3(0, -1, 0)) assert.ok(ray.intersectObject(root, true).length > 0, '管壁须有实体表面') }) check('端部有法兰与一圈螺栓,沿管轴附着', () => { const flanges = meshes.filter((m) => m.name === 'pipe-flange') const bolts = meshes.filter((m) => m.name === 'pipe-bolt') assert.equal(flanges.length, 2) assert.equal(bolts.length, 16) for (const flange of flanges) { const p = flange.getWorldPosition(new THREE.Vector3()) assert.ok(Math.abs(p.y) < 1e-7 && Math.abs(p.z) < 1e-7) } }) check('有分节接缝,没有独立定位环', () => { assert.ok(meshes.some((m) => m.name === 'pipe-seam')) assert.ok(!meshes.some((m) => m.geometry.type === 'TorusGeometry'), '旧中心定位环必须移除') }) check('管体保持供水行业色,不改源路径与示意说明', () => { const body = meshes.find((m) => m.name === 'pipe-wall') assert.ok(body) assert.equal(body.material.color.getHexString(), '168bff') assert.equal(JSON.stringify(source), original) assert.match(plan.disclaimer, /不代表工程竣工模型/) }) function trackResources(model) { const resources = new Map() model.traverse((node) => { for (const resource of [node.geometry, ...[].concat(node.material || [])].filter(Boolean)) { if (resources.has(resource)) continue resources.set(resource, 0) resource.addEventListener('dispose', () => resources.set(resource, resources.get(resource) + 1)) } }) return () => { assert.ok(resources.size > 0) for (const count of resources.values()) assert.ok(count > 0, '每个几何与材质都须释放') } } const assertInitialReleased = trackResources(root) viewer.sync({ ...plan, object: { ...plan.object, code: 'replacement' } }) check('换选移除旧模型并释放全部几何与材质', () => { assert.equal(root.parent, null) assertInitialReleased() }) check('短管段保持真实长径比例,不为可见性拉长或加粗', () => { const tiny = { ...plan, object: { ...plan.object, path: [[110, 28], [110.00001, 28]], position: [110, 28] } } viewer.sync(tiny) const next = scene.children.find((n) => n.name === `pipe-enhancement:${source.code}`) const box = new THREE.Box3().setFromObject(next) const size = box.getSize(new THREE.Vector3()) assert.ok(size.x > 0.98 && size.x < 1.02) assert.ok(size.y > 0.4 && size.y < 0.6) assert.ok(box.getCenter(new THREE.Vector3()).length() < 1e-6) }) check('纵向管段法兰随路径定向,轴向管腔不封死', () => { viewer.sync({ ...plan, object: { ...plan.object, path: [[110, 28], [110, 28.001]] } }) const next = scene.children.find((n) => n.name === `pipe-enhancement:${source.code}`) const box = new THREE.Box3().setFromObject(next) ray.set(new THREE.Vector3(0, 0, box.min.z - 1), new THREE.Vector3(0, 0, 1)) assert.equal(ray.intersectObject(next, true).length, 0) }) check('折线共享拐角不堆叠法兰,仅真实两端有法兰', () => { viewer.sync({ ...plan, object: { ...plan.object, path: [[110, 28], [110.001, 28], [110.001, 28.001]] } }) const next = scene.children.find((n) => n.name === `pipe-enhancement:${source.code}`) const flanges = [] next.traverse((n) => { if (n.name === 'pipe-flange') flanges.push(n) }) assert.equal(flanges.length, 2, '折点不能额外生成一对法兰') }) check('关闭释放当前模型全部几何材质、移除画布并可重复销毁', () => { const currentRoot = scene.children.find((n) => n.name === `pipe-enhancement:${source.code}`) const assertCurrentReleased = trackResources(currentRoot) viewer.destroy() assertCurrentReleased() viewer.destroy() assert.equal(host.children.length, 0) assert.equal(viewer.getState().status, 'destroyed') }) console.log(`T50 pipe detail: ${passes} passed, ${failures} failed`) if (failures) process.exitCode = 1