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
+36 -26
View File
@@ -188,12 +188,16 @@ export default function Page() {
}, [snapshotEntityWikiLinks]);
const editor = useEditorState(initialData, {
snapshotEntitiesRef,
setSnapshotEntities,
snapshotWikisRef,
setSnapshotWikis,
snapshotEntityWikiLinksRef,
setSnapshotEntityWikiLinks,
snapshotUndo: {
snapshotEntitiesRef,
setSnapshotEntities,
snapshotWikisRef,
setSnapshotWikis,
snapshotEntityWikiLinksRef,
setSnapshotEntityWikiLinks,
},
initialReplays: baselineSnapshot?.replays,
mode: mode,
});
const setSnapshotWikisUndoable = useCallback(
(next: SetStateAction<WikiSnapshot[]>) => {
@@ -266,16 +270,19 @@ export default function Page() {
// Timeline filter: only affects persisted snapshot features.
// New features created in the current session remain visible regardless of time range.
const timelineVisibleDraft = useMemo(() => {
if (!timelineFilterEnabled) return editor.draft;
// Nếu ở mode replay, sử dụng replayDraft thay vì main draft
const activeDraft = mode === "replay" ? editor.replayDraft : editor.mainDraft;
if (!timelineFilterEnabled) return activeDraft;
const year = clampYearToFixedRange(Math.trunc(timelineDraftYear));
return {
...editor.draft,
features: editor.draft.features.filter((feature) => {
...activeDraft,
features: activeDraft.features.filter((feature) => {
if (!editor.hasPersistedFeature(feature.properties.id)) return true;
return isFeatureVisibleAtYear(feature, year);
}),
};
}, [editor, timelineDraftYear, timelineFilterEnabled]);
}, [editor, mode, timelineDraftYear, timelineFilterEnabled]);
const projectEntityChoices = useMemo(() => {
const ids = new Set<string>();
@@ -412,36 +419,38 @@ export default function Page() {
const setMode = useCallback((m: EditorMode, featureId?: string | number) => {
if (m === "replay" && featureId) {
setReplayFeatureId(featureId);
// QUY TẮC: Geo chọn đầu tiên là geo main.
const triggerId = selectedFeatureIds.length > 0 ? selectedFeatureIds[0] : featureId;
setReplayFeatureId(triggerId);
editor.switchReplayContext(triggerId, selectedFeatureIds);
} else if (m !== "replay") {
if (mode === "replay") {
editor.closeReplayContext();
setSelectedFeatureIds([]);
}
setReplayFeatureId(null);
setHideOutside(false);
}
internalSetMode(m);
}, [internalSetMode]);
}, [internalSetMode, mode, editor, selectedFeatureIds]);
const effectiveGeometryVisibility = useMemo(() => {
const visibility: Record<string, boolean> = { ...geometryVisibility };
if (mode === "replay" && replayFeatureId) {
// Ẩn chính geo được chọn làm replay
// Ẩn chính geo được chọn làm replay (marker kịch bản)
visibility[String(replayFeatureId)] = false;
if (hideOutside) {
// Tìm feature đang replay để lấy danh sách binding
const replayFeature = editor.draft.features.find(
(f) => String(f.properties.id) === String(replayFeatureId)
);
const boundIds = new Set<string>();
if (replayFeature?.properties?.binding) {
replayFeature.properties.binding.forEach((id: string) => boundIds.add(String(id)));
}
// Ẩn tất cả các geo không nằm trong binding
editor.draft.features.forEach((f) => {
const fid = String(f.properties.id);
if (fid !== String(replayFeatureId) && !boundIds.has(fid)) {
// Trong mode replay, ta chỉ hiển thị những gì có trong draft của replay đó
const currentReplayFeatureIds = new Set(editor.draft.features.map(f => String(f.properties.id)));
// Ẩn tất cả các geo KHÔNG nằm trong draft replay hiện tại
Object.keys(visibility).forEach(fid => {
if (fid === String(replayFeatureId)) {
visibility[fid] = false;
} else {
visibility[fid] = currentReplayFeatureIds.has(fid);
}
});
}
@@ -1686,6 +1695,7 @@ export default function Page() {
isEntitySubmitting={isEntitySubmitting}
onApplyGeometryMetadata={featureCommands.applyGeometryMetadata}
changeCount={editor.changeCount}
onReplayEdit={(id) => setMode("replay", id)}
/>
) : null}
</div>