From dd74af093037e505752ffe4a301f62c8e48daa66 Mon Sep 17 00:00:00 2001 From: huangchenhao <123673748@qq.com> Date: Sat, 29 Nov 2025 19:17:05 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E5=8A=A8=E7=94=BB?= =?UTF-8?q?=E9=80=BB=E8=BE=91=20=E5=9C=A8=E5=8A=A8=E7=94=BB=E7=BB=93?= =?UTF-8?q?=E6=9D=9F=E7=9A=84=E6=97=B6=E5=80=99=20=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E6=8E=89=E6=96=B0=E5=A2=9E=E7=9A=84=E5=8A=A8=E7=94=BB=E5=AE=9E?= =?UTF-8?q?=E4=BD=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../composables/useEntityAnimation.js | 313 +++++++++++++----- .../composables/useSimulatedMarkers.js | 76 ++--- .../3DSituationalAwarenessRefactor/index.vue | 8 +- 3 files changed, 265 insertions(+), 132 deletions(-) diff --git a/packages/screen/src/views/3DSituationalAwarenessRefactor/composables/useEntityAnimation.js b/packages/screen/src/views/3DSituationalAwarenessRefactor/composables/useEntityAnimation.js index f91ed0d..b136ee9 100644 --- a/packages/screen/src/views/3DSituationalAwarenessRefactor/composables/useEntityAnimation.js +++ b/packages/screen/src/views/3DSituationalAwarenessRefactor/composables/useEntityAnimation.js @@ -146,10 +146,10 @@ export function useEntityAnimation() { const pulseScale = options.disablePulse ? 1.0 : new Cesium.CallbackProperty((time) => { - const elapsed = Cesium.JulianDate.secondsDifference(time, startTime) - // 使用正弦波产生脉冲效果,频率 3Hz,幅度 ±30% - return 1.0 + Math.sin(elapsed * 3) * 0.3 - }, false) + const elapsed = Cesium.JulianDate.secondsDifference(time, startTime) + // 使用正弦波产生脉冲效果,频率 3Hz,幅度 ±30% + return 1.0 + Math.sin(elapsed * 3) * 0.3 + }, false) // 创建动画实体 const entity = viewer.entities.add({ @@ -302,66 +302,70 @@ export function useEntityAnimation() { * @returns {Cesium.Entity} */ const createMovingEntity = (viewer, pathCoordinates, config, startTime, stopTime) => { - const positionProperty = new Cesium.SampledPositionProperty() - const numberOfPoints = pathCoordinates.length - const timeInterval = config.duration / (numberOfPoints - 1) + try { + const positionProperty = new Cesium.SampledPositionProperty() + const numberOfPoints = pathCoordinates.length - pathCoordinates.forEach((coord, index) => { - const time = Cesium.JulianDate.addSeconds( - startTime, - index * timeInterval, - new Cesium.JulianDate() - ) - const position = new Cesium.Cartesian3(coord.x, coord.y, coord.z) - positionProperty.addSample(time, position) - }) + if (numberOfPoints === 0) { + console.warn('[useEntityAnimation] 路径坐标为空') + return null + } - const pulseScale = config.disablePulse - ? 1.0 - : new Cesium.CallbackProperty((time) => { + const timeInterval = config.duration / (numberOfPoints - 1) + + pathCoordinates.forEach((coord, index) => { + const time = Cesium.JulianDate.addSeconds( + startTime, + index * timeInterval, + new Cesium.JulianDate() + ) + const position = new Cesium.Cartesian3(coord.x, coord.y, coord.z) + positionProperty.addSample(time, position) + }) + + const pulseScale = config.disablePulse + ? 1.0 + : new Cesium.CallbackProperty((time) => { const elapsed = Cesium.JulianDate.secondsDifference(time, startTime) return 1.0 + Math.sin(elapsed * 3) * 0.3 }, false) - const icon = config.type === 'device' ? deviceIcon : soldierIcon - const trailColor = config.type === 'device' ? Cesium.Color.ORANGE : Cesium.Color.CYAN + const icon = config.type === 'device' ? deviceIcon : soldierIcon - return viewer.entities.add({ - availability: new Cesium.TimeIntervalCollection([ - new Cesium.TimeInterval({ start: startTime, stop: stopTime }) - ]), - position: positionProperty, - orientation: new Cesium.VelocityOrientationProperty(positionProperty), - billboard: { - image: icon, - width: 48, - height: 56, - scale: pulseScale, - verticalOrigin: Cesium.VerticalOrigin.BOTTOM, - heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, - disableDepthTestDistance: Number.POSITIVE_INFINITY, - scaleByDistance: new Cesium.NearFarScalar(1000, 1.5, 50000, 0.8) - }, - // path: { - // resolution: 1, - // material: new Cesium.PolylineGlowMaterialProperty({ - // glowPower: 0.4, - // taperPower: 0.5, - // color: trailColor - // }), - // width: 8, - // leadTime: 0, - // trailTime: config.duration - // }, - properties: { - type: config.type === 'device' ? 'animatedDevice' : 'animatedSoldier', - name: config.name, - department: config.department, - isAnimating: true - } - }) + const entity = viewer.entities.add({ + availability: new Cesium.TimeIntervalCollection([ + new Cesium.TimeInterval({ start: startTime, stop: stopTime }) + ]), + position: positionProperty, + orientation: new Cesium.VelocityOrientationProperty(positionProperty), + billboard: { + image: icon, + width: 48, + height: 56, + scale: pulseScale, + verticalOrigin: Cesium.VerticalOrigin.BOTTOM, + heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, + disableDepthTestDistance: Number.POSITIVE_INFINITY, + scaleByDistance: new Cesium.NearFarScalar(1000, 1.5, 50000, 0.8) + }, + properties: { + type: config.type === 'device' ? 'animatedDevice' : 'animatedSoldier', + name: config.name, + department: config.department, + isAnimating: true + } + }) + + console.log(`[useEntityAnimation] 创建${config.type === 'device' ? '设备' : '人员'}实体: ${config.name}`) + return entity + + } catch (error) { + console.error('[useEntityAnimation] 创建移动实体失败:', error) + return null + } } + /** * 启动多组移动动画(设备组 + 人员组2) * @param {Cesium.Viewer} viewer @@ -373,6 +377,9 @@ export function useEntityAnimation() { return [] } + // 先停止所有现有动画 + stopAllMovements(viewer) + const config = { duration: options.duration ?? 60 } @@ -382,17 +389,15 @@ export function useEntityAnimation() { const startTime = Cesium.JulianDate.now() const stopTime = Cesium.JulianDate.addSeconds(startTime, config.duration, new Cesium.JulianDate()) + // 配置 viewer 时钟 viewer.clock.startTime = startTime.clone() viewer.clock.stopTime = stopTime.clone() viewer.clock.currentTime = startTime.clone() - viewer.clock.clockRange = Cesium.ClockRange.CLAMPED // 动画到达终点后停止,不循环 + viewer.clock.clockRange = Cesium.ClockRange.CLAMPED viewer.clock.multiplier = 1 viewer.clock.shouldAnimate = true - // 清除之前的动画实体 - animatedEntities.value.forEach(entity => { - if (entity) viewer.entities.remove(entity) - }) + // 确保实体数组是空的 animatedEntities.value = [] // 创建设备组移动实体 @@ -400,31 +405,47 @@ export function useEntityAnimation() { duration: config.duration, type: 'device', name: '应急设备车', - department: '应急装备队' + department: '应急装备队', + disablePulse: options.disablePulse }, startTime, stopTime) - animatedEntities.value.push(deviceEntity) + + if (deviceEntity) { + animatedEntities.value.push(deviceEntity) + } // 创建人员组2移动实体 const personnel2Entity = createMovingEntity(viewer, PERSONNEL_PATH_COORDINATES_2, { duration: config.duration, type: 'soldier', name: '应急救援队员', - department: '应急救援队' + department: '应急救援队', + disablePulse: options.disablePulse }, startTime, stopTime) - animatedEntities.value.push(personnel2Entity) + + if (personnel2Entity) { + animatedEntities.value.push(personnel2Entity) + } isAnimating.value = true + // 设置超时清理 + const timeoutId = setTimeout(() => { + console.log('[useEntityAnimation] 多组动画超时,强制清理') + stopAllMovements(viewer) + }, (config.duration + 5) * 1000) // 动画时长+5秒缓冲 + const removeListener = viewer.clock.onStop.addEventListener(() => { console.log('[useEntityAnimation] 多组动画已结束') + clearTimeout(timeoutId) isAnimating.value = false removeListener() }) - console.log('[useEntityAnimation] 已启动 2 组额外移动动画') + console.log('[useEntityAnimation] 已启动 2 组移动动画') return animatedEntities.value } + /** * 停止所有移动动画 * @param {Cesium.Viewer} viewer @@ -432,25 +453,64 @@ export function useEntityAnimation() { const stopAllMovements = (viewer) => { if (!viewer) return + console.log('[useEntityAnimation] 停止所有移动动画') + + // 停止时钟动画 viewer.clock.shouldAnimate = false + // 取消相机跟随 + if (viewer.trackedEntity) { + viewer.trackedEntity = undefined + } + // 停止所有脉冲效果 stopAllPulses(viewer) + // 移除单个动画实体 if (animatedEntity.value) { - viewer.entities.remove(animatedEntity.value) + try { + viewer.entities.remove(animatedEntity.value) + } catch (e) { + console.warn('[useEntityAnimation] 移除单个实体时出错:', e) + } animatedEntity.value = null } + // 移除所有动画实体 animatedEntities.value.forEach(entity => { - if (entity) viewer.entities.remove(entity) + if (entity) { + try { + viewer.entities.remove(entity) + } catch (e) { + console.warn('[useEntityAnimation] 移除实体时出错:', e) + } + } }) animatedEntities.value = [] + // 额外清理:移除所有类型为 animatedSoldier 和 animatedDevice 的实体 + const entitiesToRemove = [] + viewer.entities.values.forEach(entity => { + const entityType = entity.properties?.type?.getValue?.() + if (entityType === 'animatedSoldier' || entityType === 'animatedDevice') { + entitiesToRemove.push(entity) + } + }) + + entitiesToRemove.forEach(entity => { + try { + viewer.entities.remove(entity) + console.log('[useEntityAnimation] 清理残留实体:', entity.properties?.name?.getValue?.() || '未知') + } catch (e) { + console.warn('[useEntityAnimation] 清理残留实体时出错:', e) + } + }) + isAnimating.value = false - console.log('[useEntityAnimation] 已停止所有移动动画') + console.log('[useEntityAnimation] 已停止所有移动动画,并清理残留实体') } + /** * 根据动态路线创建动画实体 * @param {Cesium.Viewer} viewer @@ -508,13 +568,13 @@ export function useEntityAnimation() { const pulseScale = options.disablePulse ? 1.0 : new Cesium.CallbackProperty((time) => { - if (!scaleState.isPulsing) { - return scaleState.fixedScale // 停止脉冲,返回固定值 - } + if (!scaleState.isPulsing) { + return scaleState.fixedScale // 停止脉冲,返回固定值 + } - const elapsed = Cesium.JulianDate.secondsDifference(time, startTime) - return 1.0 + Math.sin(elapsed * 3) * 0.3 - }, false) + const elapsed = Cesium.JulianDate.secondsDifference(time, startTime) + return 1.0 + Math.sin(elapsed * 3) * 0.3 + }, false) // 选择图标和轨迹颜色 const icon = route.type === 'equipment' ? deviceIcon : soldierIcon @@ -633,15 +693,26 @@ export function useEntityAnimation() { // 监听时钟,检测动画完成 const clockListener = viewer.clock.onTick.addEventListener((clock) => { - checkAnimationsCompletion(viewer, clock.currentTime, onEntityComplete) + checkAnimationsCompletion(viewer, clock.currentTime, (routeId, entity) => { + // 调用原有的路线清除回调 + if (options.onEntityComplete) { + options.onEntityComplete(routeId) + } - // 注释掉自动停止时钟的逻辑,让时钟继续运行 - // 原因:时钟停止会导致 Cesium 停止更新实体位置,导致前面完成的实体消失 - // if (areAllAnimationsCompleted()) { - // console.log('[useEntityAnimation] 所有动画已完成,停止时钟') - // viewer.clock.shouldAnimate = false - // clockListener() // 移除监听器 - // } + // 延迟移除实体 + setTimeout(() => { + if (viewer && !viewer.isDestroyed() && entity) { + viewer.entities.remove(entity) + console.log(`[useEntityAnimation] 动画完成,已移除实体`) + + // 从动画实体列表中移除 + const index = animatedEntities.value.indexOf(entity) + if (index > -1) { + animatedEntities.value.splice(index, 1) + } + } + }, 1000) // 延迟1秒移除 + }) }) // 监听动画结束 @@ -652,17 +723,69 @@ export function useEntityAnimation() { clockListener() // 移除 tick 监听器 }) + // 新增:60秒超时强制清除所有实体 + const timeoutId = setTimeout(() => { + console.log('[useEntityAnimation] 60秒超时,强制清除所有动画实体') + forceCleanupAllAnimations(viewer, options.onEntityComplete) + }, 60000) // 60秒超时 + console.log(`[useEntityAnimation] 成功启动 ${animatedEntities.value.length} 个动画实体`) return animatedEntities.value } /** - * 检查动画是否完成,如果完成则停止脉冲并清除路线 - * @param {Cesium.Viewer} viewer - * @param {Cesium.JulianDate} currentTime - * @param {Function} onEntityComplete - 完成回调 (routeId) => void - */ + * 强制清理所有动画实体和路线 + * @param {Cesium.Viewer} viewer + * @param {Function} onEntityComplete - 路线清除回调 + */ + const forceCleanupAllAnimations = (viewer, onEntityComplete) => { + if (!viewer) return + + // 停止时钟 + viewer.clock.shouldAnimate = false + + // 停止所有脉冲 + stopAllPulses(viewer) + + // 移除所有实体并触发回调 + animatedEntities.value.forEach(entity => { + if (!entity || !entity.properties) return + + const routeId = entity.properties.routeId?.getValue() + + // 触发路线清除回调 + if (onEntityComplete && typeof onEntityComplete === 'function' && routeId) { + console.log(`[useEntityAnimation] 超时清除路线: ${routeId}`) + onEntityComplete(routeId) + } + + // 移除实体 + if (viewer && !viewer.isDestroyed()) { + viewer.entities.remove(entity) + } + }) + + // 清空实体列表 + animatedEntities.value = [] + + // 清除单个动画实体 + if (animatedEntity.value) { + viewer.entities.remove(animatedEntity.value) + animatedEntity.value = null + } + + isAnimating.value = false + console.log('[useEntityAnimation] 强制清理完成') + } + + + /** + * 检查动画是否完成,如果完成则停止脉冲、清除路线并移除实体 + * @param {Cesium.Viewer} viewer + * @param {Cesium.JulianDate} currentTime + * @param {Function} onEntityComplete - 完成回调 (routeId, entity) => void + */ const checkAnimationsCompletion = (viewer, currentTime, onEntityComplete) => { animatedEntities.value.forEach(entity => { if (!entity || !entity.properties) return @@ -698,17 +821,25 @@ export function useEntityAnimation() { // 2. 标记为已完成(使用 ConstantProperty) entity.properties.isCompleted = new Cesium.ConstantProperty(true) - // 3. 触发回调清除对应路线 + // 3. 触发回调清除对应路线,并传递实体信息以便移除 if (onEntityComplete && typeof onEntityComplete === 'function') { console.log(`[useEntityAnimation] 触发 onEntityComplete 回调,routeId: ${routeId}`) - onEntityComplete(routeId) + onEntityComplete(routeId, entity) // 传递实体信息 } else { console.warn(`[useEntityAnimation] onEntityComplete 回调不存在或不是函数`) + // 如果没有回调,直接移除实体 + setTimeout(() => { + if (viewer && !viewer.isDestroyed()) { + viewer.entities.remove(entity) + console.log(`[useEntityAnimation] 已自动移除实体: ${entityName}`) + } + }, 1000) // 延迟1秒移除,确保动画完全结束 } } }) } + /** * 停止所有实体的脉冲效果 * @param {Cesium.Viewer} viewer @@ -762,7 +893,9 @@ export function useEntityAnimation() { startMultipleAnimationsWithRoutes, // 新增:脉冲控制方法 checkAnimationsCompletion, - stopAllPulses + stopAllPulses, + // 新增:强制清理方法 + forceCleanupAllAnimations } } diff --git a/packages/screen/src/views/3DSituationalAwarenessRefactor/composables/useSimulatedMarkers.js b/packages/screen/src/views/3DSituationalAwarenessRefactor/composables/useSimulatedMarkers.js index 082a2fd..3f585a6 100644 --- a/packages/screen/src/views/3DSituationalAwarenessRefactor/composables/useSimulatedMarkers.js +++ b/packages/screen/src/views/3DSituationalAwarenessRefactor/composables/useSimulatedMarkers.js @@ -32,45 +32,45 @@ export function useSimulatedMarkers() { const newMarkers = [] // 1. Create 2 center markers (static, no animation) - const centerPersonnelCoords = applyRandomOffset(disasterCenter, 10, 30) - const centerPersonnel = viewer.entities.add({ - position: Cesium.Cartesian3.fromDegrees(centerPersonnelCoords.lon, centerPersonnelCoords.lat, 0), - billboard: { - image: soldierIcon, - width: 36, - height: 40, - verticalOrigin: Cesium.VerticalOrigin.BOTTOM, - heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, - disableDepthTestDistance: Number.POSITIVE_INFINITY - }, - properties: new Cesium.PropertyBag({ - type: 'centerPersonnel', - name: '应急人员', - isSimulated: true, - isStatic: true - }) - }) - newMarkers.push(centerPersonnel) + // const centerPersonnelCoords = applyRandomOffset(disasterCenter, 10, 30) + // const centerPersonnel = viewer.entities.add({ + // position: Cesium.Cartesian3.fromDegrees(centerPersonnelCoords.lon, centerPersonnelCoords.lat, 0), + // billboard: { + // image: soldierIcon, + // width: 36, + // height: 40, + // verticalOrigin: Cesium.VerticalOrigin.BOTTOM, + // heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, + // disableDepthTestDistance: Number.POSITIVE_INFINITY + // }, + // properties: new Cesium.PropertyBag({ + // type: 'centerPersonnel', + // name: '应急人员', + // isSimulated: true, + // isStatic: true + // }) + // }) + // newMarkers.push(centerPersonnel) - const centerEquipmentCoords = applyRandomOffset(disasterCenter, 10, 30) - const centerEquipment = viewer.entities.add({ - position: Cesium.Cartesian3.fromDegrees(centerEquipmentCoords.lon, centerEquipmentCoords.lat, 0), - billboard: { - image: deviceIcon, - width: 36, - height: 40, - verticalOrigin: Cesium.VerticalOrigin.BOTTOM, - heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, - disableDepthTestDistance: Number.POSITIVE_INFINITY - }, - properties: new Cesium.PropertyBag({ - type: 'centerEquipment', - name: '应急装备', - isSimulated: true, - isStatic: true - }) - }) - newMarkers.push(centerEquipment) + // const centerEquipmentCoords = applyRandomOffset(disasterCenter, 10, 30) + // const centerEquipment = viewer.entities.add({ + // position: Cesium.Cartesian3.fromDegrees(centerEquipmentCoords.lon, centerEquipmentCoords.lat, 0), + // billboard: { + // image: deviceIcon, + // width: 36, + // height: 40, + // verticalOrigin: Cesium.VerticalOrigin.BOTTOM, + // heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, + // disableDepthTestDistance: Number.POSITIVE_INFINITY + // }, + // properties: new Cesium.PropertyBag({ + // type: 'centerEquipment', + // name: '应急装备', + // isSimulated: true, + // isStatic: true + // }) + // }) + // newMarkers.push(centerEquipment) // 2. Create 4 emergency point markers (will be animated later) emergencyPoints.slice(0, 2).forEach((point, index) => { diff --git a/packages/screen/src/views/3DSituationalAwarenessRefactor/index.vue b/packages/screen/src/views/3DSituationalAwarenessRefactor/index.vue index 2c6fc22..36221ee 100644 --- a/packages/screen/src/views/3DSituationalAwarenessRefactor/index.vue +++ b/packages/screen/src/views/3DSituationalAwarenessRefactor/index.vue @@ -41,7 +41,7 @@ v-if="isCompareMode" class="situational-awareness__sync-btn" @click="toggleCameraSync" - :text="isCameraSyncEnabled ? '同步' : '不同步'" + :text="isCameraSyncEnabled ? '同步灾前灾后实景' : '未同步灾前灾后实景'" > @@ -1824,10 +1824,10 @@ provide("triggerJump", (duration = 5, height = 30) => { .situational-awareness__sync-btn { position: absolute; - left: 40%; - top: 133px; + left: 32%; + top: calc(var(--sa-header-height)); height: 31px; - transform: translateX(calc(-100% - vw(10))); + pointer-events: auto; // 恢复按钮的交互能力 } }