| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228 |
- import assert from 'node:assert/strict'
- import { planPipeEnhancement } from '../../../src/modules/cockpit/domain/pipeEnhancement.js'
- import { mountPipeEnhancement } from '../../../src/modules/cockpit/services/threePipeEnhancement.js'
- import { mountMap, unmountMap } from '../../../src/modules/cockpit/services/mapService.js'
- import { attachSceneFocus } from '../../../src/modules/cockpit/services/sceneInputFocus.js'
- let failures = 0
- let passed = 0
- function check(name, fn) {
- try { fn(); passed += 1; console.log(`PASS ${name}`) }
- catch (error) { failures += 1; console.error(`FAIL ${name}: ${error.message}`) }
- }
- class Element extends EventTarget {
- constructor(doc) {
- super(); this.ownerDocument = doc; this.attributes = new Map(); this.dataset = {}
- this.clientWidth = 900; this.clientHeight = 500; this.children = []; this.parentNode = null
- }
- getAttribute(name) { return this.attributes.get(name) ?? null }
- setAttribute(name, value) { this.attributes.set(name, String(value)) }
- removeAttribute(name) { this.attributes.delete(name) }
- focus() { this.ownerDocument.activeElement = this; this.dispatchEvent(new Event('focusin')) }
- contains(node) { return node === this || this.children.some((child) => child.contains(node)) }
- appendChild(node) { node.parentNode = this; this.children.push(node) }
- removeChild(node) { this.children.splice(this.children.indexOf(node), 1); node.parentNode = null }
- }
- function fixture() {
- const doc = { activeElement: null, createElement() { return new Element(doc) } }
- const host = new Element(doc)
- let scene
- let camera
- const rendererFactory = ({ canvas }) => ({ domElement: canvas, setPixelRatio() {}, setSize() {},
- render(value, view) { scene = value; camera = view; camera.updateMatrixWorld() }, dispose() {} })
- return { doc, host, rendererFactory, scene: () => scene, camera: () => camera }
- }
- function selected(row, component = false) {
- const item = { code: 'P', diameter: 400, positions: [[110.1, 28.4], [110.101, 28.401]], position: [110.1, 28.4], kind: 'valve', ...row }
- return planPipeEnhancement({ selected: { kind: component ? 'component' : 'pipe', code: 'P' }, visiblePipes: [item], visibleComponents: [item] })
- }
- function event(target, type, extras = {}) {
- const value = new Event(type, { cancelable: true, bubbles: true })
- Object.assign(value, extras)
- target.dispatchEvent(value)
- return value
- }
- for (const [industry, networkLevel, expected] of [
- ['GAS', undefined, 'ff3b52'], ['WATER_SUPPLY', undefined, '168bff'],
- ['DRAINAGE', 0, '29e36c'], ['DRAINAGE', 1, '00e5ff'],
- ['DRAINAGE', ' 1 ', '00e5ff'], ['DRAINAGE', null, '29e36c'],
- ['DRAINAGE', 7, 'aab7c4'], ['MANHOLE', undefined, 'b36bff'], ['unknown', undefined, 'aab7c4'],
- ]) {
- check(`${industry}/${networkLevel}: actual Three mesh uses #${expected}`, () => {
- const f = fixture()
- const viewer = mountPipeEnhancement(f.host, selected({ industry, networkLevel }), f)
- try {
- const root = f.scene().children.find((node) => node.name === 'pipe-enhancement:P')
- let mesh
- root.traverse((node) => { if (node.name === 'pipe-wall') mesh ||= node })
- assert.ok(mesh, '管壁材质必须存在')
- assert.equal(mesh.material.color.getHexString(), expected)
- } finally { viewer.destroy() }
- })
- }
- check('雨水设施构件同样使用青色', () => {
- const f = fixture()
- const viewer = mountPipeEnhancement(f.host, selected({ industry: 'DRAINAGE', networkLevel: 1 }, true), f)
- try {
- const root = f.scene().children.find((node) => node.name === 'pipe-enhancement:P')
- let mesh
- root.traverse((node) => { if (node.isMesh && node.parent.userData?.sourceKind === 'component') mesh ||= node })
- assert.ok(mesh)
- assert.equal(mesh.material.color.getHexString(), '00e5ff')
- } finally { viewer.destroy() }
- })
- check('局部视口右键取消浏览器菜单并获得焦点,销毁后移除监听', () => {
- const f = fixture()
- const viewer = mountPipeEnhancement(f.host, selected({ industry: 'WATER_SUPPLY' }), f)
- try {
- event(f.host, 'pointerdown', { button: 2 })
- assert.equal(f.doc.activeElement, f.host)
- assert.equal(event(f.host, 'contextmenu').defaultPrevented, true)
- } finally { viewer.destroy() }
- assert.equal(event(f.host, 'contextmenu').defaultPrevented, false)
- assert.equal(f.host.getAttribute('tabindex'), null)
- })
- check('局部缩放取消页面滚动,销毁后恢复默认事件', () => {
- const f = fixture()
- const viewer = mountPipeEnhancement(f.host, selected({ industry: 'GAS' }), f)
- const canvas = f.host.children[0]
- assert.equal(event(canvas, 'wheel', { deltaY: 100 }).defaultPrevented, true)
- viewer.destroy()
- assert.equal(event(canvas, 'wheel', { deltaY: 100 }).defaultPrevented, false)
- })
- check('主图宿主统一抑制右键菜单并聚焦,卸载后恢复', () => {
- const f = fixture()
- mountMap(f.host)
- try {
- event(f.host, 'pointerdown', { button: 2 })
- assert.equal(f.doc.activeElement, f.host)
- assert.equal(event(f.host, 'contextmenu').defaultPrevented, true)
- } finally { unmountMap() }
- assert.equal(event(f.host, 'contextmenu').defaultPrevented, false)
- assert.equal(f.host.getAttribute('tabindex'), null)
- })
- check('左键拖动平移,朝向不变,场景随鼠标移动', () => {
- const f = fixture()
- const viewer = mountPipeEnhancement(f.host, selected({ industry: 'GAS' }), f)
- try {
- const canvas = f.host.children[0]
- const camera = f.camera()
- const beforeRotation = camera.quaternion.clone()
- const beforePosition = camera.position.clone()
- const point = camera.position.clone().set(0, 0, 0)
- const beforeScreen = point.clone().project(camera)
- event(canvas, 'pointerdown', { button: 0, pointerId: 1, clientX: 100, clientY: 100 })
- event(canvas, 'pointermove', { pointerId: 1, clientX: 160, clientY: 130 })
- const afterScreen = point.clone().project(camera)
- assert.ok(camera.quaternion.angleTo(beforeRotation) < 1e-7, '左键不能改变相机朝向')
- assert.ok(camera.position.distanceTo(beforePosition) > 0, '左键必须平移')
- assert.ok(Math.abs((afterScreen.x - beforeScreen.x) * f.host.clientWidth / 2 - 60) < 0.01)
- assert.ok(Math.abs(-(afterScreen.y - beforeScreen.y) * f.host.clientHeight / 2 - 30) < 0.01)
- } finally { viewer.destroy() }
- })
- check('右键绕中心旋转而不缩放,松手后停止', () => {
- const f = fixture()
- const viewer = mountPipeEnhancement(f.host, selected({ industry: 'GAS' }), f)
- try {
- const canvas = f.host.children[0]
- const camera = f.camera()
- const distance = camera.position.length()
- const rotation = camera.quaternion.clone()
- event(canvas, 'pointerdown', { button: 2, pointerId: 2, clientX: 100, clientY: 100 })
- event(canvas, 'pointermove', { pointerId: 2, clientX: 160, clientY: 140 })
- assert.ok(camera.quaternion.angleTo(rotation) > 0.01)
- assert.ok(Math.abs(camera.position.length() - distance) < 1e-7, '旋转不能改变半径')
- event(canvas, 'pointerup', { pointerId: 2 })
- const stopped = camera.position.clone()
- event(canvas, 'pointermove', { pointerId: 2, clientX: 200, clientY: 200 })
- assert.ok(camera.position.equals(stopped))
- } finally { viewer.destroy() }
- })
- check('滚轮向上拉近、向下拉远,缩放不改变朝向', () => {
- const f = fixture()
- const viewer = mountPipeEnhancement(f.host, selected({ industry: 'GAS' }), f)
- try {
- const canvas = f.host.children[0]
- const camera = f.camera()
- const distance = camera.position.length()
- const rotation = camera.quaternion.clone()
- event(canvas, 'wheel', { deltaY: -100 })
- assert.ok(camera.position.length() < distance)
- assert.ok(camera.quaternion.angleTo(rotation) < 1e-7)
- const near = camera.position.length()
- event(canvas, 'wheel', { deltaY: 100 })
- assert.ok(camera.position.length() > near)
- } finally { viewer.destroy() }
- })
- check('非当前指针和取消后的移动不能继续拖拽', () => {
- const f = fixture()
- const viewer = mountPipeEnhancement(f.host, selected({ industry: 'GAS' }), f)
- try {
- const canvas = f.host.children[0]
- event(canvas, 'pointerdown', { button: 2, pointerId: 4, clientX: 100, clientY: 100 })
- const initial = f.camera().position.clone()
- event(canvas, 'pointermove', { pointerId: 5, clientX: 150, clientY: 150 })
- assert.ok(f.camera().position.equals(initial))
- event(canvas, 'pointercancel', { pointerId: 4 })
- event(canvas, 'pointermove', { pointerId: 4, clientX: 180, clientY: 180 })
- assert.ok(f.camera().position.equals(initial))
- } finally { viewer.destroy() }
- })
- check('聚焦仅影响场景,输入框和场景外保留默认菜单,焦点离开可退出', () => {
- const f = fixture()
- f.host.setAttribute('tabindex', '3')
- const detach = attachSceneFocus(f.host)
- try {
- event(f.host, 'pointerdown', { button: 0 })
- assert.equal(f.host.getAttribute('data-scene-focused'), 'true')
- const input = new Element(f.doc)
- input.closest = () => input
- const menu = new Event('contextmenu', { cancelable: true })
- Object.defineProperty(menu, 'target', { value: input })
- f.host.dispatchEvent(menu)
- assert.equal(menu.defaultPrevented, false)
- const outside = new Element(f.doc)
- assert.equal(event(outside, 'contextmenu').defaultPrevented, false)
- event(f.host, 'focusout', { relatedTarget: outside })
- assert.equal(f.host.getAttribute('data-scene-focused'), null)
- } finally { detach() }
- assert.equal(f.host.getAttribute('tabindex'), '3')
- })
- check('面板尺寸变化重设画布,相机比例正确,销毁后停止观察和渲染', () => {
- const original = globalThis.ResizeObserver
- let callback
- let disconnected = false
- globalThis.ResizeObserver = class {
- constructor(fn) { callback = fn }
- observe() {}
- disconnect() { disconnected = true }
- }
- const f = fixture()
- let renders = 0
- const factory = f.rendererFactory
- f.rendererFactory = (options) => {
- const renderer = factory(options)
- const render = renderer.render
- renderer.render = (...args) => { renders += 1; render(...args) }
- return renderer
- }
- let viewer
- try {
- viewer = mountPipeEnhancement(f.host, selected({ industry: 'GAS' }), f)
- f.host.clientWidth = 1000
- f.host.clientHeight = 400
- callback()
- assert.equal(f.camera().aspect, 2.5)
- viewer.destroy()
- assert.equal(disconnected, true)
- const previous = renders
- callback()
- assert.equal(renders, previous)
- } finally {
- viewer?.destroy()
- if (original === undefined) delete globalThis.ResizeObserver
- else globalThis.ResizeObserver = original
- }
- })
- console.log(`T23 visual/input: ${passed} passed, ${failures} failed`)
- if (failures) process.exitCode = 1
|