import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import vm from 'node:vm' import { parse as parseJS } from '@babel/parser' import { parse, compileScript } from '@vue/compiler-sfc' import * as Vue from 'vue' import * as Cesium from 'cesium' import { createPinia, setActivePinia } from 'pinia' import { attachPointPick } from '../../../src/modules/cockpit/services/pointPick.js' import { resolveOverlapPick } from '../../../src/modules/cockpit/domain/overlapDevices.js' import { PIPE_ENHANCEMENT_KIND, planPipeEnhancement } from '../../../src/modules/cockpit/domain/pipeEnhancement.js' import { usePipeEnhancementStore } from '../../../src/modules/cockpit/stores/pipeEnhancementStore.js' import { useEnhancementDevicesStore } from '../../../src/modules/cockpit/stores/enhancementDevicesStore.js' const src = (name) => readFileSync(new URL(`../../../src/modules/cockpit/${name}`, import.meta.url), 'utf8') const service = src('services/mapService.js') const shell = parse(src('CockpitShell.vue')).descriptor function nodes(source) { return parseJS(source, { sourceType: 'module' }).program.body.map((n) => n.declaration || n) } function functions(source, names) { return nodes(source).filter((n) => n.type === 'FunctionDeclaration' && names.includes(n.id.name)) .map((n) => source.slice(n.start, n.end)).join('\n') } const engineCode = src('services/cesiumEngine.js') .replace(/^import\s+[^]*?from\s+['"][^'"\n]+['"][ \t]*$/gm, '') .replace(/^import\s+['"][^'"\n]+['"][ \t]*$/gm, '').replace(/^export /gm, '') const engineContext = vm.createContext({ ...Cesium, CesiumMath: Cesium.Math }) vm.runInContext(`${engineCode}\nglobalThis.engine = { pickDeviceMarkerAt, pickPointMarkerAt, pickPipeObjectAt };`, engineContext) const source = new Cesium.CustomDataSource('cockpit-pipelines') source.entities.add({ id: 'pipe:P-2', properties: { kind: 'pipe', code: 'P-2' } }) const batch = { kind: 'pipe-model-batch', instances: ['pipe:P-1', 'pipe:P-2'] } let hits = [{ id: batch, instanceId: 1 }] const devices = new Cesium.CustomDataSource('cockpit-associated-device-markers') const viewer = { dataSources: { getByName: (name) => name === source.name ? [source] : name === devices.name ? [devices] : [] }, canvas: { getBoundingClientRect: () => ({ left: 20, top: 30 }) }, scene: { pick: () => hits[0], drillPick: () => hits }, } setActivePinia(createPinia()) const store = usePipeEnhancementStore() const pipes = ['P-1', 'P-2'].map((code) => ({ code, positions: [[110.1, 28.4], [110.101, 28.401]] })) const plan = Vue.computed(() => planPipeEnhancement({ selected: store.selected, visiblePipes: pipes, radiusM: store.radiusM })) const selectContext = vm.createContext({ PIPE_ENHANCEMENT_KIND, pipeEnhancementStore: store }) vm.runInContext(`${functions(shell.scriptSetup.content, ['selectPipeEnhancement'])}\nglobalThis.select = selectPipeEnhancement`, selectContext) const context = vm.createContext({ instance: { engine: engineContext.engine, viewer }, resolveOverlapPick, pipelinePickHandlers: new Set(), pointPickHandlers: new Set(), overlapExpandedGroup: '', overlapPreviewGroup: '', lastPicked: null, collapseOverlap() {}, paintPicked() {}, expandOverlap() {}, }) vm.runInContext(`${functions(service, ['onPipelinePick', 'pickPipelineAt', 'pickDeviceOrPointAt'])}\nglobalThis.api = { onPipelinePick, pickPipelineAt, pickDeviceOrPointAt };`, context) // Execute the actual service registration, preserving its device/point-first branch. const registration = service.match(/record\.detachPickInput = attachPointPick\(record\.host,[\s\S]*?\n \}\)/)?.[0] assert.ok(registration, 'map must attach its pointer input') const host = new EventTarget() Object.assign(context, { record: { host }, attachPointPick }) vm.runInContext(registration, context) // Execute the actual MapCanvas forwarding expression, followed by the real shell action. const forwarding = src('components/MapCanvas.vue').match(/onPipelinePick\(\(hit\) => emit\('pipeline-pick', hit\)\)/)?.[0] assert.ok(forwarding) Object.assign(context, { onPipelinePick: context.api.onPipelinePick, emit(event, hit) { assert.equal(event, 'pipeline-pick') selectContext.select(hit) } }) vm.runInContext(forwarding, context) function click(x = 120, y = 230, upX = x) { for (const [type, clientX] of [['pointerdown', x], ['pointerup', upX]]) { const event = new Event(type) Object.assign(event, { button: 0, pointerId: 1, clientX, clientY: y }) host.dispatchEvent(event) } } // Real Vue render/watch/close path; WebGL is replaced only at its renderer boundary. const enhancement = parse(src('components/PipeEnhancement.vue')).descriptor let mounts = 0 let releases = 0 let devicePick const script = compileScript(enhancement, { id: 't23-click', inlineTemplate: true }).content const imports = { vue: Vue, './EquipmentDetailSections.vue': { default: { props: ['deviceCode', 'aggregate'], render() { return Vue.h('div', { 'data-device-detail': this.deviceCode }) } } }, '../services/threePipeEnhancement.js': { mountPipeEnhancement(_host, next, options) { devicePick = options.onDevicePick assert.equal(next.object.code, 'P-2') mounts += 1 return { destroy() { releases += 1 } } } } } let compiled = script for (const node of parseJS(script, { sourceType: 'module' }).program.body.filter((n) => n.type === 'ImportDeclaration').reverse()) { const declarations = node.specifiers.map((s) => `const ${s.local.name} = imports[${JSON.stringify(node.source.value)}][${JSON.stringify(s.type === 'ImportDefaultSpecifier' ? 'default' : s.imported.name)}];`).join('\n') compiled = compiled.slice(0, node.start) + declarations + compiled.slice(node.end) } const Component = new Function('imports', `${compiled.replace('export default', 'return')}`)(imports) const element = (tag) => ({ tag, children: [], props: {}, parent: null }) const renderer = Vue.createRenderer({ createElement: element, createText: (text) => ({ text }), createComment: (text) => ({ comment: text }), setText(n, text) { n.text = text }, setElementText(n, text) { n.text = text }, parentNode: (n) => n.parent, nextSibling: () => null, insert(n, parent, anchor) { if (n.parent) this.remove(n); n.parent = parent; const i = parent.children.indexOf(anchor); parent.children.splice(i < 0 ? parent.children.length : i, 0, n) }, remove(n) { if (n.parent) n.parent.children.splice(n.parent.children.indexOf(n), 1); n.parent = null }, patchProp(n, key, _old, next) { n.props[key] = next }, }) const root = element('root') const deviceStore = useEnhancementDevicesStore() const app = renderer.createApp({ setup: () => () => Vue.h(Component, { plan: plan.value, deviceState: deviceStore, onRadiusChange: (radius) => store.setRadius(radius), onClose: () => { store.clear(); deviceStore.clear() }, onDevicePick: (device) => deviceStore.open(device, { fetchDetail: async (code) => ({ state: 'success', data: { deviceCode: code, sections: {} } }) }), }) }) app.mount(root) function find(node, key) { if (Object.hasOwn(node.props || {}, key)) return node return (node.children || []).map((n) => find(n, key)).find(Boolean) } assert.equal(find(root, 'data-pipe-enhancement'), undefined) click() await Vue.nextTick(); await Vue.nextTick() assert.ok(store.selected, `点击未派发:device/point=${JSON.stringify(context.api.pickDeviceOrPointAt(120, 230))}, pipe=${JSON.stringify(engineContext.engine.pickPipeObjectAt(viewer, 120, 230))}, subscribers=${context.pipelinePickHandlers.size}`) assert.equal(store.selected.code, 'P-2') assert.equal(mounts, 1) assert.ok(find(root, 'data-pipe-enhancement'), 'pointer hit must render the enhancement dialog') devicePick({ code: 'D-real', pointCode: 'MP-real', name: '真实设备', placementText: '邻近设备 · 示意安装' }) await Vue.nextTick(); await Vue.nextTick() assert.equal(deviceStore.aggregate.data.deviceCode, 'D-real') assert.equal(find(root, 'data-device-detail').props['data-device-detail'], 'D-real') assert.equal(mounts, 1, '打开设备详情不能重建三维场景') assert.equal(store.selected.code, 'P-2') function findTag(node, tag) { return node.tag === tag ? node : (node.children || []).map((child) => findTag(child, tag)).find(Boolean) } findTag(root, 'select').props.onChange({ target: { value: '100' } }) await Vue.nextTick(); await Vue.nextTick() assert.equal(store.radiusM, 100) assert.equal(plan.value.radiusM, 100) assert.equal(mounts, 2, '范围切换后重绘邻域') find(root, 'data-pipe-enhancement-close').props.onClick() await Vue.nextTick() assert.equal(store.selected, null) assert.equal(releases, 2) assert.equal(find(root, 'data-pipe-enhancement'), undefined) click(120, 230, 150) assert.equal(store.selected, null, 'dragging the map must not select a pipe') console.log('PASS pointer → 设备/点位优先链 → GPU instanceId=1 → MapCanvas emit → Pinia → Vue 弹窗 → 关闭释放;拖拽不选中') app.unmount() context.record.detachPickInput()