| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104 |
- import assert from 'node:assert/strict'
- import * as THREE from 'three'
- import { planPipeEnhancement } from '../../../src/modules/cockpit/domain/pipeEnhancement.js'
- import { planEnhancementDevices } from '../../../src/modules/cockpit/domain/enhancementDevices.js'
- import { mountPipeEnhancement, deviceMarkerPlan } from '../../../src/modules/cockpit/services/threePipeEnhancement.js'
- const plan = planPipeEnhancement({ selected: { kind: 'pipe', code: 'P' }, visiblePipes: [
- { code: 'P', industry: 'WATER_SUPPLY', diameter: 400, positions: [[110, 28], [110.001, 28]] },
- ] })
- Object.assign(plan, planEnhancementDevices(plan, [{ code: 'D', name: '真实供水设备', industry: 'WATER_SUPPLY', pointCode: 'MP', longitude: 110.0005, latitude: 28, coordinate: { renderable: true } }]))
- const canvas = new EventTarget()
- canvas.getBoundingClientRect = () => ({ left: 0, top: 0, width: 900, height: 500 })
- let scene, camera, picked
- let resolvedDevice = null
- let textureUrl = ''
- const host = { clientWidth: 900, clientHeight: 500, appendChild(n) { n.parentNode = this }, removeChild(n) { n.parentNode = null } }
- const viewer = mountPipeEnhancement(host, plan, {
- onDevicePick: (d) => { picked = d },
- rendererFactory: () => ({ domElement: canvas, setSize() {}, dispose() {}, render(s, c) { scene = s; camera = c } }),
- // 离线 seam:图标必须来自主图同款合成产物(`renderMarker`),这里注入一个可观测替代,
- // 并注入同步假贴图加载器(Node 无 DOM Image,真 TextureLoader 不可用)。
- iconResolver: async (device) => {
- resolvedDevice = device
- return { url: `mock-marker://${device.code}`, width: 26.7, height: 33.5 }
- },
- loadTexture: (url, onLoad) => {
- textureUrl = url
- const texture = new THREE.Texture()
- texture.image = { data: new Uint8Array([255, 255, 255, 255]), width: 1, height: 1 }
- texture.userData = { seeded: true }
- onLoad(texture)
- },
- })
- await new Promise((resolve) => setImmediate(resolve))
- scene.updateMatrixWorld(true)
- camera.updateMatrixWorld()
- const device = scene.getObjectByName('device:D')
- assert.ok(device)
- const icon = device.getObjectByName('device-icon')
- assert.ok(icon?.isSprite, '每个可显示设备必须有朝向镜头的点击图标')
- assert.equal(icon.material.depthTest, false, '设备图标必须在放大管体前可见')
- assert.equal(resolvedDevice?.code, 'D', '图标必须由主图同款合成链路按设备身份生成')
- assert.equal(textureUrl, 'mock-marker://D', 'Sprite 贴图必须采用合成产物 URL,而不是自绘图元')
- assert.ok(icon.material.map?.userData?.seeded, '贴图必须挂到 Sprite 材质上')
- assert.equal(icon.visible, true)
- assert.equal(icon.center.y, 0, '水滴尖端锚定:Sprite 底部中心为原点')
- const initialWorldSize = icon.getWorldScale(new THREE.Vector3()).y
- const initialDistance = camera.position.distanceTo(icon.getWorldPosition(new THREE.Vector3()))
- const screenSize = initialWorldSize / initialDistance
- assert.ok(screenSize > 0)
- assert.equal(viewer.focusDevice('D'), true)
- // focusDevice 改了相机姿态;假渲染器不会替我们刷新矩阵,投影前必须显式更新。
- scene.updateMatrixWorld(true)
- camera.updateMatrixWorld()
- const focusedWorldSize = icon.getWorldScale(new THREE.Vector3()).y
- const focusedDistance = camera.position.distanceTo(icon.getWorldPosition(new THREE.Vector3()))
- assert.ok(Math.abs(focusedWorldSize / focusedDistance - screenSize) / screenSize < 0.03, '图标保持固定屏幕尺寸')
- // 点击点取「图标可见区域中心」——锚点是水滴尖端(Sprite 底部),直接点锚点会落在图形之外。
- const clickPoint = new THREE.Box3().setFromObject(icon).getCenter(new THREE.Vector3())
- const screen = clickPoint.project(camera)
- const clientX = (screen.x + 1) * 450
- const clientY = (1 - screen.y) * 250
- function pointer(type, x = clientX) {
- const event = new Event(type, { cancelable: true })
- Object.assign(event, { button: 0, pointerId: 1, clientX: x, clientY })
- canvas.dispatchEvent(event)
- }
- pointer('pointerdown'); pointer('pointerup')
- assert.equal(picked.code, 'D')
- assert.equal(picked.pointCode, 'MP')
- assert.equal(picked.placementText, '邻近设备 · 示意安装')
- picked = null
- const root = scene.getObjectByName('pipe-enhancement:P')
- const blocker = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 0.5), new THREE.MeshBasicMaterial())
- blocker.position.copy(root.worldToLocal(camera.position.clone().lerp(device.getWorldPosition(new THREE.Vector3()), 0.5)))
- root.add(blocker)
- root.updateMatrixWorld(true)
- pointer('pointerdown'); pointer('pointerup')
- assert.equal(picked.code, 'D', '可见的浮动设备图标即使覆盖管壁也可直接点开详情')
- root.remove(blocker); blocker.geometry.dispose(); blocker.material.dispose()
- picked = null
- pointer('pointerdown'); pointer('pointermove', clientX + 30); pointer('pointerup', clientX + 30)
- assert.equal(picked, null)
- viewer.destroy()
- pointer('pointerdown'); pointer('pointerup')
- assert.equal(picked, null)
- console.log('PASS 真实Three设备模型射线点选派发业务身份;拖拽和销毁后不误派发')
- // ---- 语义同步:颜色状态与中间类型图标必须与主图同表(维护者 2026-10-22 第二次反馈)----
- assert.equal(deviceMarkerPlan({ status: 'ALARM', industry: 'GAS', pointCode: 'MP' }).tint, 'lv1', '报警→lv1')
- assert.equal(deviceMarkerPlan({ status: 'WARNING', industry: 'GAS', pointCode: 'MP' }).tint, 'lv2', '预警→lv2')
- assert.equal(deviceMarkerPlan({ status: 'WORK_ORDER', industry: 'GAS', pointCode: 'MP' }).tint, 'lv3', '工单处置→lv3')
- assert.equal(deviceMarkerPlan({ status: 'NORMAL', industry: 'GAS', pointCode: 'MP' }).tint, 'lv4', '正常→lv4')
- assert.equal(deviceMarkerPlan({ status: 'OFFLINE', industry: 'GAS', pointCode: 'MP' }).tint, 'point-offline', '离线必须用设备表的 point-offline,不能落点位表的 neutral')
- assert.equal(deviceMarkerPlan({ status: '未知态', industry: 'GAS', pointCode: 'MP' }).tint, 'point-unknown', '认不出必须 point-unknown,不能猜正常')
- const gasPlan = deviceMarkerPlan({ status: 'ALARM', industry: 'GAS', pointCode: 'MP' })
- assert.equal(gasPlan.inner.enabled, true, '有行业必须有内层类型图标')
- assert.equal(gasPlan.inner.icon, 'gas')
- assert.equal(deviceMarkerPlan({ status: 'ALARM', industry: 'WATER_SUPPLY', pointCode: 'MP' }).inner.icon, 'water')
- assert.equal(deviceMarkerPlan({ status: 'ALARM', industry: 'DRAINAGE', pointCode: 'MP' }).inner.icon, 'drainage')
- assert.equal(deviceMarkerPlan({ status: 'ALARM', industry: 'MANHOLE', pointCode: 'MP' }).inner.icon, 'manhole')
- assert.equal(deviceMarkerPlan({ status: 'ALARM', industry: '', pointCode: 'MP' }).inner.enabled, false, '无行业不造类型图标')
- assert.equal(gasPlan.master, 'solid', '有点位归属=实心母版')
- assert.equal(deviceMarkerPlan({ status: 'NORMAL', industry: 'GAS' }).master, 'hollow', '未关联设备=镂空母版')
- console.log('PASS 场景图标颜色状态/内层类型图标/母版形状与主图同表(设备五态 + 未知态)')
|