| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214 |
- /**
- * T47 设备状态视觉特效与门控 纯逻辑检查(票 47;决策票 07 的 `## Answer`)。
- *
- * 零依赖 node 断言脚本(约定见 t01-panel-catalog.mjs)。
- * 运行:node .scratch/cockpit-shell-panel-layout/verify/t47-device-effects.mjs
- *
- * **这是三层证据里的第 1 层**(领域纯逻辑):映射唯一、未知不猜、门控与 30 台降级可断言。
- * 它**不能单独作为验收** —— 映射表对不等于画面在动;"真在跑"的证据在
- * `t47-device-effects-walkthrough.mjs` 的**跨帧快照**与真实帧差里(决策票 07 问题 7)。
- */
- import {
- DEVICE_EFFECT,
- DEFAULT_ABNORMAL_LIMIT,
- RING_LAYERS,
- deviceEffectOf,
- hasDeviceEffect,
- effectPhaseAt,
- pulseStrengthAt,
- blinkStrengthAt,
- spinAngleAt,
- ringScaleAt,
- ringAlphaAt,
- ringFrameAt,
- glowStrengthAt,
- glowScaleAt,
- planDeviceEffects,
- degradedAbnormalNote,
- } from '../../../src/modules/cockpit/domain/deviceEffects.js'
- import { DEVICE_STATUS_VISUALS, DEVICE_UNKNOWN_VISUAL } from '../../../src/modules/cockpit/domain/deviceStatus.js'
- import { builtinConfig, normalizeConfig } from '../../../src/modules/cockpit/services/cockpitConfig.js'
- const checks = []
- function check(name, pass, detail = '') {
- checks.push({ name, pass })
- const rest = detail === '' ? '' : ` — ${detail}`
- console.log(`${pass ? 'PASS' : 'FAIL'} ${name}${rest}`)
- }
- const device = (code, status) => ({ code, status, pointCode: 'MP-1' })
- // ================================================================ A 状态 → 动效映射
- {
- check('A1-three-attention-states-animate',
- deviceEffectOf('ALARM') === DEVICE_EFFECT.PULSE &&
- deviceEffectOf('WARNING') === DEVICE_EFFECT.BLINK &&
- deviceEffectOf('WORK_ORDER') === DEVICE_EFFECT.SPIN,
- `ALARM=${deviceEffectOf('ALARM')} WARNING=${deviceEffectOf('WARNING')} WORK_ORDER=${deviceEffectOf('WORK_ORDER')}`)
- check('A2-normal-offline-have-no-effect',
- deviceEffectOf('NORMAL') === '' && deviceEffectOf('OFFLINE') === '',
- `NORMAL='${deviceEffectOf('NORMAL')}' OFFLINE='${deviceEffectOf('OFFLINE')}'(决策票 07:五态全动会把优先级语义淹没)`)
- check('A3-unknown-does-not-guess-normal',
- deviceEffectOf('MAINTENANCE') === '' &&
- deviceEffectOf('') === '' &&
- deviceEffectOf(undefined) === '',
- '未知/空/undefined 都无动效(状态都不知道,更不能编一个"正在报警"的样子)')
- check('A4-effect-lives-in-the-device-table',
- DEVICE_STATUS_VISUALS.ALARM.effect === 'pulse' && DEVICE_UNKNOWN_VISUAL.effect === null,
- 'effect 维度挂在票 44 的设备独立词表上(不借点位表)')
- }
- // ================================================================ B 相位(跨时间采样)
- {
- // 纯逻辑也要能证明"值随时间变",否则只是映射表对。
- const samples = [0, 200, 400, 600, 800, 1000, 1200, 1400]
- const pulses = samples.map((t) => pulseStrengthAt(t))
- const blinks = samples.map((t) => blinkStrengthAt(t))
- const spins = samples.map((t) => spinAngleAt(t))
- check('B1-pulse-varies-over-time',
- new Set(pulses.map((v) => v.toFixed(4))).size >= 3 && Math.min(...pulses) >= 0 && Math.max(...pulses) <= 1,
- `脉冲强度采样 ${pulses.map((v) => v.toFixed(2)).join(',')}`)
- check('B2-blink-varies-over-time',
- new Set(blinks).size >= 2 && Math.min(...blinks) > 0 && Math.max(...blinks) <= 1,
- `闪烁强度采样 ${blinks.map((v) => v.toFixed(2)).join(',')}`)
- check('B3-spin-advances-monotonically-within-period',
- spins[0] < spins[1] && spins[1] < spins[2] && spins[7] < Math.PI * 2,
- `旋转角采样 ${spins.map((v) => v.toFixed(2)).join(',')}`)
- check('B4-phase-is-periodic',
- Math.abs(effectPhaseAt(DEVICE_EFFECT.PULSE, 0) - effectPhaseAt(DEVICE_EFFECT.PULSE, 1600)) < 1e-9,
- '相位按周期回绕(周期由 EFFECT_PERIOD_MS 定义)')
- check('B5-no-effect-has-zero-phase',
- effectPhaseAt('', 500) === 0 && effectPhaseAt(null, 500) === 0,
- '无动效的相位恒为 0(不会给正常/离线算出动画)')
- }
- // ================================================================ C 门控与降级
- {
- const rows = [device('EQ-A', 'ALARM'), device('EQ-W', 'WARNING'), device('EQ-O', 'WORK_ORDER'), device('EQ-N', 'NORMAL'), device('EQ-F', 'OFFLINE')]
- const off = planDeviceEffects({ devices: rows, enabled: false, abnormalLimit: 30 })
- check('C1-disabled-means-no-animation',
- off.enabled === false && off.animated.length === 0 && off.degraded === false,
- `特效关闭:动效台数 ${off.animated.length}(静态色/形/文字仍由反应量与实体属性表达)`)
- const on = planDeviceEffects({ devices: rows, enabled: true, abnormalLimit: 30 })
- check('C2-enabled-animates-only-attention-three',
- on.enabled === true && on.degraded === false &&
- on.animated.map((item) => item.code).sort().join(',') === 'EQ-A,EQ-O,EQ-W',
- `参与动效 ${on.animated.map((item) => `${item.code}:${item.effect}`).join(' ')}(正常/离线不在内)`)
- check('C3-abnormal-count-is-attention-only',
- on.abnormalCount === 2,
- `异常台数 ${on.abnormalCount}(ALARM/WARNING;工单处置不算异常,判据来自票 46 的单一住址)`)
- // 30 台降级:31 台异常 ⇒ 动画全停 + 静态异常标记仍在。
- const many = Array.from({ length: 31 }, (_, i) => device(`EQ-M${i}`, 'ALARM'))
- const degraded = planDeviceEffects({ devices: many, enabled: true, abnormalLimit: DEFAULT_ABNORMAL_LIMIT })
- check('C4-over-limit-degrades-animation',
- degraded.degraded === true && degraded.animated.length === 0 && degraded.abnormalCount === 31,
- `31 台异常 ⇒ 降级=${degraded.degraded}、动效台数=${degraded.animated.length}(动画全停,不是"只给前 30 台动")`)
- check('C5-degraded-keeps-abnormal-visible',
- degraded.staticAbnormal.length === 31,
- `降级后仍保留 ${degraded.staticAbnormal.length} 台静态异常标记(绝不静默删点/变成"什么都没画")`)
- check('C6-degraded-note-tells-the-count',
- degradedAbnormalNote(degraded) === '31 台异常' && degradedAbnormalNote(on) === '',
- `降级文案「${degradedAbnormalNote(degraded)}」;未降级不显示计数`)
- // 边界:恰好等于上限**不降级**("超过 30 台"才降)。
- const exactly = Array.from({ length: 30 }, (_, i) => device(`EQ-E${i}`, 'WARNING'))
- const atLimit = planDeviceEffects({ devices: exactly, enabled: true, abnormalLimit: 30 })
- check('C7-exactly-at-limit-does-not-degrade',
- atLimit.degraded === false && atLimit.animated.length === 30,
- `恰好 30 台异常:降级=${atLimit.degraded}、动效台数=${atLimit.animated.length}("超过"才降)`)
- // 可调阈值:上限调到 10,40 台异常(23 台异常的场景同构)⇒ 降级。
- const calibrated = planDeviceEffects({
- devices: [...Array.from({ length: 23 }, (_, i) => device(`EQ-X${i}`, i % 2 ? 'ALARM' : 'WARNING')),
- ...Array.from({ length: 17 }, (_, i) => device(`EQ-Y${i}`, 'NORMAL'))],
- enabled: true,
- abnormalLimit: 10,
- })
- check('C8-limit-is-configurable',
- calibrated.degraded === true && calibrated.abnormalCount === 23 && calibrated.animated.length === 0,
- `上限 10 + 23 台异常 ⇒ 降级(与票 08 现场校准同构:已降级、动效台数 0、异常台数 23 仍在)`)
- }
- // ================================================================ D 配置(map.effects)
- {
- const builtin = builtinConfig()
- check('D1-builtin-defaults',
- builtin.map.effects.enabled === null && builtin.map.effects.abnormalLimit === 30,
- `内置默认 enabled=${builtin.map.effects.enabled}(null=跟随构建形态)abnormalLimit=${builtin.map.effects.abnormalLimit}`)
- const explicit = normalizeConfig({ map: { effects: { enabled: true, abnormalLimit: 5 } } })
- check('D2-explicit-values-win',
- explicit.config.map.effects.enabled === true && explicit.config.map.effects.abnormalLimit === 5,
- `显式 true/5 被采纳:${JSON.stringify(explicit.config.map.effects)}`)
- const dirty = normalizeConfig({ map: { effects: { enabled: 'yes', abnormalLimit: 0 } } })
- check('D3-dirty-values-fall-back-and-report',
- dirty.config.map.effects.enabled === null &&
- dirty.config.map.effects.abnormalLimit === 30 &&
- dirty.issues.some((item) => item.includes('map.effects.enabled')) &&
- dirty.issues.some((item) => item.includes('map.effects.abnormalLimit')),
- `脏值退回默认并记 issue:${dirty.issues.filter((i) => i.includes('map.effects')).join(' | ')}`)
- }
- // ================================================================ E 科技感光环(决策票 07 新增方向)
- {
- const samples = [0, 150, 300, 450, 600, 750, 900, 1050, 1200, 1350, 1500, 1650]
- // 多层同心环:层数 ≥3(参考图 3–4 层),每层都随时间扩张。
- check('E1-ring-layers-are-multi',
- RING_LAYERS >= 3,
- `光环层数 ${RING_LAYERS}(决策票 07 参考图:3–4 层同心扩散环)`)
- const layerScales = Array.from({ length: RING_LAYERS }, (_, layer) =>
- samples.map((t) => ringScaleAt(layer, t, DEVICE_EFFECT.PULSE)))
- check('E2-every-ring-expands-over-time',
- layerScales.every((list) => new Set(list.map((v) => v.toFixed(4))).size >= 5),
- `每层都在扩张(各层 scale 取值数:${layerScales.map((l) => new Set(l.map((v) => v.toFixed(4))).size).join('/')})`)
- check('E3-ring-scales-outward-and-fades',
- ringScaleAt(0, 0, DEVICE_EFFECT.PULSE) >= 1 &&
- ringScaleAt(0, 1700, DEVICE_EFFECT.PULSE) > 2.5 &&
- ringAlphaAt(0, 1700, DEVICE_EFFECT.PULSE) < ringAlphaAt(0, 100, DEVICE_EFFECT.PULSE),
- `环由内向外扩张(scale→${ringScaleAt(0, 1700, DEVICE_EFFECT.PULSE).toFixed(2)})且渐隐(alpha ${ringAlphaAt(0, 100, DEVICE_EFFECT.PULSE).toFixed(2)}→${ringAlphaAt(0, 1700, DEVICE_EFFECT.PULSE).toFixed(2)})`)
- // 层间相位错开 ⇒ 同一时刻各层 scale 不同("连续向外扩散"的观感来源)。
- const staggered = samples.some((t) => new Set(
- Array.from({ length: RING_LAYERS }, (_, layer) => ringScaleAt(layer, t, DEVICE_EFFECT.PULSE).toFixed(3))).size === RING_LAYERS)
- check('E4-layers-are-phase-staggered', staggered,
- '同一时刻各层 scale 互不相同(层间错开相位 ⇒ 连续扩散观感,而非同涨同落)')
- // 中心辉光:只给最高优先级(报警),且随时间呼吸。
- const glowSamples = samples.map((t) => glowStrengthAt(t))
- check('E5-glow-breathes-for-alarm-only',
- new Set(glowSamples.map((v) => v.toFixed(4))).size >= 5 &&
- ringFrameAt('ALARM', 300).glow !== null &&
- ringFrameAt('WARNING', 300).glow === null &&
- ringFrameAt('WORK_ORDER', 300).glow === null,
- `辉光只给报警(三个状态辉光:${['ALARM', 'WARNING', 'WORK_ORDER'].map((s) => (ringFrameAt(s, 300).glow ? '有' : '无')).join('/')}),强度取值数 ${new Set(glowSamples.map((v) => v.toFixed(4))).size}`)
- // 无动效状态不得有光环(正常/离线/未知)。
- check('E6-no-rings-for-static-states',
- ['NORMAL', 'OFFLINE', 'MAINTENANCE'].every((status) => {
- const frame = ringFrameAt(status, 500)
- return frame.rings.length === 0 && frame.glow === null
- }),
- '正常/离线/未知**没有任何光环**(决策票 07:静态状态不参与动效)')
- // 三种需处置状态都有环(层数一致 ⇒ 不靠层数表达优先级)。
- check('E7-all-attention-states-have-rings',
- ['ALARM', 'WARNING', 'WORK_ORDER'].every((status) => ringFrameAt(status, 500).rings.length === RING_LAYERS),
- `三种需处置状态都是 ${RING_LAYERS} 层环(层数一致,差异靠节奏与色相表达)`)
- }
- const failed = checks.filter((item) => !item.pass)
- console.log('')
- console.log(`T47 设备状态动效检查:${checks.length - failed.length}/${checks.length} 项通过。`)
- if (failed.length) {
- console.log('未通过:')
- for (const item of failed) console.log(` · ${item.name}`)
- process.exit(1)
- }
- console.log('T47 设备状态动效全部通过。')
|