|
|
@@ -0,0 +1,138 @@
|
|
|
+import assert from 'node:assert/strict'
|
|
|
+import { readFileSync } from 'node:fs'
|
|
|
+import vm from 'node:vm'
|
|
|
+import { resolveDetailAnchor, detailAnchorLocation } from '../../../src/modules/cockpit/domain/detailAnchor.js'
|
|
|
+import { solvePointAnchor } from '../../../src/modules/cockpit/domain/pointAnchor.js'
|
|
|
+
|
|
|
+const root = new URL('../../../src/modules/cockpit/', import.meta.url)
|
|
|
+const read = (path) => readFileSync(new URL(path, root), 'utf8')
|
|
|
+const engine = read('services/cesiumEngine.js')
|
|
|
+const fn = (name) => {
|
|
|
+ const match = engine.match(new RegExp(`(?:export )?function ${name}\\([^]*?\\n\\}`))
|
|
|
+ assert.ok(match, `${name} exists`)
|
|
|
+ return match[0].replace(/^export /, '')
|
|
|
+}
|
|
|
+const property = (value) => ({ getValue: () => value })
|
|
|
+const core = {
|
|
|
+ id: 'device|MP-001|EQ-A', show: true,
|
|
|
+ properties: { deviceCode: property('EQ-A') },
|
|
|
+ billboard: { scale: 1, color: { alpha: 1 }, rotation: 0 },
|
|
|
+}
|
|
|
+core.billboard.pixelOffset = property({ x: 42, y: -26 })
|
|
|
+const halo = {
|
|
|
+ id: 'fx|MP-001|EQ-A|ring0', show: true,
|
|
|
+ properties: { effectFx: property(true), effectFxFor: property('EQ-A'), effectFxStatus: property('WARNING'), effectFxKind: property('ring'), effectFxLayer: property(0) },
|
|
|
+ billboard: { scale: 1, color: { alpha: 1 }, rotation: 0 },
|
|
|
+}
|
|
|
+const source = { entities: { values: [core, halo] } }
|
|
|
+let renderRequests = 0
|
|
|
+const viewer = { scene: { requestRender() { renderRequests += 1 } } }
|
|
|
+const context = vm.createContext({
|
|
|
+ deviceEffectState: { viewer, plan: { animated: [{ code: 'EQ-A', effect: 'blink' }] }, selectedCode: '', tick: 0 },
|
|
|
+ deviceSources: new Map([[viewer, source]]),
|
|
|
+ baseStyleOf: () => ({ scale: 0.75, alpha: 0.45 }),
|
|
|
+ DEVICE_EFFECT: { SPIN: 'spin', BLINK: 'blink' },
|
|
|
+ Color: { WHITE: { withAlpha: (alpha) => ({ alpha }) } },
|
|
|
+ blinkStrengthAt: () => 0.25, pulseStrengthAt: () => 0,
|
|
|
+ spinAngleAt: () => 1, glowStrengthAt: () => 0, glowScaleAt: () => 1,
|
|
|
+ ringFrameAt: () => ({ rings: [{ scale: 2, alpha: 0.8 }] }), Date,
|
|
|
+ Cartesian2: class { constructor(x, y) { this.x = x; this.y = y } },
|
|
|
+})
|
|
|
+vm.runInContext(`${fn('animatedByCode')}\n${fn('writeEffectFrame')}\nwriteEffectFrame()`, context)
|
|
|
+let failures = 0
|
|
|
+const check = (name, action) => {
|
|
|
+ try { action(); console.log(`PASS ${name}`) }
|
|
|
+ catch (error) { failures += 1; console.error(`FAIL ${name}: ${String(error.message).split('\n')[0]}`) }
|
|
|
+}
|
|
|
+check('center icon remains opaque at the dimmest blink phase', () => assert.equal(core.billboard.color.alpha, 1))
|
|
|
+check('center icon never shrinks below its normal size', () => assert.ok(core.billboard.scale >= 1))
|
|
|
+check('blink applies to the outer halo while center stays opaque', () => assert.equal(halo.billboard.color.alpha, 0.2))
|
|
|
+check('halo follows the expanded device pixelOffset', () => assert.equal(halo.billboard.pixelOffset, core.billboard.pixelOffset))
|
|
|
+check('active effects request a real render', () => assert.ok(renderRequests > 0))
|
|
|
+check('halo disappears with its device', () => {
|
|
|
+ core.show = false
|
|
|
+ vm.runInContext('writeEffectFrame()', context)
|
|
|
+ assert.equal(halo.show, false)
|
|
|
+ core.show = true
|
|
|
+})
|
|
|
+const objects = [{ kind: 'monitoring-point', code: 'MP-001', coordinate: { renderable: true } }]
|
|
|
+const devices = [
|
|
|
+ { code: 'EQ-A', pointCode: 'MP-001', coordinate: { renderable: true } },
|
|
|
+ { code: 'EQ-B', pointCode: 'MP-001', coordinate: { renderable: true } },
|
|
|
+ { code: 'EQ-X', pointCode: 'MP-001', coordinate: { renderable: false, validated: true, reason: 'UNVERIFIED_COORDINATE' } },
|
|
|
+]
|
|
|
+const target = (level, deviceCode = '') => resolveDetailAnchor({ selection: { level, pointCode: 'MP-001', deviceCode }, objects, devices })
|
|
|
+check('equipment A anchors to A while keeping point ownership', () => {
|
|
|
+ assert.equal(target('equipment', 'EQ-A').code, 'EQ-A')
|
|
|
+ assert.equal(target('equipment', 'EQ-A').pointCode, 'MP-001')
|
|
|
+ assert.equal(target('equipment', 'EQ-A').kind, 'monitoring-device')
|
|
|
+})
|
|
|
+check('equipment A to B changes anchor identity', () => assert.equal(target('equipment', 'EQ-B').code, 'EQ-B'))
|
|
|
+check('back to point summary restores point anchor', () => assert.equal(target('point', 'EQ-B').code, 'MP-001'))
|
|
|
+check('no-location equipment uses its own verdict', () => assert.equal(detailAnchorLocation(target('equipment', 'EQ-X')).reasonText, '坐标未经校验'))
|
|
|
+check('missing equipment does not borrow its parent location', () => assert.equal(detailAnchorLocation(target('equipment', 'EQ-missing')).present, false))
|
|
|
+check('unassociated equipment anchors to itself', () => {
|
|
|
+ const standalone = { kind: 'unassociated-equipment', code: 'EQ-U' }
|
|
|
+ assert.equal(resolveDetailAnchor({ selection: { level: 'equipment', deviceCode: 'EQ-U' }, objects: [standalone] }).object, standalone)
|
|
|
+})
|
|
|
+check('closed detail has no anchor', () => assert.equal(target('none', 'EQ-A'), null))
|
|
|
+check('shell consumes anchor policy and includes line movement in its update key', () => {
|
|
|
+ assert.ok(/resolveDetailAnchor\(/.test(read('CockpitShell.vue')))
|
|
|
+ assert.ok(/JSON.stringify\(layout.line\)/.test(read('CockpitShell.vue')))
|
|
|
+})
|
|
|
+
|
|
|
+// Execute the production projection with a small Cesium public-API double.
|
|
|
+const entity = {
|
|
|
+ isShowing: true, show: true, position: property({ longitude: 1, latitude: 2 }),
|
|
|
+ properties: { deviceCode: property('EQ-A'), monitoringPointCode: property('MP-001') },
|
|
|
+ billboard: { pixelOffset: property({ x: 42, y: -26 }), scale: property(1.6), width: property(14), height: property(14) },
|
|
|
+}
|
|
|
+const projectionViewer = {
|
|
|
+ clock: { currentTime: 0 }, canvas: { getBoundingClientRect: () => ({ left: 12, top: 102 }) },
|
|
|
+ scene: { globe: { ellipsoid: { cartesianToCartographic: (p) => p }, getHeight: () => 123 } },
|
|
|
+ camera: { positionWC: {}, directionWC: {} },
|
|
|
+}
|
|
|
+function Cartesian3() {}
|
|
|
+Cartesian3.fromRadians = (longitude, latitude, height) => ({ longitude, latitude, height })
|
|
|
+Cartesian3.subtract = () => ({})
|
|
|
+Cartesian3.dot = () => 1
|
|
|
+const projectionContext = vm.createContext({
|
|
|
+ Cartesian3,
|
|
|
+ EllipsoidalOccluder: class { isPointVisible() { return true } },
|
|
|
+ SceneTransforms: { worldToWindowCoordinates: (_scene, position) => { assert.equal(position.height, 123); return { x: 500, y: 320 } } },
|
|
|
+ deviceSources: new Map([[projectionViewer, { show: true, entities: { values: [entity] } }]]),
|
|
|
+ viewer: projectionViewer,
|
|
|
+})
|
|
|
+vm.runInContext(`${fn('findDeviceMarker')}\n${fn('projectMarkerEntity')}\n${fn('projectDeviceMarker')}`, projectionContext)
|
|
|
+check('device anchor includes terrain, fan pixelOffset and selected icon size', () => {
|
|
|
+ const at = vm.runInContext("projectDeviceMarker(viewer, 'EQ-A', 'MP-001')", projectionContext)
|
|
|
+ assert.equal(at.x, 554)
|
|
|
+ assert.equal(at.y, 396)
|
|
|
+ assert.equal(at.iconSize, 22.400000000000002)
|
|
|
+ const layout = solvePointAnchor({ anchor: at, canvas: { left: 12, top: 102, right: 1650, bottom: 842 }, iconSize: at.iconSize })
|
|
|
+ assert.ok(layout.rect.left > at.x + at.iconSize / 2)
|
|
|
+ assert.equal(layout.line.from.x, at.x + at.iconSize / 2)
|
|
|
+})
|
|
|
+check('hidden fan device has no fake parent anchor', () => {
|
|
|
+ entity.show = false
|
|
|
+ assert.equal(vm.runInContext("projectDeviceMarker(viewer, 'EQ-A', 'MP-001')", projectionContext), null)
|
|
|
+})
|
|
|
+check('effects off cancels the loop without hiding the center icon', () => {
|
|
|
+ const cancelled = []
|
|
|
+ context.window = { cancelAnimationFrame: (id) => cancelled.push(id) }
|
|
|
+ context.clearRingEntities = () => {}
|
|
|
+ context.deviceEffectState.raf = 17
|
|
|
+ vm.runInContext(`${fn('applyDeviceEffects')}\napplyDeviceEffects(deviceEffectState.viewer, { enabled: false, animated: [] })`, context)
|
|
|
+ assert.deepEqual(cancelled, [17])
|
|
|
+ assert.equal(context.deviceEffectState.raf, 0)
|
|
|
+ assert.equal(core.billboard.color.alpha, 1)
|
|
|
+})
|
|
|
+check('wrong parent identity never resolves another device marker', () => {
|
|
|
+ entity.show = true
|
|
|
+ assert.equal(vm.runInContext("projectDeviceMarker(viewer, 'EQ-A', 'MP-other')", projectionContext), null)
|
|
|
+})
|
|
|
+check('pending device does not pretend to have a known location failure', () => {
|
|
|
+ assert.equal(detailAnchorLocation({ code: 'EQ-pending', object: { placement: 'pending' } }).present, false)
|
|
|
+})
|
|
|
+console.log(`T47 readability/anchor: ${failures ? 'FAILED' : 'passed'}`)
|
|
|
+process.exitCode = failures ? 1 : 0
|