refactor(3d-situational-awareness): 优化路径动画和可视化效果

*   禁用路径轨迹可视化,以实现更简洁的实体移动
*   将路径颜色更改为红色,以提高可见度
*   将动画速度倍率从 10 提高到 40
*   添加 `onEntityComplete` 回调函数,以便在动画结束时清除路径
*   改进位置属性外推算法和实体完成度跟踪功能
*   增强 `clearRoute` 函数,以优化属性值处理和日志记录
This commit is contained in:
Zzc 2025-11-28 00:29:40 +08:00
parent 0dca401b1c
commit fa93f4e2ce
4 changed files with 146 additions and 74 deletions

View File

@ -42,7 +42,7 @@ export function useEmergencyDispatch({
// 初始化路由相关 composables // 初始化路由相关 composables
const { calculateRoutes, isCalculating } = useAmapRouting() const { calculateRoutes, isCalculating } = useAmapRouting()
const { selectByType } = useEmergencyRouteSelection() const { selectByType } = useEmergencyRouteSelection()
const { drawMultipleRoutes, clearRoutes, getRoutesBounds } = useRouteVisualization() const { drawMultipleRoutes, clearRoutes, clearRoute, getRoutesBounds } = useRouteVisualization()
// Loading 状态 // Loading 状态
const showLoading = ref(false) const showLoading = ref(false)
@ -164,8 +164,13 @@ export function useEmergencyDispatch({
hideEmergencyPointMarkers(viewer) hideEmergencyPointMarkers(viewer)
startMultipleAnimationsWithRoutes(viewer, routes, { startMultipleAnimationsWithRoutes(viewer, routes, {
speedMultiplier: 10, speedMultiplier: 40,
disablePulse: true // 禁用脉冲效果 disablePulse: true, // 禁用脉冲效果
// 新增:实体到达时清除对应路线的回调
onEntityComplete: (routeId) => {
console.log(`[useEmergencyDispatch] 实体到达目标,清除路线: ${routeId}`)
clearRoute(viewer, routeId)
}
}) })
// 7. 相机飞向全景位置 // 7. 相机飞向全景位置

View File

@ -176,17 +176,17 @@ export function useEntityAnimation() {
) )
}, },
// 添加发光轨迹线 // 添加发光轨迹线
path: { // path: {
resolution: 1, // resolution: 1,
material: new Cesium.PolylineGlowMaterialProperty({ // material: new Cesium.PolylineGlowMaterialProperty({
glowPower: 0.4, // 发光强度 // glowPower: 0.4, // 发光强度
taperPower: 0.5, // 渐变效果 // taperPower: 0.5, // 渐变效果
color: Cesium.Color.CYAN // 青色轨迹 // color: Cesium.Color.CYAN // 青色轨迹
}), // }),
width: 8, // width: 8,
leadTime: 0, // 前方不显示轨迹 // leadTime: 0, // 前方不显示轨迹
trailTime: config.duration // 后方显示完整轨迹 // trailTime: config.duration // 后方显示完整轨迹
}, // },
properties: { properties: {
type: 'animatedSoldier', type: 'animatedSoldier',
name: config.personnelName, name: config.personnelName,
@ -342,17 +342,17 @@ export function useEntityAnimation() {
disableDepthTestDistance: Number.POSITIVE_INFINITY, disableDepthTestDistance: Number.POSITIVE_INFINITY,
scaleByDistance: new Cesium.NearFarScalar(1000, 1.5, 50000, 0.8) scaleByDistance: new Cesium.NearFarScalar(1000, 1.5, 50000, 0.8)
}, },
path: { // path: {
resolution: 1, // resolution: 1,
material: new Cesium.PolylineGlowMaterialProperty({ // material: new Cesium.PolylineGlowMaterialProperty({
glowPower: 0.4, // glowPower: 0.4,
taperPower: 0.5, // taperPower: 0.5,
color: trailColor // color: trailColor
}), // }),
width: 8, // width: 8,
leadTime: 0, // leadTime: 0,
trailTime: config.duration // trailTime: config.duration
}, // },
properties: { properties: {
type: config.type === 'device' ? 'animatedDevice' : 'animatedSoldier', type: config.type === 'device' ? 'animatedDevice' : 'animatedSoldier',
name: config.name, name: config.name,
@ -483,6 +483,11 @@ export function useEntityAnimation() {
// 创建位置属性 // 创建位置属性
const positionProperty = new Cesium.SampledPositionProperty() const positionProperty = new Cesium.SampledPositionProperty()
// 关键修复:设置外推类型,确保在采样范围外保持最后位置
positionProperty.forwardExtrapolationType = Cesium.ExtrapolationType.HOLD
positionProperty.backwardExtrapolationType = Cesium.ExtrapolationType.HOLD
const timeInterval = animationDuration / (positions.length - 1) const timeInterval = animationDuration / (positions.length - 1)
positions.forEach((position, index) => { positions.forEach((position, index) => {
@ -525,9 +530,6 @@ export function useEntityAnimation() {
// 创建动画实体 // 创建动画实体
return viewer.entities.add({ return viewer.entities.add({
availability: new Cesium.TimeIntervalCollection([
new Cesium.TimeInterval({ start: startTime, stop: stopTime })
]),
position: positionProperty, position: positionProperty,
orientation: new Cesium.VelocityOrientationProperty(positionProperty), orientation: new Cesium.VelocityOrientationProperty(positionProperty),
billboard: { billboard: {
@ -540,22 +542,23 @@ export function useEntityAnimation() {
disableDepthTestDistance: Number.POSITIVE_INFINITY, disableDepthTestDistance: Number.POSITIVE_INFINITY,
scaleByDistance: new Cesium.NearFarScalar(1000, 1.5, 50000, 0.8) scaleByDistance: new Cesium.NearFarScalar(1000, 1.5, 50000, 0.8)
}, },
path: { // path: {
resolution: 1, // resolution: 1,
material: new Cesium.PolylineGlowMaterialProperty({ // material: new Cesium.PolylineGlowMaterialProperty({
glowPower: 0.4, // glowPower: 0.4,
taperPower: 0.5, // taperPower: 0.5,
color: trailColor // color: trailColor
}), // }),
width: 8, // width: 8,
leadTime: 0, // leadTime: 0,
trailTime: animationDuration // trailTime: animationDuration
}, // },
properties: { properties: {
type: route.type === 'equipment' ? 'animatedDevice' : 'animatedSoldier', type: route.type === 'equipment' ? 'animatedDevice' : 'animatedSoldier',
name: route.name, name: route.name,
routeId: route.id, routeId: route.id,
isAnimating: true, isAnimating: true,
isCompleted: false, // 新增:标记动画是否完成
scaleState: scaleState, // 新增:保存状态引用 scaleState: scaleState, // 新增:保存状态引用
animationDuration: animationDuration, // 新增:保存动画时长 animationDuration: animationDuration, // 新增:保存动画时长
animationStartTime: startTime.clone() // 新增:保存开始时间 animationStartTime: startTime.clone() // 新增:保存开始时间
@ -576,10 +579,13 @@ export function useEntityAnimation() {
return [] return []
} }
console.log(`[useEntityAnimation] 启动 ${routes.length} 条路线的动画 (disablePulse: ${options.disablePulse || false})`) // 解构回调参数
const { speedMultiplier: baseSpeedMultiplier, disablePulse, onEntityComplete } = options
console.log(`[useEntityAnimation] 启动 ${routes.length} 条路线的动画 (disablePulse: ${disablePulse || false})`)
// 计算最大动画时长 // 计算最大动画时长
const speedMultiplier = options.speedMultiplier || 10 const speedMultiplier = baseSpeedMultiplier || 10
const durations = routes.map(r => const durations = routes.map(r =>
Math.min(Math.max(r.duration / speedMultiplier, 30), 120) Math.min(Math.max(r.duration / speedMultiplier, 30), 120)
) )
@ -597,7 +603,7 @@ export function useEntityAnimation() {
viewer.clock.startTime = startTime.clone() viewer.clock.startTime = startTime.clone()
viewer.clock.stopTime = stopTime.clone() viewer.clock.stopTime = stopTime.clone()
viewer.clock.currentTime = startTime.clone() viewer.clock.currentTime = startTime.clone()
viewer.clock.clockRange = Cesium.ClockRange.CLAMPED // 动画到达终点后停止,不循环 viewer.clock.clockRange = Cesium.ClockRange.UNBOUNDED // 允许时间继续前进,支持不同时长动画独立完成
viewer.clock.multiplier = 1 viewer.clock.multiplier = 1
viewer.clock.shouldAnimate = true viewer.clock.shouldAnimate = true
@ -627,7 +633,15 @@ export function useEntityAnimation() {
// 监听时钟,检测动画完成 // 监听时钟,检测动画完成
const clockListener = viewer.clock.onTick.addEventListener((clock) => { const clockListener = viewer.clock.onTick.addEventListener((clock) => {
checkAnimationsCompletion(viewer, clock.currentTime) checkAnimationsCompletion(viewer, clock.currentTime, onEntityComplete)
// 注释掉自动停止时钟的逻辑,让时钟继续运行
// 原因:时钟停止会导致 Cesium 停止更新实体位置,导致前面完成的实体消失
// if (areAllAnimationsCompleted()) {
// console.log('[useEntityAnimation] 所有动画已完成,停止时钟')
// viewer.clock.shouldAnimate = false
// clockListener() // 移除监听器
// }
}) })
// 监听动画结束 // 监听动画结束
@ -644,31 +658,53 @@ export function useEntityAnimation() {
} }
/** /**
* 检查动画是否完成如果完成则停止脉冲 * 检查动画是否完成如果完成则停止脉冲并清除路线
* @param {Cesium.Viewer} viewer * @param {Cesium.Viewer} viewer
* @param {Cesium.JulianDate} currentTime * @param {Cesium.JulianDate} currentTime
* @param {Function} onEntityComplete - 完成回调 (routeId) => void
*/ */
const checkAnimationsCompletion = (viewer, currentTime) => { const checkAnimationsCompletion = (viewer, currentTime, onEntityComplete) => {
animatedEntities.value.forEach(entity => { animatedEntities.value.forEach(entity => {
if (!entity || !entity.properties) return if (!entity || !entity.properties) return
const scaleState = entity.properties.scaleState?.getValue() const scaleState = entity.properties.scaleState?.getValue()
const startTime = entity.properties.animationStartTime?.getValue() const startTime = entity.properties.animationStartTime?.getValue()
const duration = entity.properties.animationDuration?.getValue() const duration = entity.properties.animationDuration?.getValue()
const isCompleted = entity.properties.isCompleted?.getValue()
const routeId = entity.properties.routeId?.getValue()
if (!scaleState || !startTime || !duration) return if (!scaleState || !startTime || !duration || !routeId) return
// 如果已经停止脉冲,跳过 // 如果已经完成,跳过(避免重复触发)
if (!scaleState.isPulsing) return if (isCompleted) return
// 计算已过时间 // 计算已过时间
const elapsed = Cesium.JulianDate.secondsDifference(currentTime, startTime) const elapsed = Cesium.JulianDate.secondsDifference(currentTime, startTime)
// 如果动画已完成留0.5秒容差) // 如果动画已完成留0.5秒容差)
if (elapsed >= duration - 0.5) { if (elapsed >= duration - 0.5) {
console.log(`[useEntityAnimation] 实体 ${entity.properties.name.getValue()} 动画完成,停止脉冲`) const entityName = entity.properties.name?.getValue() || '未知实体'
console.log(`[useEntityAnimation] ========================================`)
console.log(`[useEntityAnimation] 实体 "${entityName}" 动画完成`)
console.log(`[useEntityAnimation] - routeId: ${routeId}`)
console.log(`[useEntityAnimation] - elapsed: ${elapsed.toFixed(2)}`)
console.log(`[useEntityAnimation] - duration: ${duration}`)
console.log(`[useEntityAnimation] ========================================`)
// 1. 停止脉冲效果
scaleState.isPulsing = false scaleState.isPulsing = false
scaleState.fixedScale = 1.0 // 设置为正常大小 scaleState.fixedScale = 1.0
// 2. 标记为已完成(使用 ConstantProperty
entity.properties.isCompleted = new Cesium.ConstantProperty(true)
// 3. 触发回调清除对应路线
if (onEntityComplete && typeof onEntityComplete === 'function') {
console.log(`[useEntityAnimation] 触发 onEntityComplete 回调routeId: ${routeId}`)
onEntityComplete(routeId)
} else {
console.warn(`[useEntityAnimation] onEntityComplete 回调不存在或不是函数`)
}
} }
}) })
} }
@ -693,6 +729,19 @@ export function useEntityAnimation() {
console.log('[useEntityAnimation] 已停止所有脉冲效果') console.log('[useEntityAnimation] 已停止所有脉冲效果')
} }
/**
* 检查所有动画是否都已完成
* @returns {boolean}
*/
const areAllAnimationsCompleted = () => {
if (animatedEntities.value.length === 0) return false
return animatedEntities.value.every(entity => {
const isCompleted = entity.properties?.isCompleted?.getValue()
return isCompleted === true
})
}
return { return {
animatedEntity, animatedEntity,
animatedEntities, animatedEntities,

View File

@ -65,7 +65,7 @@ export function useRouteVisualization() {
polyline: { polyline: {
positions: positions, positions: positions,
width: styleOptions.width || style.width, width: styleOptions.width || style.width,
material: styleOptions.material || style.material(viewer), material: styleOptions.material || (typeof style.material === 'function' ? style.material(viewer) : style.material),
clampToGround: true, clampToGround: true,
classificationType: Cesium.ClassificationType.TERRAIN classificationType: Cesium.ClassificationType.TERRAIN
}, },
@ -82,7 +82,7 @@ export function useRouteVisualization() {
routeEntities.value.push(entity) routeEntities.value.push(entity)
console.log(`[useRouteVisualization] 绘制路线: ${routeData.name} (${type})`) console.log(`[useRouteVisualization] 绘制路线: ${routeData.name} (${type}), routeId: ${routeData.id}`)
return entity return entity
} }
@ -224,27 +224,52 @@ export function useRouteVisualization() {
* @param {string} routeId - 路线 ID * @param {string} routeId - 路线 ID
*/ */
const clearRoute = (viewer, routeId) => { const clearRoute = (viewer, routeId) => {
if (!viewer || !routeId) return if (!viewer || !routeId) {
console.warn(`[useRouteVisualization] clearRoute 参数无效: viewer=${!!viewer}, routeId=${routeId}`)
return
}
console.log(`[useRouteVisualization] 开始清除路线: ${routeId}`)
console.log(`[useRouteVisualization] 当前路线实体数量: ${routeEntities.value.length}`)
console.log(`[useRouteVisualization] 当前标签实体数量: ${labelEntities.value.length}`)
let removedRoutes = 0
let removedLabels = 0
// 清除路线实体 // 清除路线实体
routeEntities.value = routeEntities.value.filter(entity => { routeEntities.value = routeEntities.value.filter(entity => {
if (entity && entity.properties && entity.properties.routeId === routeId) { if (entity && entity.properties) {
viewer.entities.remove(entity) // 关键修复:使用 getValue() 获取 Cesium Property 的实际值
return false const entityRouteId = entity.properties.routeId?.getValue ? entity.properties.routeId.getValue() : entity.properties.routeId
console.log(`[useRouteVisualization] 检查路线实体 routeId: "${entityRouteId}", 目标: "${routeId}", 匹配: ${entityRouteId === routeId}`)
if (entityRouteId === routeId) {
console.log(`[useRouteVisualization] ✅ 匹配成功,移除路线实体: ${routeId}`)
viewer.entities.remove(entity)
removedRoutes++
return false
}
} }
return true return true
}) })
// 清除标签实体 // 清除标签实体
labelEntities.value = labelEntities.value.filter(entity => { labelEntities.value = labelEntities.value.filter(entity => {
if (entity && entity.properties && entity.properties.routeId === routeId) { if (entity && entity.properties) {
viewer.entities.remove(entity) // 关键修复:使用 getValue() 获取 Cesium Property 的实际值
return false const entityRouteId = entity.properties.routeId?.getValue ? entity.properties.routeId.getValue() : entity.properties.routeId
if (entityRouteId === routeId) {
console.log(`[useRouteVisualization] 移除标签实体: ${routeId}`)
viewer.entities.remove(entity)
removedLabels++
return false
}
} }
return true return true
}) })
console.log(`[useRouteVisualization] 已清除路线: ${routeId}`) console.log(`[useRouteVisualization] 已清除路线: ${routeId}, 移除了 ${removedRoutes} 条路线和 ${removedLabels} 个标签`)
} }
/** /**

View File

@ -11,25 +11,18 @@ import * as Cesium from 'cesium'
export const ROUTE_STYLES = { export const ROUTE_STYLES = {
// 人员路线样式 // 人员路线样式
personnel: { personnel: {
color: Cesium.Color.CYAN, color: Cesium.Color.RED,
width: 5, width: 5,
material: (viewer) => new Cesium.PolylineGlowMaterialProperty({ material: Cesium.Color.RED,
glowPower: 0.3, labelColor: Cesium.Color.RED
color: Cesium.Color.CYAN
}),
labelColor: Cesium.Color.CYAN
}, },
// 装备路线样式 // 装备路线样式
equipment: { equipment: {
color: Cesium.Color.ORANGE, color: Cesium.Color.RED,
width: 6, width: 6,
material: (viewer) => new Cesium.PolylineDashMaterialProperty({ material: Cesium.Color.RED,
color: Cesium.Color.ORANGE, labelColor: Cesium.Color.RED
dashLength: 20.0,
dashPattern: 255
}),
labelColor: Cesium.Color.ORANGE
}, },
// 降级直线样式API 失败时使用) // 降级直线样式API 失败时使用)
@ -65,7 +58,7 @@ export const LABEL_STYLE = {
*/ */
export const ROUTE_ANIMATION_CONFIG = { export const ROUTE_ANIMATION_CONFIG = {
// 动画速度倍数(相对于真实时间) // 动画速度倍数(相对于真实时间)
speedMultiplier: 10, speedMultiplier: 40,
// 最小动画时长(秒) // 最小动画时长(秒)
minDuration: 30, minDuration: 30,