(function Map({
Capture View
+
+
;
return a.label === next.label && JSON.stringify(a.prev) === JSON.stringify(next.prev);
}
+ case "replay": {
+ const next = b as Extract;
+ return (
+ a.geometryId === next.geometryId
+ && a.label === next.label
+ && JSON.stringify(a.prevReplay) === JSON.stringify(next.prevReplay)
+ );
+ }
+ case "replay_session": {
+ const next = b as Extract;
+ return (
+ a.geometryId === next.geometryId
+ && a.label === next.label
+ && JSON.stringify(a.prevReplay) === JSON.stringify(next.prevReplay)
+ );
+ }
case "group": {
const next = b as Extract;
return a.label === next.label && JSON.stringify(a.actions) === JSON.stringify(next.actions);
diff --git a/src/uhm/lib/editor/project/useProjectCommands.ts b/src/uhm/lib/editor/project/useProjectCommands.ts
index 361cb51..8b3695b 100644
--- a/src/uhm/lib/editor/project/useProjectCommands.ts
+++ b/src/uhm/lib/editor/project/useProjectCommands.ts
@@ -20,6 +20,7 @@ type EditorDraftApi = {
draft: FeatureCollection;
mainDraft: FeatureCollection;
replays: BattleReplay[];
+ effectiveReplays: BattleReplay[];
buildPayload: () => Change[];
clearChanges: () => void;
hasPersistedFeature: (id: Feature["properties"]["id"]) => boolean;
@@ -78,7 +79,7 @@ export function useProjectCommands(options: Options) {
snapshotEntities: state.snapshotEntities,
snapshotWikis: state.snapshotWikis,
snapshotEntityWikiLinks: state.snapshotEntityWikiLinks,
- replays: options.editor.replays,
+ replays: options.editor.effectiveReplays,
previousSnapshot: state.baselineSnapshot,
hasPersistedFeature: options.editor.hasPersistedFeature,
});
@@ -113,7 +114,7 @@ export function useProjectCommands(options: Options) {
state.setSnapshotEntities(sessionSnapshot.entities || []);
state.setSnapshotWikis(sessionSnapshot.wikis || []);
state.setSnapshotEntityWikiLinks(sessionSnapshot.entity_wiki || []);
- state.setInitialData(options.editor.draft);
+ state.setInitialData(options.editor.mainDraft);
options.editor.clearChanges();
state.setCommitTitle("");
state.setProjectCommits(await fetchProjectCommits(state.activeSection.id));
diff --git a/src/uhm/lib/editor/snapshot/editorSnapshot.ts b/src/uhm/lib/editor/snapshot/editorSnapshot.ts
index 2905378..e046898 100644
--- a/src/uhm/lib/editor/snapshot/editorSnapshot.ts
+++ b/src/uhm/lib/editor/snapshot/editorSnapshot.ts
@@ -3,7 +3,7 @@ import { normalizeGeoTypeKey, typeKeyToGeoTypeCode } from "@/uhm/lib/map/geo/geo
import type { Change } from "@/uhm/lib/editor/draft/editorTypes";
import type { EntitySnapshot } from "@/uhm/types/entities";
import type { EntitySnapshotOperation } from "@/uhm/types/entities";
-import type { Feature, FeatureCollection, Geometry, GeometryEntitySnapshot, GeometrySnapshot } from "@/uhm/types/geo";
+import type { Feature, FeatureCollection, GeometryEntitySnapshot, GeometrySnapshot } from "@/uhm/types/geo";
import type { BattleReplay, EditorSnapshot, Project } from "@/uhm/types/projects";
import type { WikiSnapshot } from "@/uhm/types/wiki";
@@ -25,20 +25,6 @@ interface RawEntityRow extends UnknownRecord {
status?: number;
}
-interface RawGeometryRow extends UnknownRecord {
- id?: string | number;
- operation?: string;
- source?: string;
- ref?: { id?: string };
- type?: string | number;
- geo_type?: string | number;
- draw_geometry?: Geometry;
- geometry?: Geometry;
- binding?: string[];
- time_start?: number;
- time_end?: number;
-}
-
interface RawWikiRow extends UnknownRecord {
id?: string;
operation?: string;
@@ -364,9 +350,9 @@ export function buildEditorSnapshot(options: {
if (prev.source !== "inline") continue;
// Carry forward as current-state inline entity; operation is a per-commit delta signal.
const cloned = JSON.parse(JSON.stringify(prev)) as EntitySnapshot;
- const { operation: _op, ...rest } = cloned;
+ delete cloned.operation;
entityRows.set(id, {
- ...rest,
+ ...cloned,
id,
source: "inline",
operation: "reference",
@@ -689,14 +675,6 @@ export function toApiEditorSnapshot(snapshot: EditorSnapshot): EditorSnapshot {
});
}
- if (Array.isArray(cloned.replays)) {
- cloned.replays = cloned.replays.map((replay) => {
- // Strip local-only replay_features before sending to BE
- const { replay_features: _, ...rest } = replay;
- return rest;
- });
- }
-
return cloned;
}
diff --git a/src/uhm/lib/editor/state/useEditorState.ts b/src/uhm/lib/editor/state/useEditorState.ts
index 5cc5d7d..6725fab 100644
--- a/src/uhm/lib/editor/state/useEditorState.ts
+++ b/src/uhm/lib/editor/state/useEditorState.ts
@@ -11,7 +11,7 @@ import { useUndoStack } from "@/uhm/lib/editor/draft/useUndoStack";
import type { Change, UndoAction } from "@/uhm/lib/editor/draft/editorTypes";
import type { EntitySnapshot } from "@/uhm/types/entities";
import type { WikiSnapshot } from "@/uhm/types/wiki";
-import type { BattleReplay, EditorSnapshot, EntityWikiLinkSnapshot } from "@/uhm/types/projects";
+import type { BattleReplay, EntityWikiLinkSnapshot } from "@/uhm/types/projects";
import { EMPTY_FEATURE_COLLECTION } from "@/uhm/lib/map/geo/constants";
import type { EditorMode } from "@/uhm/lib/editor/session/sessionTypes";
@@ -32,10 +32,14 @@ type FeaturePropertiesPatch = {
patch: Partial;
};
+type DraftRef = { current: FeatureCollection };
+type DraftCommit = (next: FeatureCollection) => void;
+type ReplayDraftSyncMode = "none" | "reset";
+
// State trung tâm của editor:
-// - draft: dữ liệu nguồn để render UI (chuyển đổi giữa main và replay)
-// - changes: map các thay đổi chờ lưu
-// - undoStack: lịch sử thao tác tối thiểu để hoàn tác
+// - main draft: dữ liệu section thông thường
+// - active replay draft: bản sao đầy đủ của toàn bộ BattleReplay đang chỉnh
+// - replay feature draft: FeatureCollection con để map/editor hiện tại thao tác
export function useEditorState(
initialData: FeatureCollection,
options: {
@@ -47,27 +51,87 @@ export function useEditorState(
const { snapshotUndo, initialReplays, mode } = options;
const mainDraftState = useDraftState(initialData);
- const replayDraftState = useDraftState(EMPTY_FEATURE_COLLECTION);
+ const replayFeatureDraftState = useDraftState(EMPTY_FEATURE_COLLECTION);
+ const {
+ draft: mainDraft,
+ draftRef: mainDraftRef,
+ commitDraft: commitMainDraft,
+ resetDraft: resetMainDraft,
+ } = mainDraftState;
+ const {
+ draft: replayDraft,
+ draftRef: replayDraftRef,
+ commitDraft: commitReplayDraft,
+ resetDraft: resetReplayDraft,
+ } = replayFeatureDraftState;
const [replays, setReplays] = useState(initialReplays || []);
const [activeReplayId, setActiveReplayId] = useState(null);
+ const [activeReplayDraft, setActiveReplayDraft] = useState(null);
- const activeDraftState = mode === "replay" ? replayDraftState : mainDraftState;
- const { draft, draftRef, commitDraft, resetDraft } = activeDraftState;
+ const replaysRef = useRef(initialReplays || []);
+ const activeReplayDraftRef = useRef(null);
+ const activeReplayOriginRef = useRef(null);
+ const activeReplaySeedRef = useRef(null);
- // Map baseline (id -> feature) để diff draft hiện tại ra changes.
+ const activeDraft = mode === "replay" ? replayDraft : mainDraft;
+ const activeDraftRef = mode === "replay" ? replayDraftRef : mainDraftRef;
+ const activeCommitDraft = mode === "replay" ? commitReplayDraft : commitMainDraft;
+
+ const updateReplaysState = useCallback((next: SetStateAction) => {
+ const resolved = resolveStateAction(next, replaysRef.current);
+ const cloned = deepClone(resolved || []);
+ replaysRef.current = cloned;
+ setReplays(cloned);
+ return cloned;
+ }, []);
+
+ const syncReplayFeatureDraft = useCallback((nextFeatures: FeatureCollection) => {
+ resetReplayDraft(deepClone(nextFeatures));
+ }, [resetReplayDraft]);
+
+ const setActiveReplayDraftState = useCallback((
+ next: SetStateAction,
+ syncMode: ReplayDraftSyncMode = "reset"
+ ) => {
+ const resolved = resolveStateAction(next, activeReplayDraftRef.current);
+ const cloned = resolved ? deepClone(resolved) : null;
+ activeReplayDraftRef.current = cloned;
+ setActiveReplayDraft(cloned);
+
+ if (syncMode === "reset") {
+ syncReplayFeatureDraft(cloned?.replay_features || EMPTY_FEATURE_COLLECTION);
+ }
+
+ return cloned;
+ }, [syncReplayFeatureDraft]);
+
+ useEffect(() => {
+ replaysRef.current = replays;
+ }, [replays]);
+
+ useEffect(() => {
+ activeReplayDraftRef.current = activeReplayDraft;
+ }, [activeReplayDraft]);
+
+ // Map baseline (id -> feature) để diff main draft ra changes.
const initialMapRef = useRef