feat: 优化播放控制器和 WebSocket 逻辑,移除不必要的内部状态,增强场景加载后机器人初始化流程

This commit is contained in:
xudan 2025-10-09 16:36:45 +08:00
parent e3f3ab5c2a
commit 3420c7cc9c
3 changed files with 41 additions and 81 deletions

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { CaretRightOutlined, PauseOutlined } from '@ant-design/icons-vue';
import { computed, ref, watch, onUnmounted } from 'vue';
import { computed, ref, watch } from 'vue';
import { useTimelineTicks } from '../hooks/useTimelineTicks';
@ -21,7 +21,6 @@ const SLIDER_PADDING = 8; // Ant Design Slider has 8px padding on each side
const HOUR_IN_MS = 3600 * 1000;
// --- Internal State ---
const internalCurrentTime = ref(props.currentTime);
const selectedHour = ref(Math.floor(props.currentTime / HOUR_IN_MS));
// --- Computed View Range ---
@ -36,9 +35,6 @@ watch(
() => props.currentTime,
(newValue) => {
// Only update from prop if not playing
if (!props.isPlaying) {
internalCurrentTime.value = newValue;
}
const newHour = Math.floor(newValue / HOUR_IN_MS);
if (newHour !== selectedHour.value) {
selectedHour.value = newHour;
@ -47,54 +43,6 @@ watch(
{ immediate: true },
);
// --- Animation Frame Timer for Playback ---
let animationFrameId: number | null = null;
let lastTimestamp: number | null = null;
const animationLoop = (timestamp: number) => {
if (!lastTimestamp) {
lastTimestamp = timestamp;
}
const deltaTime = timestamp - lastTimestamp;
lastTimestamp = timestamp;
// Ensure we don't jump too far ahead if the tab was inactive
const clampedDelta = Math.min(deltaTime, 1000); // Max 1-second jump
const newTime = internalCurrentTime.value + clampedDelta;
if (newTime < props.totalDuration) {
internalCurrentTime.value = newTime;
// No need to emit timeupdate as per user decision
// emit('timeupdate', newTime);
animationFrameId = requestAnimationFrame(animationLoop);
} else {
internalCurrentTime.value = props.totalDuration;
emit('pause'); // Auto-pause at the end
}
};
watch(
() => props.isPlaying,
(isPlaying) => {
if (isPlaying) {
lastTimestamp = null; // Reset timestamp on play
animationFrameId = requestAnimationFrame(animationLoop);
} else {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
}
},
);
onUnmounted(() => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
});
// --- Computed properties for display and binding ---
const formatTime = (ms: number): string => {
if (isNaN(ms) || ms < 0) return '00:00:00';
@ -105,20 +53,19 @@ const formatTime = (ms: number): string => {
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
};
const currentTimeFormatted = computed(() => formatTime(internalCurrentTime.value));
const currentTimeFormatted = computed(() => formatTime(props.currentTime));
const totalDurationFormatted = computed(() => formatTime(props.totalDuration));
const sliderValue = computed({
get: () => internalCurrentTime.value,
get: () => props.currentTime,
set: (val: number) => {
internalCurrentTime.value = val;
emit('seek', val);
},
});
const playheadPosition = computed(() => {
if (!props.totalDuration) return '0%';
const relativeTime = internalCurrentTime.value - viewStartTime.value;
const relativeTime = props.currentTime - viewStartTime.value;
const viewDuration = viewEndTime.value - viewStartTime.value;
return `${(relativeTime / viewDuration) * 100}%`;
});
@ -149,7 +96,6 @@ const handleTimelineClick = (event: MouseEvent) => {
const newTimeInView = percentage * HOUR_IN_MS;
const newAbsoluteTime = viewStartTime.value + newTimeInView;
internalCurrentTime.value = newAbsoluteTime;
emit('seek', newAbsoluteTime);
};

View File

@ -62,11 +62,10 @@ export function usePlaybackWebSocket(editorService: ShallowRef<EditorService | u
});
client.value = wsInstance;
wsInstance.onopen = () => {
isConnected.value = true;
message.success('回放连接已建立');
startRenderLoop();
};
// [关键修复] ws.create() 返回时连接已建立,直接执行连接成功逻辑
isConnected.value = true;
message.success('回放连接已建立');
startRenderLoop();
wsInstance.onmessage = (event) => {
const msg = JSON.parse(event.data) as PlaybackMessage;
@ -79,6 +78,7 @@ export function usePlaybackWebSocket(editorService: ShallowRef<EditorService | u
sceneJson.value = msg.data;
latestRobotData.clear();
} else if (msg.type === 'AMR') {
console.log('[Playback] Received AMR data:', msg.data);
(msg.data as RobotRealtimeInfo[]).forEach(robot => {
latestRobotData.set(robot.id, robot);
});
@ -138,11 +138,15 @@ export function usePlaybackWebSocket(editorService: ShallowRef<EditorService | u
};
const batchUpdateRobots = (updates: Array<{ id: string; data: RobotRealtimeInfo }>) => {
const editor = editorService.value;
console.log('[Playback] Batch update: Editor service is', editor ? 'available' : 'NOT available', '. Updates to process:', updates);
if (!editor || updates.length === 0) return;
updates.forEach(({ id, data }) => {
if (editor.checkRobotById(id)) {
const robotExists = editor.checkRobotById(id);
console.log(`[Playback] Checking robot ID: ${id}. Exists in editor: ${robotExists}`);
if (robotExists) {
const { x, y, angle, ...rest } = data;
editor.updateRobot(id, rest);
editor.setValue({
@ -157,22 +161,30 @@ export function usePlaybackWebSocket(editorService: ShallowRef<EditorService | u
};
const renderLoop = () => {
const updates: Array<{ id: string; data: RobotRealtimeInfo }> = [];
for (const [id, data] of latestRobotData.entries()) {
updates.push({ id, data });
try {
console.log('[Playback] Render loop executing...');
const updates: Array<{ id: string; data: RobotRealtimeInfo }> = [];
for (const [id, data] of latestRobotData.entries()) {
updates.push({ id, data });
}
latestRobotData.clear();
if (updates.length > 0) {
console.log(`[Playback] Render loop: Preparing to update ${updates.length} robots.`);
batchUpdateRobots(updates);
}
// if (isPlaying.value && currentTime.value >= totalDuration.value && totalDuration.value > 0) {
// pause();
// currentTime.value = totalDuration.value;
// }
editorService.value?.render();
} catch (error) {
console.error('[Playback] Error in renderLoop:', error);
stopRenderLoop(); // Stop the loop on error to prevent flooding the console
return; // Exit the function
}
latestRobotData.clear();
if (updates.length > 0) {
batchUpdateRobots(updates);
}
// if (isPlaying.value && currentTime.value >= totalDuration.value && totalDuration.value > 0) {
// pause();
// currentTime.value = totalDuration.value;
// }
editorService.value?.render();
animationFrameId = requestAnimationFrame(renderLoop);
};

View File

@ -97,9 +97,11 @@ const handleDateChange = (date: Dayjs) => {
}
};
watch(playback.sceneJson, (newJson) => {
watch(playback.sceneJson, async (newJson) => {
if (newJson) {
editor.value?.load(newJson);
await editor.value?.load(newJson);
// []
await editor.value?.initRobots();
}
});