feat: implement replay system with action dispatchers and context switching between main and playback modes

This commit is contained in:
taDuc
2026-05-15 19:39:02 +07:00
parent 3682f25282
commit 4c81862bb4
15 changed files with 595 additions and 59 deletions
+50 -4
View File
@@ -1,6 +1,6 @@
"use client";
import { type CSSProperties, useEffect, useRef } from "react";
import { type CSSProperties, useEffect, useRef, forwardRef, useImperativeHandle, useCallback } from "react";
import "maplibre-gl/dist/maplibre-gl.css";
import { Feature, FeatureCollection, Geometry } from "@/uhm/lib/editor/state/useEditorState";
@@ -19,6 +19,16 @@ export type MapHoverPayload = {
lngLat: { lng: number; lat: number };
};
export type MapHandle = {
getViewState: () => {
center: { lng: number; lat: number };
zoom: number;
pitch: number;
bearing: number;
projection: string;
} | null;
};
type MapProps = {
mode: EditorMode;
draft: FeatureCollection;
@@ -45,7 +55,7 @@ type MapProps = {
onToggleHideOutside?: () => void;
};
export default function Map({
const Map = forwardRef<MapHandle, MapProps>(function Map({
mode,
onSetMode,
draft,
@@ -69,7 +79,7 @@ export default function Map({
focusPadding,
hideOutside = false,
onToggleHideOutside,
}: MapProps) {
}, ref) {
const modeRef = useRef<MapProps["mode"]>(mode);
const draftRef = useRef<FeatureCollection>(draft);
const onSelectFeatureIdsRef = useRef(onSelectFeatureIds);
@@ -100,8 +110,21 @@ export default function Map({
geolocationCenteredRef,
handleZoomByStep,
handleZoomSliderChange,
getViewState,
} = useMapInstance();
useImperativeHandle(ref, () => ({
getViewState,
}), [getViewState]);
const handleLogViewState = useCallback(() => {
const state = getViewState();
console.log("Current Map View State:", state);
if (state) {
alert(`Captured View State:\nCenter: ${state.center.lng.toFixed(4)}, ${state.center.lat.toFixed(4)}\nZoom: ${state.zoom.toFixed(2)}\nPitch: ${state.pitch.toFixed(1)}°\nBearing: ${state.bearing.toFixed(1)}°\nProjection: ${state.projection}`);
}
}, [getViewState]);
const {
editingEngineRef,
setupMapInteractions,
@@ -251,6 +274,27 @@ export default function Map({
Thoát Replay Edit
</button>
<button
type="button"
onClick={handleLogViewState}
title="Capture current map view state"
style={{
...zoomButtonStyle,
width: "auto",
padding: "0 12px",
fontSize: "12px",
fontWeight: 700,
background: "#1e293b",
color: "#38bdf8",
border: "1px solid #334155",
borderRadius: "999px",
cursor: "pointer",
marginRight: "8px",
}}
>
Capture View
</button>
<div
onClick={onToggleHideOutside}
style={{
@@ -406,7 +450,9 @@ export default function Map({
</div>
</div>
);
}
});
export default Map;
const zoomButtonStyle: React.CSSProperties = {
width: "28px",