|
|
@@ -61,7 +61,8 @@ import { removeSceneTileset } from './tilesetCleanup.js'
|
|
|
const regionControllers = new WeakMap()
|
|
|
// 票 47:动效的**相位与映射**同样在 domain(纯函数,可跨时间采样断言);
|
|
|
// 本层只把相位写成 billboard 的 scale/alpha/rotation。
|
|
|
-import { DEVICE_EFFECT, RING_LAYERS, blinkStrengthAt, glowScaleAt, glowStrengthAt, hasGlow, pulseStrengthAt, ringEntityCount, ringFrameAt, spinAngleAt } from '../domain/deviceEffects.js'
|
|
|
+// 票 76(C5):环实体与逐帧动效已退役,引擎只保留「层数」这一个读数常量(DOM 侧层数由 profile 决定)。
|
|
|
+import { RING_LAYERS } from '../domain/deviceEffects.js'
|
|
|
// 场景调色的兜底色直接取 domain 的默认(不再手抄一份 #2fa8ff 的浮点,两处会漂)。
|
|
|
// 注意:GRADE_DEFAULTS 来自 sceneGrade;parseHexColor 的 owner 是 fog.js ——
|
|
|
// sceneGrade 自己也只是导入它,从这里导 parseHexColor 会是 ESM 链接错误(引擎整个起不来)。
|
|
|
@@ -1712,25 +1713,10 @@ export function projectOverlapSymbols(viewer) {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 票 47:点位标记(监测点/未关联设备)的每帧缩放读数。
|
|
|
- * 强调(`highlightEntities` 的 1.55)写在 billboard.scale 上,DOM 要镜像同一个量,
|
|
|
- * 否则面板联动强调时两层尺寸不一致(维护者走查抓到的「小了一圈」)。
|
|
|
- * @returns {Map<string, number>} `kind|code` → scale(没实体 = 不在图上,DOM 自行隐藏)
|
|
|
+ * 票 76(C3):点位强调 scale 的 billboard 回读**已退役**。
|
|
|
+ * 真值改由服务层 `mapService.pointEmphasisScales()` 产出(同一份 domain `shouldEmphasize`),
|
|
|
+ * DOM 不再从被隐藏的 billboard 实体读尺寸 —— 与票 58 §四「DOM 是唯一视觉尺寸源」一致。
|
|
|
*/
|
|
|
-export function readPointMarkerScales(viewer) {
|
|
|
- const scales = new Map()
|
|
|
- const source = (viewer && markerSources.get(viewer)) || (viewer && viewer.dataSources ? viewer.dataSources.getByName(POINT_MARKER_SOURCE)[0] : null)
|
|
|
- if (!source) return scales
|
|
|
- for (const entity of source.entities.values) {
|
|
|
- const properties = entity.properties || {}
|
|
|
- const read = (key) => (properties[key] && properties[key].getValue ? properties[key].getValue() : '')
|
|
|
- const code = read('pointCode')
|
|
|
- if (!code) continue
|
|
|
- const kind = read('pointKind') || 'monitoring-point'
|
|
|
- scales.set(`${kind}|${code}`, entity.billboard && entity.billboard.scale ? Number(entity.billboard.scale.getValue()) || 1 : 1)
|
|
|
- }
|
|
|
- return scales
|
|
|
-}
|
|
|
|
|
|
/** 只读回关联设备图层的真实内容(走查据此断言「设备真的画在独立图层上」,以及「画成的样子来自接缝」)。 */
|
|
|
export function readAssociatedDevices(viewer) {
|
|
|
@@ -1809,32 +1795,7 @@ export function clearAssociatedDevices(viewer) {
|
|
|
deviceMarkerSource(viewer).entities.removeAll()
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * 屏幕坐标 → **设备标记身份**(票 42 的独立拾取入口;票 43 会在此基础上做双向联动)。
|
|
|
- *
|
|
|
- * 与 `pickPointMarkerAt()` 同一条纪律:只认本图层上带 `deviceCode` 的实体,
|
|
|
- * 回**身份**不回 Cesium 实体,且**设备优先于监测点**(票 15 的拾取优先级)。
|
|
|
- */
|
|
|
-function overlapTargetAt(viewer, windowX, windowY) {
|
|
|
- const targets = readOverlapTargets(viewer)
|
|
|
- let nearest = null
|
|
|
- let distance = Infinity
|
|
|
- for (const target of targets.filter((item) => item.visible)) {
|
|
|
- const current = Math.hypot(target.x - windowX, target.y - windowY)
|
|
|
- if (current <= 24 && current < distance) {
|
|
|
- nearest = target
|
|
|
- distance = current
|
|
|
- }
|
|
|
- }
|
|
|
- if (!nearest) return null
|
|
|
- if (nearest.aggregate) return { kind: 'monitoring-device-overlap', code: '', pointCode: '', overlapGroup: nearest.overlapGroup }
|
|
|
- if (nearest.deviceCode) return {
|
|
|
- kind: 'monitoring-device', code: nearest.deviceCode, pointCode: nearest.pointCode, status: nearest.status,
|
|
|
- overlapEntry: nearest.overlapEntry, overlapGroup: nearest.overlapGroup,
|
|
|
- }
|
|
|
- return null
|
|
|
-}
|
|
|
-
|
|
|
+/** 设备、监测点之外的第三层拾取入口:只返回管段/构件身份(管线是 Cesium 几何,不属标记层)。 */
|
|
|
export function pickPipeObjectAt(viewer, windowX, windowY) {
|
|
|
if (!viewer || !viewer.scene || !viewer.canvas || typeof viewer.scene.pick !== 'function') return null
|
|
|
const rect = viewer.canvas.getBoundingClientRect()
|
|
|
@@ -1861,59 +1822,6 @@ export function pickPipeObjectAt(viewer, windowX, windowY) {
|
|
|
return null
|
|
|
}
|
|
|
|
|
|
-/** 设备、监测点之外的第三层拾取入口:只返回管段/构件身份。 */
|
|
|
-
|
|
|
-/** 仅穿过本管线图层;其它场景对象仍按 Cesium 的前后顺序遮挡。 */
|
|
|
-function pickThroughPipelines(viewer, position) {
|
|
|
- const scene = viewer.scene
|
|
|
- const picked = scene.pick(position)
|
|
|
- const source = pipelineSource(viewer)
|
|
|
- const isPipeline = (hit) => {
|
|
|
- const id = hit && hit.id
|
|
|
- return Boolean(id && (id.kind === 'pipe-model-batch' || (source && source.entities.values.includes(id))))
|
|
|
- }
|
|
|
- if (!isPipeline(picked)) return picked
|
|
|
- // 普通拾取不受影响;只有最上层是管线时才执行多层拾取。
|
|
|
- if (typeof scene.drillPick !== 'function') return null
|
|
|
- return scene.drillPick(position).find((hit) => !isPipeline(hit)) || null
|
|
|
-}
|
|
|
-
|
|
|
-export function pickDeviceMarkerAt(viewer, windowX, windowY) {
|
|
|
- if (!viewer || !viewer.scene || !viewer.canvas || typeof viewer.scene.pick !== 'function') return null
|
|
|
-
|
|
|
- // 票45:展开后的设备拥有屏幕pixelOffset;先按公开投影读数做近邻身份判定,
|
|
|
- // 避免同屏监测点/地形顶层对象让普通scene.pick吞掉真实设备。仍保持普通拾取语义。
|
|
|
- const projectedHit = overlapTargetAt(viewer, windowX, windowY)
|
|
|
- if (projectedHit) return projectedHit
|
|
|
-
|
|
|
- const rect = viewer.canvas.getBoundingClientRect()
|
|
|
- const picked = pickThroughPipelines(viewer, new Cartesian2(windowX - rect.left, windowY - rect.top))
|
|
|
- const entity = picked && picked.id
|
|
|
- const properties = entity && entity.properties
|
|
|
- if (properties) {
|
|
|
- const read = (key) => (properties[key] ? properties[key].getValue() : '')
|
|
|
- if (read('overlapAggregate') === true) {
|
|
|
- return {
|
|
|
- kind: 'monitoring-device-overlap',
|
|
|
- code: '',
|
|
|
- pointCode: read('overlapPointCode'),
|
|
|
- overlapGroup: read('overlapGroup'),
|
|
|
- overlapCount: read('overlapCount'),
|
|
|
- }
|
|
|
- }
|
|
|
- const code = read('deviceCode')
|
|
|
- if (code) return {
|
|
|
- kind: 'monitoring-device', code, pointCode: read('monitoringPointCode'), status: read('deviceStatus'),
|
|
|
- name: read('deviceName') || '',
|
|
|
- overlapEntry: read('overlapEntry') === true, overlapGroup: read('overlapGroup'),
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 重叠聚合与监测点同屏时,scene.pick 可能只返回顶层点位;
|
|
|
- // 用聚合实体投影做近邻补偿,仍是普通拾取、不改真实位置。
|
|
|
- return overlapTargetAt(viewer, windowX, windowY)
|
|
|
-}
|
|
|
-
|
|
|
// ================================================================ 设备点的对应反应(票 43)
|
|
|
|
|
|
/**
|
|
|
@@ -1947,8 +1855,8 @@ export function applyDeviceReactions(viewer, reactions, styleOf) {
|
|
|
const reaction = byId.get(entity.id) || 'normal'
|
|
|
const style = styleOf(reaction) || { scale: 1, alpha: 1, ring: false }
|
|
|
const billboard = entity.billboard
|
|
|
- if (!billboard || (entity.properties && (entity.properties.effectFx ||
|
|
|
- (entity.properties.overlapAggregate && entity.properties.overlapAggregate.getValue() === true)))) continue
|
|
|
+ // 票 76(C5):光环实体退役后只剩聚合符号需要跳过(它不吃反应量)。
|
|
|
+ if (!billboard || (entity.properties && entity.properties.overlapAggregate && entity.properties.overlapAggregate.getValue() === true)) continue
|
|
|
|
|
|
const isDevice = entity.properties && entity.properties.deviceCode
|
|
|
billboard.scale = isDevice ? Math.max(1, style.scale) : style.scale
|
|
|
@@ -1996,100 +1904,6 @@ 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) {
|
|
|
- // 票 45:光环基准尺寸取标记**绘制宽**(`drawWidth`,设备 26.72/点位 32.72),不再取
|
|
|
- // 14px 图标尺寸 —— 14px 基准下环带峰值半径只有 ~5.5px,要 scale≥1.92 才探出图标本体,
|
|
|
- // 而那时 alpha 已衰减到 0.59(「特效被图标尺寸压制」的直接成因)。
|
|
|
- // 旧调用方(单测/走查桩)没有 drawWidth 时回退原尺寸,行为不变。
|
|
|
- const size = Number.isFinite(device.drawWidth) ? device.drawWidth : (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,
|
|
|
- effectFxDisplayGroup: '',
|
|
|
- // 刻意**不写 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,
|
|
|
- effectFxDisplayGroup: '',
|
|
|
- },
|
|
|
- }))
|
|
|
- }
|
|
|
- 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 }
|
|
|
}
|
|
|
@@ -2143,198 +1957,57 @@ function writeDeviceBaseFrame(source) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * 动效帧(票 35 卡顿整改后的版本)。
|
|
|
- *
|
|
|
- * 与原实现的差别**只在不该做的写入上**:基础图标样式与光环实体属性都改成"变了才写",
|
|
|
- * 位置/像素偏移/显示组只在归属真的换了才重新绑定。视觉结果逐帧一致,但镜头静止或
|
|
|
- * 只有少量设备在报警时,几乎不再产生实体脏标记与集合重建。
|
|
|
- */
|
|
|
-function writeEffectFrame() {
|
|
|
- const viewer = deviceEffectState.viewer
|
|
|
- const source = viewer && (deviceSources.get(viewer) || (viewer.dataSources ? viewer.dataSources.getByName(DEVICE_MARKER_SOURCE)[0] : null))
|
|
|
- if (!viewer || !source) return false
|
|
|
- const animated = animatedByCode(deviceEffectState.plan)
|
|
|
- const now = Date.now()
|
|
|
- writeDeviceBaseFrame(source)
|
|
|
-
|
|
|
- // ── 科技感光环层(多层同心扩散环 + 中心辉光)──
|
|
|
- // 光环实体不带 deviceCode,所以**不参与上面的设备图标循环**,也不吃拾取。
|
|
|
- // 票 45:选中强化 1.25 → 1.55,与标记强调(highlightEntities 的 1.55)同步——
|
|
|
- // 选中时图标放大 1.55 而环只放大 1.25,两层会 visibly 分离(「小一圈」同源问题)。
|
|
|
- const selectedBoost = 1.55
|
|
|
- const owners = new Map(source.entities.values
|
|
|
- .filter((entity) => entity.properties && entity.properties.deviceCode)
|
|
|
- .map((entity) => [entity.properties.deviceCode.getValue(), entity]))
|
|
|
- const aggregates = new Map(source.entities.values
|
|
|
- .filter((entity) => entity.properties && entity.properties.overlapAggregate && entity.properties.overlapAggregate.getValue() === true)
|
|
|
- .map((entity) => [entity.properties.overlapGroup.getValue(), entity]))
|
|
|
- const entries = new Map([...owners.values()]
|
|
|
- .filter((entity) => entity.show !== false && entity.properties.overlapEntry && entity.properties.overlapEntry.getValue() === true)
|
|
|
- .map((entity) => [entity.properties.overlapGroup.getValue(), entity]))
|
|
|
- let animatedNow = 0
|
|
|
- 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) : ''
|
|
|
- const owner = owners.get(forCode)
|
|
|
- const groupKey = owner && owner.properties.overlapGroup ? owner.properties.overlapGroup.getValue() : ''
|
|
|
- const aggregate = aggregates.get(groupKey)
|
|
|
- const entry = entries.get(groupKey)
|
|
|
- const entryHasGroupEffect = entry && aggregate && animated.has(entry.properties.deviceCode.getValue()) &&
|
|
|
- entry.properties.deviceStatus.getValue() === aggregate.properties.deviceStatus.getValue()
|
|
|
- // 聚合最高状态的光环借用当前入口;设备来源身份和状态不变。
|
|
|
- const representsGroup = !entryHasGroupEffect && owner && owner.show === false && aggregate && aggregate.properties.overlapEffectCode &&
|
|
|
- aggregate.properties.overlapEffectCode.getValue() === forCode
|
|
|
- const displayOwner = representsGroup ? (entry || aggregate) : owner
|
|
|
- const shouldShow = Boolean(effect && source.show !== false && displayOwner && displayOwner.show !== false)
|
|
|
- if (entity.show !== shouldShow) entity.show = shouldShow
|
|
|
- let record = effectApplied.get(entity)
|
|
|
- if (!record) {
|
|
|
- record = { scale: undefined, alpha: undefined, rotation: undefined, owner: undefined, group: undefined }
|
|
|
- effectApplied.set(entity, record)
|
|
|
- }
|
|
|
- // 归属只在真的换了才重新绑位置/像素偏移(原实现每帧无条件重新赋值,等于每帧标脏)。
|
|
|
- if (record.owner !== displayOwner) {
|
|
|
- record.owner = displayOwner
|
|
|
- if (displayOwner && displayOwner.position) entity.position = displayOwner.position
|
|
|
- billboard.pixelOffset = displayOwner && displayOwner.billboard ? displayOwner.billboard.pixelOffset : ZERO_PIXEL_OFFSET
|
|
|
- }
|
|
|
- const displayGroup = representsGroup ? groupKey : ''
|
|
|
- if (record.group !== displayGroup) {
|
|
|
- record.group = displayGroup
|
|
|
- if (properties.effectFxDisplayGroup) properties.effectFxDisplayGroup.setValue(displayGroup)
|
|
|
- }
|
|
|
- if (!effect) {
|
|
|
- // 无动效(正常/离线/未知、特效关闭或降级):光环**完全透明**,不留残影。
|
|
|
- writeIfChanged(record, 'scale', 1, (v) => { billboard.scale = v })
|
|
|
- writeIfChanged(record, 'alpha', 0, () => { billboard.color = Color.WHITE.withAlpha(0) })
|
|
|
- continue
|
|
|
- }
|
|
|
- animatedNow += 1
|
|
|
- const isSelected = deviceEffectState.selectedCode && forCode === deviceEffectState.selectedCode
|
|
|
- const boost = isSelected ? selectedBoost : 1
|
|
|
- if (kind === 'glow') {
|
|
|
- const strength = glowStrengthAt(now)
|
|
|
- writeIfChanged(record, 'scale', glowScaleAt(now) * boost, (v) => { billboard.scale = v })
|
|
|
- writeIfChanged(record, 'alpha', 0.32 * strength * boost, (v) => { billboard.color = Color.WHITE.withAlpha(v) })
|
|
|
- continue
|
|
|
- }
|
|
|
- // 多层环:每层相位错开、由内向外扩张渐隐。
|
|
|
- const frame = ringFrameAt(status, now)
|
|
|
- const ring = frame.rings[layer] || { scale: 1, alpha: 0 }
|
|
|
- writeIfChanged(record, 'scale', ring.scale * boost, (v) => { billboard.scale = v })
|
|
|
- const flicker = effect === DEVICE_EFFECT.BLINK ? blinkStrengthAt(now) : 1
|
|
|
- writeIfChanged(record, 'alpha', Math.min(1, ring.alpha * boost * flicker), (v) => { billboard.color = Color.WHITE.withAlpha(v) })
|
|
|
- writeIfChanged(record, 'rotation', effect === DEVICE_EFFECT.SPIN ? spinAngleAt(now) : 0, (v) => { billboard.rotation = v })
|
|
|
- }
|
|
|
- deviceEffectState.tick += 1
|
|
|
- return animatedNow > 0
|
|
|
-}
|
|
|
-
|
|
|
-function ensureEffectLoop() {
|
|
|
- if (typeof window === 'undefined' || typeof window.requestAnimationFrame !== 'function') return
|
|
|
- if (deviceEffectState.raf) return
|
|
|
- const step = () => {
|
|
|
- if (!deviceEffectState.raf) return
|
|
|
- // 30Hz 足够:光环周期以秒计,60→30 的一半帧率肉眼几乎无差,
|
|
|
- // 但每帧省下的实体属性写入与 GPU 缓冲重建是实打实的。
|
|
|
- const now = typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now()
|
|
|
- if (now - (deviceEffectState.lastFrameAt || 0) >= 33) {
|
|
|
- deviceEffectState.lastFrameAt = now
|
|
|
- const moving = writeEffectFrame()
|
|
|
- // 没有任何动效时不主动要帧:让 requestRenderMode 真正进入省电/省算力的静止态。
|
|
|
- if (moving && deviceEffectState.viewer && deviceEffectState.viewer.scene) {
|
|
|
- deviceEffectState.viewer.scene.requestRender()
|
|
|
- }
|
|
|
- }
|
|
|
- deviceEffectState.raf = window.requestAnimationFrame(step)
|
|
|
- }
|
|
|
- deviceEffectState.raf = window.requestAnimationFrame(step)
|
|
|
-}
|
|
|
-
|
|
|
/**
|
|
|
* 应用动效计划(由服务层在设备图层同步后调用)。
|
|
|
+ *
|
|
|
+ * 票 76(C5):设备状态动效**全部由 DOM overlay 承载**(`buildFxLayers` + CSS keyframes)。
|
|
|
+ * 引擎侧不再建光环实体、也不再跑逐帧动效循环 —— 本函数只做两件事:
|
|
|
+ * ① 记录计划与选中(`setOverlapPresentation` 与读数面用);
|
|
|
+ * ② 把「反应量基准」(选中/hover/弱化)写到设备 billboard —— 它是 DOM 设备行的
|
|
|
+ * 投影真值(`projectDeviceMarkers` 的 scale/alpha 镜像),不是视觉本身。
|
|
|
* @param {Viewer} viewer
|
|
|
* @param {object} plan `domain/deviceEffects.js` 的 `planDeviceEffects()` 输出
|
|
|
- * @param {object} [options] `{ selectedCode }` 选中设备额外强化
|
|
|
- * @returns {boolean} 是否启动了动效
|
|
|
+ * @param {object} [options] `{ selectedCode }`
|
|
|
+ * @returns {boolean} 计划是否「在动」(DOM 据此挂特效;引擎不再逐帧写)
|
|
|
*/
|
|
|
export function applyDeviceEffects(viewer, plan, options = {}) {
|
|
|
deviceEffectState.viewer = viewer || null
|
|
|
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) {
|
|
|
- // 票 76(交互规格 2026-10-23):设备视觉让位 DOM 时,**billboard 层的环一律清掉** ——
|
|
|
- // 环的归属改由 DOM 决定:聚合点不挂环,只有「中心(被选中)设备」按自身状态挂环。
|
|
|
- // 否则旧环实体没有跟着 `hideDeviceVisual` 一起消失,观感上「特效逻辑说不清」。
|
|
|
- if (deviceVisualHidden) {
|
|
|
- clearRingEntities(source)
|
|
|
- } else 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)
|
|
|
- }
|
|
|
+ deviceEffectState.baseDirty = true
|
|
|
+ writeDeviceBaseFrame(source)
|
|
|
}
|
|
|
- if (running) ensureEffectLoop()
|
|
|
- else if (deviceEffectState.raf && typeof window !== 'undefined') {
|
|
|
- window.cancelAnimationFrame(deviceEffectState.raf)
|
|
|
- deviceEffectState.raf = 0
|
|
|
- }
|
|
|
- // 中心符号保持可读;停用时外围动画复位。
|
|
|
- writeEffectFrame()
|
|
|
- return running
|
|
|
+ deviceEffectState.tick += 1
|
|
|
+ return Boolean(plan && plan.enabled === true && !plan.degraded && plan.animated && plan.animated.length)
|
|
|
}
|
|
|
|
|
|
-/** 停止动效并写回基准(图层撤下 / 详情关闭 / 特效被关掉)。 */
|
|
|
+/** 停止动效(图层撤下 / 详情关闭):只清计划与选中,视觉已由 DOM 承载。 */
|
|
|
export function stopDeviceEffects() {
|
|
|
- if (typeof window !== 'undefined' && deviceEffectState.raf && typeof window.cancelAnimationFrame === 'function') {
|
|
|
- window.cancelAnimationFrame(deviceEffectState.raf)
|
|
|
- }
|
|
|
- deviceEffectState.raf = 0
|
|
|
deviceEffectState.plan = null
|
|
|
deviceEffectState.selectedCode = ''
|
|
|
- writeEffectFrame()
|
|
|
+ deviceEffectState.baseDirty = true
|
|
|
+ deviceEffectState.tick += 1
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 只读读回每台设备的**动效状态与当前帧属性**(走查的"真在跑"证据)。
|
|
|
+ * 只读读回设备行**当前的基准反应量**(走查用它判断选中/hover/弱化有没有落笔)。
|
|
|
*
|
|
|
- * 三层证据的第 2 层(场景图跨帧快照):走查对同一设备**隔帧读两次**,
|
|
|
- * 断言 `scale`/`alpha`(脉冲/闪烁)或 `rotation`(旋转环)确实变了,
|
|
|
- * 而正常/离线的设备**完全不变**。这样就不是"拿两份 JS 数据互比"的恒真断言。
|
|
|
+ * 票 76(C5):光环实体已退役 —— `fx` 恒为空数组;动效「真在跑」的证据改由 DOM 承担
|
|
|
+ * (`data-marker-effect*` 与 CSS 动画,见 markerOverlay)。这里如实返回空,不伪造。
|
|
|
*/
|
|
|
export function readDeviceEffects(viewer) {
|
|
|
const source = viewer && (deviceSources.get(viewer) || (viewer.dataSources ? viewer.dataSources.getByName(DEVICE_MARKER_SOURCE)[0] : null))
|
|
|
- if (!source) return { tick: deviceEffectState.tick, devices: [] }
|
|
|
const animated = animatedByCode(deviceEffectState.plan)
|
|
|
+ const summary = {
|
|
|
+ tick: deviceEffectState.tick,
|
|
|
+ running: false,
|
|
|
+ degraded: Boolean(deviceEffectState.plan && deviceEffectState.plan.degraded),
|
|
|
+ enabled: Boolean(deviceEffectState.plan && deviceEffectState.plan.enabled),
|
|
|
+ ringLayers: RING_LAYERS,
|
|
|
+ }
|
|
|
+ if (!source) return { ...summary, devices: [], fx: [] }
|
|
|
const devices = []
|
|
|
for (const entity of source.entities.values) {
|
|
|
const billboard = entity.billboard
|
|
|
@@ -2354,39 +2027,8 @@ 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'),
|
|
|
- displayGroup: get('effectFxDisplayGroup'),
|
|
|
- visible: source.show !== false && entity.show !== false && entity.isShowing !== false,
|
|
|
- pixelOffset: billboard.pixelOffset && billboard.pixelOffset.getValue ? billboard.pixelOffset.getValue() : billboard.pixelOffset,
|
|
|
- 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,
|
|
|
- rotation: billboard.rotation && billboard.rotation.getValue ? billboard.rotation.getValue() : (billboard.rotation || 0),
|
|
|
- })
|
|
|
- }
|
|
|
- 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,
|
|
|
- }
|
|
|
+ return { ...summary, devices, fx: [] }
|
|
|
}
|
|
|
-
|
|
|
/**
|
|
|
* 选中设备的**归属引线**:从设备点连到监测点标记中心(票 04/12 的面板锚点)。
|
|
|
*
|
|
|
@@ -2425,16 +2067,6 @@ export function clearDeviceLead(viewer) {
|
|
|
return true
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * 屏幕坐标 → 设备标记身份(**hover 用**,与 `pickDeviceMarkerAt` 同一条判据)。
|
|
|
- *
|
|
|
- * 单独一个函数只是为了让 hover 的调用点读起来是"我在做 hover 拾取",
|
|
|
- * 实现完全复用点选那一个(**不另写一套判据** —— 两套迟早会分歧)。
|
|
|
- */
|
|
|
-export function pickDeviceMarkerForHover(viewer, windowX, windowY) {
|
|
|
- return pickDeviceMarkerAt(viewer, windowX, windowY)
|
|
|
-}
|
|
|
-
|
|
|
/**
|
|
|
* 强调一批编码对应的地图实体(票 14:点事件详情时高亮关联设备与关联管线)。
|
|
|
*
|
|
|
@@ -2565,85 +2197,6 @@ function findDeviceMarker(viewer, code, pointCode) {
|
|
|
}) || null
|
|
|
}
|
|
|
|
|
|
-/** 贴地投影与屏幕分离共用一份口径,返回视觉中心及当前图标尺寸。 */
|
|
|
-function projectMarkerEntity(viewer, entity) {
|
|
|
- if (!viewer || !viewer.scene || !viewer.canvas || !entity || !entity.isShowing || !entity.position || !entity.billboard) return null
|
|
|
- const original = entity.position.getValue(viewer.clock.currentTime)
|
|
|
- if (!original) return null
|
|
|
- const ellipsoid = viewer.scene.globe.ellipsoid
|
|
|
- const carto = ellipsoid.cartesianToCartographic(original)
|
|
|
- if (!carto) return null
|
|
|
-
|
|
|
- // 地面高程:实体位置是静态的,地形瓦片也只会逐步补齐;每帧给每个实体查一次
|
|
|
- // `globe.getHeight`(四叉树下降)是纯重复。按实体缓存 3 秒,瓦片补齐后自然刷新。
|
|
|
- const nowMs = typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now()
|
|
|
- let height = 0
|
|
|
- const cachedHeight = entityGroundHeightCache.get(entity)
|
|
|
- if (cachedHeight && nowMs - cachedHeight.at < 3000) {
|
|
|
- height = cachedHeight.height
|
|
|
- } else {
|
|
|
- const sampled = viewer.scene.globe.getHeight(carto)
|
|
|
- height = Number.isFinite(sampled) ? sampled : 0
|
|
|
- entityGroundHeightCache.set(entity, { height, at: nowMs })
|
|
|
- }
|
|
|
-
|
|
|
- const world = Cartesian3.fromRadians(carto.longitude, carto.latitude, height)
|
|
|
- const camera = viewer.camera
|
|
|
- // 复用同一个 scratch 向量:每实体每帧 new Cartesian3 在高频投影里是纯垃圾。
|
|
|
- const toward = Cartesian3.subtract(world, camera.positionWC, projectionCache.toward)
|
|
|
- if (Cartesian3.dot(toward, camera.directionWC) <= 0) return null
|
|
|
- // 遮挡体同一帧内所有实体共用(原本每实体 new 一个,是纯浪费)。
|
|
|
- const occluder = occluderCached(viewer, camera)
|
|
|
- if (!occluder.isPointVisible(world)) return null
|
|
|
- const at = SceneTransforms.worldToWindowCoordinates(viewer.scene, world)
|
|
|
- if (!at || !Number.isFinite(at.x) || !Number.isFinite(at.y)) return null
|
|
|
- // 画布矩形只随窗口尺寸变;120ms 保鲜即可,不必每个实体读一次布局。
|
|
|
- const rect = canvasRectCached(viewer)
|
|
|
- const time = viewer.clock.currentTime
|
|
|
- const read = (property, fallback) => property && property.getValue ? property.getValue(time) : fallback
|
|
|
- const offset = read(entity.billboard.pixelOffset, ZERO_PIXEL_OFFSET)
|
|
|
- const scale = read(entity.billboard.scale, 1)
|
|
|
- const iconSize = Math.max(read(entity.billboard.width, 18), read(entity.billboard.height, 18)) * scale
|
|
|
- return { x: at.x + rect.left + offset.x, y: at.y + rect.top + offset.y, iconSize }
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 屏幕坐标 → **点位标记身份**(票 10 的地图点选)。
|
|
|
- *
|
|
|
- * 三件事必须说清:
|
|
|
- * 1. 只认点位标记数据源上的实体 —— 拾到地形、影像、3D Tiles 或空白一律回 `null`
|
|
|
- * (不是「拾到什么就打开详情」);判据是实体属性里有 `pointCode`;
|
|
|
- * 2. 坐标要吃**画布坐标**,而事件给的是窗口坐标:`scene.pick` 用的是画布内部坐标系,
|
|
|
- * 所以先减去画布原点(票 03 的 `projectPivot()` 踩过同一个坑,那里是加、这里是减);
|
|
|
- * 3. 回的是**标记身份**而不是 Cesium 实体:引擎对象不出这一层,上层只认 kind/code/status
|
|
|
- * (与 `syncPointMarkers` 只认 shape/tone 是同一条纪律)。
|
|
|
- *
|
|
|
- * @returns {{kind: string, code: string, status: string}|null}
|
|
|
- */
|
|
|
-export function pickPointMarkerAt(viewer, windowX, windowY) {
|
|
|
- if (!viewer || !viewer.scene || !viewer.canvas || typeof viewer.scene.pick !== 'function') return null
|
|
|
-
|
|
|
- const rect = viewer.canvas.getBoundingClientRect()
|
|
|
- const picked = pickThroughPipelines(viewer, new Cartesian2(windowX - rect.left, windowY - rect.top))
|
|
|
- const entity = picked && picked.id
|
|
|
- const properties = entity && entity.properties
|
|
|
- if (!properties || !properties.pointCode) return null
|
|
|
-
|
|
|
- const read = (key) => (properties[key] ? properties[key].getValue() : '')
|
|
|
- const code = read('pointCode')
|
|
|
- if (!code) return null
|
|
|
-
|
|
|
- return { kind: read('pointKind'), code, status: read('pointStatus'), name: read('pointName') || '' }
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 点位标记 hover 拾取(批次 3:给「hover 显示完整名称」的通用 tooltip 链路用)。
|
|
|
- * 与 `pickPointMarkerAt` 同一实体源,但**额外带 name** 且不参与点选竞态。
|
|
|
- */
|
|
|
-export function pickPointMarkerForHover(viewer, windowX, windowY) {
|
|
|
- return pickPointMarkerAt(viewer, windowX, windowY)
|
|
|
-}
|
|
|
-
|
|
|
/**
|
|
|
* 经纬度 → 画布窗口坐标(批次 3 第五轮,DOM overlay 的 3D 定位入口)。
|
|
|
* @returns {{x: number, y: number}|null} 相对画布左上角;未就绪/不可投影返回 null
|