|
|
@@ -49,7 +49,7 @@ import 'cesium/Build/Cesium/Widgets/widgets.css'
|
|
|
import { EMPHASIS_STYLE, shouldEmphasize } from '../domain/mapEmphasis.js'
|
|
|
// 票 47:动效的**相位与映射**同样在 domain(纯函数,可跨时间采样断言);
|
|
|
// 本层只把相位写成 billboard 的 scale/alpha/rotation。
|
|
|
-import { DEVICE_EFFECT, blinkStrengthAt, pulseStrengthAt, spinAngleAt } from '../domain/deviceEffects.js'
|
|
|
+import { DEVICE_EFFECT, RING_LAYERS, blinkStrengthAt, glowScaleAt, glowStrengthAt, hasGlow, pulseStrengthAt, ringEntityCount, ringFrameAt, spinAngleAt } from '../domain/deviceEffects.js'
|
|
|
|
|
|
const DEG = Math.PI / 180
|
|
|
|
|
|
@@ -1401,6 +1401,94 @@ const deviceEffectState = {
|
|
|
/** 反应量写下的**基准** scale/alpha(动效在其上做乘性修饰)。键=实体 id。 */
|
|
|
const deviceBaseStyle = new Map()
|
|
|
|
|
|
+/**
|
|
|
+ * 票 47(科技感方向):光环实体池。
|
|
|
+ *
|
|
|
+ * ── 为什么用**独立 billboard 实体**而不是改设备图标本身 ──
|
|
|
+ * 决策票 07 的参考图要的是「多层同心扩散环 + 中心辉光 + 能量描边」;
|
|
|
+ * 设备图标只有一个 billboard,塞不下多层环。所以另建一层光环实体(`fx|` 前缀):
|
|
|
+ * · **不带 `deviceCode`** ⇒ 按研究笔记第 7 条,它不参与设备拾取判定,不会挡住设备点;
|
|
|
+ * · 位置跟设备同坐标,随图层一起重建/销毁(特效挂在图层上);
|
|
|
+ * · 每台最多 `RING_LAYERS` 个环 + 1 个辉光,同处一个数据源(一次 draw call 量级)。
|
|
|
+ */
|
|
|
+const RING_ENTITY_PREFIX = 'fx|'
|
|
|
+
|
|
|
+function ringEntitiesOf(source, code) {
|
|
|
+ return source.entities.values.filter((entity) =>
|
|
|
+ typeof entity.id === 'string' &&
|
|
|
+ entity.id.startsWith(RING_ENTITY_PREFIX) &&
|
|
|
+ entity.id.includes(`|${code}|`))
|
|
|
+}
|
|
|
+
|
|
|
+/** 建/更新某台设备的光环实体(层数固定:缺则建、数量不符则整组重建)。 */
|
|
|
+function ensureRingEntities(source, device, color, fxImageOf) {
|
|
|
+ const size = Number.isFinite(device.size) ? device.size : 14
|
|
|
+ // 层数与"要不要辉光"都问 domain(引擎不认识业务状态名,只认"几层环")。
|
|
|
+ const wanted = ringEntityCount(device.status)
|
|
|
+ if (!wanted) return []
|
|
|
+ const existing = ringEntitiesOf(source, device.code)
|
|
|
+ if (existing.length === wanted) return existing
|
|
|
+ for (const entity of existing) source.entities.remove(entity)
|
|
|
+ const created = []
|
|
|
+ for (let layer = 0; layer < RING_LAYERS; layer += 1) {
|
|
|
+ created.push(source.entities.add({
|
|
|
+ id: `${RING_ENTITY_PREFIX}${device.pointCode}|${device.code}|ring${layer}`,
|
|
|
+ position: Cartesian3.fromDegrees(device.longitude, device.latitude),
|
|
|
+ billboard: {
|
|
|
+ // 环用**能量场图片**(径向渐变柔和边缘):比 2px 描边圆更像特效,
|
|
|
+ // 放大时边缘自然扩散,不会一放大就细成一根线(首版真机截图抓到的问题)。
|
|
|
+ image: fxImageOf('ring', color, size),
|
|
|
+ width: size,
|
|
|
+ height: size,
|
|
|
+ scale: 1,
|
|
|
+ color: Color.WHITE.withAlpha(0),
|
|
|
+ heightReference: HeightReference.CLAMP_TO_GROUND,
|
|
|
+ verticalOrigin: VerticalOrigin.CENTER,
|
|
|
+ },
|
|
|
+ properties: {
|
|
|
+ effectFx: true,
|
|
|
+ effectFxKind: 'ring',
|
|
|
+ effectFxLayer: layer,
|
|
|
+ effectFxFor: device.code,
|
|
|
+ effectFxStatus: device.status,
|
|
|
+ // 刻意**不写 deviceCode / monitoringPointCode**:光环不冒充设备、不吃拾取。
|
|
|
+ },
|
|
|
+ }))
|
|
|
+ }
|
|
|
+ if (hasGlow(device.status)) {
|
|
|
+ created.push(source.entities.add({
|
|
|
+ id: `${RING_ENTITY_PREFIX}${device.pointCode}|${device.code}|glow`,
|
|
|
+ position: Cartesian3.fromDegrees(device.longitude, device.latitude),
|
|
|
+ billboard: {
|
|
|
+ image: fxImageOf('glow', color, size),
|
|
|
+ width: size,
|
|
|
+ height: size,
|
|
|
+ scale: 1,
|
|
|
+ color: Color.WHITE.withAlpha(0),
|
|
|
+ heightReference: HeightReference.CLAMP_TO_GROUND,
|
|
|
+ verticalOrigin: VerticalOrigin.CENTER,
|
|
|
+ },
|
|
|
+ properties: {
|
|
|
+ effectFx: true,
|
|
|
+ effectFxKind: 'glow',
|
|
|
+ effectFxLayer: -1,
|
|
|
+ effectFxFor: device.code,
|
|
|
+ effectFxStatus: device.status,
|
|
|
+ },
|
|
|
+ }))
|
|
|
+ }
|
|
|
+ return created
|
|
|
+}
|
|
|
+
|
|
|
+/** 清掉某台(或全部)光环实体。 */
|
|
|
+function clearRingEntities(source, code = '') {
|
|
|
+ for (const entity of source.entities.values.slice()) {
|
|
|
+ if (typeof entity.id !== 'string' || !entity.id.startsWith(RING_ENTITY_PREFIX)) continue
|
|
|
+ if (code && !entity.id.includes(`|${code}|`)) continue
|
|
|
+ source.entities.remove(entity)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
function baseStyleOf(entityId) {
|
|
|
return deviceBaseStyle.get(entityId) || { scale: 1, alpha: 1 }
|
|
|
}
|
|
|
@@ -1451,6 +1539,40 @@ function writeEffectFrame() {
|
|
|
billboard.color = Color.WHITE.withAlpha(base.alpha * (0.45 + 0.55 * factor))
|
|
|
billboard.rotation = 0
|
|
|
}
|
|
|
+
|
|
|
+ // ── 科技感光环层(多层同心扩散环 + 中心辉光)──
|
|
|
+ // 光环实体不带 deviceCode,所以**不参与上面的设备图标循环**,也不吃拾取。
|
|
|
+ const selectedBoost = 1.25
|
|
|
+ for (const entity of source.entities.values) {
|
|
|
+ const billboard = entity.billboard
|
|
|
+ const properties = entity.properties || {}
|
|
|
+ const isFx = properties.effectFx && properties.effectFx.getValue && properties.effectFx.getValue() === true
|
|
|
+ if (!billboard || !isFx) continue
|
|
|
+ const forCode = properties.effectFxFor && properties.effectFxFor.getValue ? properties.effectFxFor.getValue() : ''
|
|
|
+ const status = properties.effectFxStatus && properties.effectFxStatus.getValue ? properties.effectFxStatus.getValue() : ''
|
|
|
+ const kind = properties.effectFxKind && properties.effectFxKind.getValue ? properties.effectFxKind.getValue() : ''
|
|
|
+ const layer = properties.effectFxLayer && properties.effectFxLayer.getValue ? properties.effectFxLayer.getValue() : 0
|
|
|
+ const effect = forCode ? animated.get(forCode) : ''
|
|
|
+ if (!effect) {
|
|
|
+ // 无动效(正常/离线/未知、特效关闭或降级):光环**完全透明**,不留残影。
|
|
|
+ billboard.scale = 1
|
|
|
+ billboard.color = Color.WHITE.withAlpha(0)
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ const isSelected = deviceEffectState.selectedCode && forCode === deviceEffectState.selectedCode
|
|
|
+ const boost = isSelected ? selectedBoost : 1
|
|
|
+ if (kind === 'glow') {
|
|
|
+ const strength = glowStrengthAt(now)
|
|
|
+ billboard.scale = glowScaleAt(now) * boost
|
|
|
+ billboard.color = Color.WHITE.withAlpha(0.32 * strength * boost)
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ // 多层环:每层相位错开、由内向外扩张渐隐。
|
|
|
+ const frame = ringFrameAt(status, now)
|
|
|
+ const ring = frame.rings[layer] || { scale: 1, alpha: 0 }
|
|
|
+ billboard.scale = ring.scale * boost
|
|
|
+ billboard.color = Color.WHITE.withAlpha(Math.min(1, ring.alpha * boost))
|
|
|
+ }
|
|
|
deviceEffectState.tick += 1
|
|
|
}
|
|
|
|
|
|
@@ -1477,6 +1599,33 @@ export function applyDeviceEffects(viewer, plan, options = {}) {
|
|
|
deviceEffectState.plan = plan || null
|
|
|
deviceEffectState.selectedCode = (options && options.selectedCode) || ''
|
|
|
const running = Boolean(viewer && plan && plan.enabled === true && !plan.degraded && plan.animated && plan.animated.length)
|
|
|
+ // 光环实体按计划重建:参与的设备建环,其余(含降级/关闭时全部)清掉 —— 降级后动画全停,
|
|
|
+ // 光环也必须一起消失,否则"停了动画却还留着环"会被读成"还在报警"。
|
|
|
+ const source = viewer && (deviceSources.get(viewer) || (viewer.dataSources ? viewer.dataSources.getByName(DEVICE_MARKER_SOURCE)[0] : null))
|
|
|
+ if (source) {
|
|
|
+ if (running && options && options.devices) {
|
|
|
+ const animatedCodes = new Set((plan.animated || []).map((item) => item.code))
|
|
|
+ for (const device of options.devices) {
|
|
|
+ if (!animatedCodes.has(device.code)) { clearRingEntities(source, device.code); continue }
|
|
|
+ ensureRingEntities(
|
|
|
+ source,
|
|
|
+ device,
|
|
|
+ options.colorOf ? options.colorOf(device) : Color.WHITE,
|
|
|
+ options.fxImageOf || (() => ''),
|
|
|
+ )
|
|
|
+ }
|
|
|
+ // 已不在本批设备里的光环(换点/换状态)也要清。
|
|
|
+ const known = new Set(options.devices.map((item) => item.code))
|
|
|
+ for (const entity of source.entities.values.slice()) {
|
|
|
+ const isFx = entity.properties && entity.properties.effectFx && entity.properties.effectFx.getValue && entity.properties.effectFx.getValue() === true
|
|
|
+ if (!isFx) continue
|
|
|
+ const forCode = entity.properties.effectFxFor && entity.properties.effectFxFor.getValue ? entity.properties.effectFxFor.getValue() : ''
|
|
|
+ if (!known.has(forCode) || !animatedCodes.has(forCode)) source.entities.remove(entity)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ clearRingEntities(source)
|
|
|
+ }
|
|
|
+ }
|
|
|
if (running) ensureEffectLoop()
|
|
|
// 即使不跑动效也要写一帧:把上一轮可能残留的放大/透明度**写回基准**。
|
|
|
writeEffectFrame()
|
|
|
@@ -1524,12 +1673,32 @@ export function readDeviceEffects(viewer) {
|
|
|
rotation: billboard.rotation && billboard.rotation.getValue ? billboard.rotation.getValue() : (billboard.rotation || 0),
|
|
|
})
|
|
|
}
|
|
|
+ // 科技感光环的逐层读数(多层同心环 + 辉光):走查据此断言"环真的在扩张渐隐"。
|
|
|
+ const fx = []
|
|
|
+ for (const entity of source.entities.values) {
|
|
|
+ const billboard = entity.billboard
|
|
|
+ if (!billboard) continue
|
|
|
+ const properties = entity.properties || {}
|
|
|
+ const get = (key) => (properties[key] && properties[key].getValue ? properties[key].getValue() : '')
|
|
|
+ if (get('effectFx') !== true) continue
|
|
|
+ const color = billboard.color && billboard.color.getValue ? billboard.color.getValue() : null
|
|
|
+ fx.push({
|
|
|
+ for: get('effectFxFor'),
|
|
|
+ kind: get('effectFxKind'),
|
|
|
+ layer: get('effectFxLayer'),
|
|
|
+ status: get('effectFxStatus'),
|
|
|
+ scale: billboard.scale && billboard.scale.getValue ? billboard.scale.getValue() : billboard.scale,
|
|
|
+ alpha: color ? color.alpha : null,
|
|
|
+ })
|
|
|
+ }
|
|
|
return {
|
|
|
tick: deviceEffectState.tick,
|
|
|
running: deviceEffectState.raf !== 0,
|
|
|
degraded: Boolean(deviceEffectState.plan && deviceEffectState.plan.degraded),
|
|
|
enabled: Boolean(deviceEffectState.plan && deviceEffectState.plan.enabled),
|
|
|
+ ringLayers: RING_LAYERS,
|
|
|
devices,
|
|
|
+ fx,
|
|
|
}
|
|
|
}
|
|
|
|