complete replay editor v1
This commit is contained in:
+315
-145
@@ -1,194 +1,364 @@
|
||||
/**
|
||||
* Tài liệu schema tham chiếu cho snapshot commit.
|
||||
* Schema tham chiếu cho commit snapshot.
|
||||
*
|
||||
* Lưu ý:
|
||||
* - Đây không phải "single source of truth" của runtime hiện tại; logic normalize/build thật nằm ở
|
||||
* `src/uhm/lib/editor/snapshot/editorSnapshot.ts`.
|
||||
* - Các phần như `replays` và nhóm `UIFunctionName` / `MapFunctionName` mô tả schema dự phòng hoặc tương lai.
|
||||
* Editor route `/editor/[id]` hiện có mode `replay`, nhưng chưa thực thi hệ thống scripted replay đầy đủ theo file này.
|
||||
* - Các field denormalized dùng cho UI như `entity_ids`, `entity_name`, `binding`, `time_start`, `time_end`
|
||||
* có thể xuất hiện trong editor runtime, nhưng frontend sẽ strip hoặc tái sinh chúng khi build snapshot API.
|
||||
* Đây là file doc tự chứa, không import runtime types.
|
||||
* Mục tiêu là mô tả đúng shape dữ liệu hiện tại của editor/commit/replay
|
||||
* mà không phụ thuộc trực tiếp vào source code runtime.
|
||||
*
|
||||
* Ghi chú:
|
||||
* - Payload tạo commit hiện là `{ snapshot_json, edit_summary }`.
|
||||
* - `CommitSnapshot` hiện tương đương `EditorSnapshot`.
|
||||
* - Nhiều field root để optional vì frontend còn phải đọc snapshot cũ / partial.
|
||||
* - Replay actions trong dữ liệu thật dùng `params: unknown[]` theo positional tuple.
|
||||
* - Snapshot replay cũ còn `replay_features` sẽ được FE migrate sang `target_geometry_ids` khi load.
|
||||
* - Trước khi gửi API, frontend còn normalize thêm một số field, ví dụ `geometries[].type`.
|
||||
*/
|
||||
|
||||
// ---- Root request ----
|
||||
|
||||
export type CreateCommitRequest = {
|
||||
snapshot_json: CommitSnapshot;
|
||||
edit_summary: string;
|
||||
};
|
||||
|
||||
// ---- Snapshot root ----
|
||||
|
||||
export type CommitSnapshot = {
|
||||
editor_feature_collection: FeatureCollection;
|
||||
entities: EntitySnapshot[];
|
||||
geometries: GeometrySnapshot[];
|
||||
geometry_entity: GeometryEntitySnapshot[];
|
||||
wikis: WikiSnapshot[];
|
||||
entity_wiki: EntityWikiLinkSnapshot[];
|
||||
replays?: BattleReplay[];
|
||||
};
|
||||
|
||||
// ---- Replay / Scripting System ----
|
||||
|
||||
export type UIFunctionName =
|
||||
| "hide_timeline" // Ẩn thanh timeline
|
||||
| "hide_layer_panel" // Ẩn panel lớp bản đồ
|
||||
| "hide_wiki_panel" // Ẩn panel wiki (bên phải)
|
||||
| "hide_zoom_panel" // Ẩn các nút điều khiển zoom
|
||||
| "hide_all_UI" // Ẩn toàn bộ giao diện điều khiển (cinematic mode)
|
||||
| "open_wiki" // Mở panel wiki
|
||||
| "show_toast_message" // Hiển thị thông báo ngắn (toast)
|
||||
| "focus_wiki_header" // Cuộn đến đề mục cụ thể trong Wiki
|
||||
| "set_playback_speed"; // Thay đổi tốc độ phát replay
|
||||
|
||||
export type MapFunctionName =
|
||||
| "zoom_to_lnglat" // Di chuyển camera đến tọa độ [lng, lat]
|
||||
| "zoom_scale" // Thay đổi mức zoom của bản đồ
|
||||
| "zoom_geometries" // Zoom bao quát danh sách các geometry
|
||||
| "change_geometry_color" // Thay đổi màu của một geometry
|
||||
| "change_geometries_color" // Thay đổi màu của danh sách geometry
|
||||
| "change_geometry_texture" // Thay đổi texture của một geometry
|
||||
| "change_geometries_texture"// Thay đổi texture của danh sách geometry
|
||||
| "hide_geometries" // Ẩn danh sách các geometry
|
||||
| "set_camera_view" // Đặt trạng thái camera (center, zoom, pitch, bearing)
|
||||
| "fly_to_geometry" // Di chuyển mượt mà đến một geometry
|
||||
| "rotate_around_point" // Xoay camera quanh một điểm
|
||||
| "pulse_geometry" // Hiệu ứng nhấp nháy cho geometry
|
||||
| "set_time_filter" // Thay đổi bộ lọc thời gian trên bản đồ
|
||||
| "toggle_labels"; // Bật/tắt hiển thị nhãn (labels) trên bản đồ
|
||||
|
||||
export type NarrativeFunctionName =
|
||||
| "set_title" // Đặt tiêu đề cho bước replay
|
||||
| "set_descriptions" // Đặt mô tả/nội dung diễn giải
|
||||
| "show_dialog_box" // Hiển thị hộp thoại dẫn chuyện (có avatar)
|
||||
| "display_historical_image" // Hiển thị hình ảnh tư liệu đè lên bản đồ
|
||||
| "set_step_subtitle"; // Hiển thị phụ đề phía dưới màn hình
|
||||
|
||||
export type ReplayAction<T> = {
|
||||
function_name: T;
|
||||
params: any[];
|
||||
};
|
||||
|
||||
export type ReplayStep = {
|
||||
duration: number; // Trọng số thời gian của step trong 1 stage
|
||||
use_UI_function: ReplayAction<UIFunctionName>[];
|
||||
use_map_function: ReplayAction<MapFunctionName>[];
|
||||
use_narrow_function: ReplayAction<NarrativeFunctionName>[];
|
||||
};
|
||||
|
||||
export type ReplayStage = {
|
||||
id: number; // số đếm thứ tự từ 0
|
||||
title?: string;
|
||||
detail_time_start: string;
|
||||
detail_time_stop: string;
|
||||
steps: ReplayStep[];
|
||||
};
|
||||
|
||||
export type BattleReplay = {
|
||||
geometry_id: string; // geometry mà khi nhấn vào là có thể replay
|
||||
detail: ReplayStage[];
|
||||
snapshot_json: CommitSnapshot;
|
||||
edit_summary: string;
|
||||
};
|
||||
|
||||
// ---- GeoJSON / FeatureCollection ----
|
||||
|
||||
export type GeometryPreset = "line" | "polygon" | "circle-area" | "point";
|
||||
|
||||
export type Geometry =
|
||||
| ({ type: "Point"; coordinates: [number, number] } & CircleGeometryMetadata)
|
||||
| ({ type: "MultiPoint"; coordinates: [number, number][] } & CircleGeometryMetadata)
|
||||
| ({ type: "LineString"; coordinates: [number, number][] } & CircleGeometryMetadata)
|
||||
| ({ type: "MultiLineString"; coordinates: [number, number][][] } & CircleGeometryMetadata)
|
||||
| ({ type: "Polygon"; coordinates: [number, number][][] } & CircleGeometryMetadata)
|
||||
| ({ type: "MultiPolygon"; coordinates: [number, number][][][] } & CircleGeometryMetadata);
|
||||
| ({ type: "Point"; coordinates: [number, number] } & CircleGeometryMetadata)
|
||||
| ({ type: "MultiPoint"; coordinates: [number, number][] } & CircleGeometryMetadata)
|
||||
| ({ type: "LineString"; coordinates: [number, number][] } & CircleGeometryMetadata)
|
||||
| ({ type: "MultiLineString"; coordinates: [number, number][][] } & CircleGeometryMetadata)
|
||||
| ({ type: "Polygon"; coordinates: [number, number][][] } & CircleGeometryMetadata)
|
||||
| ({ type: "MultiPolygon"; coordinates: [number, number][][][] } & CircleGeometryMetadata);
|
||||
|
||||
export type CircleGeometryMetadata = {
|
||||
circle_center?: [number, number];
|
||||
circle_radius?: number;
|
||||
circle_center?: [number, number];
|
||||
circle_radius?: number;
|
||||
};
|
||||
|
||||
export type FeatureId = string | number;
|
||||
|
||||
export type FeatureProperties = {
|
||||
id: FeatureId;
|
||||
type?: string | null; //generate
|
||||
geometry_preset?: string | null;
|
||||
time_start?: number | null; //generate
|
||||
time_end?: number | null; //generate
|
||||
binding?: string[]; //generate
|
||||
id: FeatureId;
|
||||
type?: string | null;
|
||||
geometry_preset?: GeometryPreset | null;
|
||||
time_start?: number | null;
|
||||
time_end?: number | null;
|
||||
binding?: string[];
|
||||
|
||||
// Legacy/UI-only fields should not be relied on by the backend.
|
||||
// FE strips these when building snapshot_json, but we keep them optional here
|
||||
// because older snapshots may still contain them.
|
||||
entity_id?: string | null; //generate
|
||||
entity_ids?: string[]; //generate
|
||||
entity_name?: string | null; //generate
|
||||
entity_names?: string[]; //generate
|
||||
entity_type_id?: string | null; //generate
|
||||
// UI/editor-only denormalized fields.
|
||||
entity_id?: string | null;
|
||||
entity_ids?: string[];
|
||||
entity_name?: string | null;
|
||||
entity_names?: string[];
|
||||
entity_type_id?: string | null;
|
||||
point_label?: string | null;
|
||||
line_label?: string | null;
|
||||
polygon_label?: string | null;
|
||||
};
|
||||
|
||||
export type Feature = {
|
||||
type: "Feature";
|
||||
properties: FeatureProperties;
|
||||
geometry: Geometry;
|
||||
type: "Feature";
|
||||
properties: FeatureProperties;
|
||||
geometry: Geometry;
|
||||
};
|
||||
|
||||
export type FeatureCollection = {
|
||||
type: "FeatureCollection";
|
||||
features: Feature[];
|
||||
type: "FeatureCollection";
|
||||
features: Feature[];
|
||||
};
|
||||
|
||||
// ---- Snapshot rows ----
|
||||
|
||||
export type SnapshotSource = "inline" | "ref";
|
||||
|
||||
export type SnapshotOperation = "create" | "update" | "delete" | "reference";
|
||||
|
||||
export type EntitySnapshot = {
|
||||
id: string;
|
||||
source: SnapshotSource;
|
||||
operation?: SnapshotOperation;
|
||||
export type EntitySnapshotOperation = SnapshotOperation;
|
||||
export type GeometrySnapshotOperation = SnapshotOperation;
|
||||
export type WikiSnapshotOperation = SnapshotOperation;
|
||||
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
export type EntitySnapshot = {
|
||||
id: string;
|
||||
source: SnapshotSource;
|
||||
operation?: EntitySnapshotOperation;
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
};
|
||||
|
||||
export type GeometrySnapshot = {
|
||||
id: string;
|
||||
source: SnapshotSource;
|
||||
operation?: SnapshotOperation;
|
||||
type?: string | null;
|
||||
draw_geometry?: Geometry;
|
||||
binding?: string[];
|
||||
time_start?: number | null;
|
||||
time_end?: number | null;
|
||||
bbox?: {
|
||||
min_lng: number;
|
||||
min_lat: number;
|
||||
max_lng: number;
|
||||
max_lat: number;
|
||||
} | null;
|
||||
id: string;
|
||||
source: SnapshotSource;
|
||||
operation?: GeometrySnapshotOperation;
|
||||
type?: string | null;
|
||||
draw_geometry?: Geometry;
|
||||
geometry?: Geometry;
|
||||
binding?: string[];
|
||||
time_start?: number | null;
|
||||
time_end?: number | null;
|
||||
bbox?: {
|
||||
min_lng: number;
|
||||
min_lat: number;
|
||||
max_lng: number;
|
||||
max_lat: number;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type GeometryEntitySnapshot = {
|
||||
geometry_id: string;
|
||||
entity_id: string;
|
||||
operation?: "reference" | "delete" | "binding";
|
||||
geometry_id: string;
|
||||
entity_id: string;
|
||||
operation?: "reference" | "binding" | "delete";
|
||||
};
|
||||
|
||||
// FE stores wiki doc as a string (often HTML) or null for ref-only rows.
|
||||
export type WikiDoc = string | null;
|
||||
|
||||
export type WikiSnapshot = {
|
||||
id: string;
|
||||
source: SnapshotSource;
|
||||
operation?: SnapshotOperation;
|
||||
|
||||
title: string;
|
||||
slug?: string | null;
|
||||
doc: WikiDoc;
|
||||
id: string;
|
||||
source: SnapshotSource;
|
||||
operation?: WikiSnapshotOperation;
|
||||
title: string;
|
||||
slug?: string | null;
|
||||
doc: WikiDoc;
|
||||
};
|
||||
|
||||
export type EntityWikiLinkSnapshot = {
|
||||
entity_id: string;
|
||||
wiki_id: string;
|
||||
operation?: "reference" | "delete" | "binding";
|
||||
entity_id: string;
|
||||
wiki_id: string;
|
||||
operation?: "reference" | "binding" | "delete";
|
||||
};
|
||||
|
||||
// ---- Replay / Scripting System (runtime shape) ----
|
||||
|
||||
/**
|
||||
* Canonical UI action names trong snapshot hiện tại.
|
||||
* Không còn wrapper `function_name: "UI"` trong shape mới.
|
||||
*/
|
||||
export type UIOptionName =
|
||||
| "timeline"
|
||||
| "layer_panel"
|
||||
| "wiki_panel"
|
||||
| "zoom_panel"
|
||||
| "wiki"
|
||||
| "toast"
|
||||
| "wiki_header"
|
||||
| "playback_speed";
|
||||
|
||||
export type MapFunctionName =
|
||||
| "set_camera_view"
|
||||
| "set_time_filter"
|
||||
| "enable_timeline_filter"
|
||||
| "disable_timeline_filter"
|
||||
| "toggle_labels"
|
||||
| "show_labels"
|
||||
| "hide_labels"
|
||||
| "reset_camera_north";
|
||||
|
||||
export type GeoFunctionName =
|
||||
| "fly_to_geometry"
|
||||
| "fly_to_geometries"
|
||||
| "set_geometry_visibility"
|
||||
| "show_geometries"
|
||||
| "hide_geometries"
|
||||
| "fit_to_geometries"
|
||||
| "orbit_camera_around_geometry"
|
||||
| "pulse_geometry"
|
||||
| "animate_dashed_border"
|
||||
| "set_geometry_style"
|
||||
| "show_geometry_label"
|
||||
| "follow_geometry_path"
|
||||
| "follow_geometries_path"
|
||||
| "dim_other_geometries";
|
||||
|
||||
export type NarrativeFunctionName =
|
||||
| "set_title"
|
||||
| "set_descriptions"
|
||||
| "show_dialog_box"
|
||||
| "display_historical_image"
|
||||
| "set_step_subtitle";
|
||||
|
||||
/**
|
||||
* Runtime thật hiện dùng positional array cho params.
|
||||
* File doc này giữ đúng shape đó.
|
||||
*/
|
||||
export type ReplayAction<T> = {
|
||||
function_name: T;
|
||||
params: unknown[];
|
||||
};
|
||||
|
||||
export type ReplayStep = {
|
||||
duration: number;
|
||||
use_UI_function: ReplayAction<UIOptionName>[];
|
||||
use_map_function: ReplayAction<MapFunctionName>[];
|
||||
use_geo_function: ReplayAction<GeoFunctionName>[];
|
||||
use_narrow_function: ReplayAction<NarrativeFunctionName>[];
|
||||
};
|
||||
|
||||
export type ReplayStage = {
|
||||
id: number;
|
||||
title?: string;
|
||||
detail_time_start: string;
|
||||
detail_time_stop: string;
|
||||
steps: ReplayStep[];
|
||||
};
|
||||
|
||||
export type BattleReplay = {
|
||||
geometry_id: string;
|
||||
target_geometry_ids: string[];
|
||||
detail: ReplayStage[];
|
||||
};
|
||||
|
||||
// ---- Replay tuple docs ----
|
||||
|
||||
/**
|
||||
* Doc-only helper để giải thích meaning của từng vị trí trong `params`.
|
||||
* Runtime không ép các tuple này; chúng chỉ là tài liệu tham chiếu.
|
||||
*/
|
||||
|
||||
export type ReplayCameraViewStateDoc = {
|
||||
center?: [number, number] | { lng: number; lat: number };
|
||||
zoom?: number;
|
||||
pitch?: number;
|
||||
bearing?: number;
|
||||
duration?: number;
|
||||
};
|
||||
|
||||
export type ReplayUiParamTupleDocs = {
|
||||
timeline: [visible: boolean];
|
||||
layer_panel: [visible: boolean];
|
||||
wiki_panel: [visible: boolean];
|
||||
zoom_panel: [visible: boolean];
|
||||
wiki: [wiki_id: string];
|
||||
toast: [message: string];
|
||||
wiki_header: [header_id: string];
|
||||
playback_speed: [speed: number];
|
||||
};
|
||||
|
||||
/**
|
||||
* Snapshot cũ kiểu `function_name: "UI"` chỉ còn là legacy input.
|
||||
* Frontend hiện normalize chúng sang `function_name: UIOptionName` khi load.
|
||||
*/
|
||||
|
||||
export type ReplayMapFunctionParamTupleDocs = {
|
||||
set_camera_view: [state: ReplayCameraViewStateDoc];
|
||||
set_time_filter: [year: number];
|
||||
enable_timeline_filter: [];
|
||||
disable_timeline_filter: [];
|
||||
toggle_labels: [visible: boolean];
|
||||
show_labels: [];
|
||||
hide_labels: [];
|
||||
reset_camera_north: [];
|
||||
};
|
||||
|
||||
export type ReplayGeoFunctionParamTupleDocs = {
|
||||
fly_to_geometry: [
|
||||
geometry_id: string,
|
||||
zoom?: number,
|
||||
padding?: number,
|
||||
duration?: number,
|
||||
];
|
||||
fly_to_geometries: [geometry_ids: string[]];
|
||||
set_geometry_visibility: [geometry_ids: string[], visible: boolean];
|
||||
show_geometries: [geometry_ids: string[]];
|
||||
hide_geometries: [geometry_ids: string[]];
|
||||
fit_to_geometries: [
|
||||
geometry_ids: string[],
|
||||
padding?: number,
|
||||
duration?: number,
|
||||
];
|
||||
orbit_camera_around_geometry: [
|
||||
geometry_id: string,
|
||||
zoom?: number,
|
||||
pitch?: number,
|
||||
revolutions?: number,
|
||||
duration?: number,
|
||||
];
|
||||
pulse_geometry: [
|
||||
geometry_id: string,
|
||||
color?: string,
|
||||
repeat?: number,
|
||||
duration?: number,
|
||||
];
|
||||
animate_dashed_border: [
|
||||
geometry_id: string,
|
||||
color?: string,
|
||||
width?: number,
|
||||
speed?: number,
|
||||
duration?: number,
|
||||
];
|
||||
set_geometry_style: [
|
||||
geometry_ids: string[],
|
||||
fill_color?: string,
|
||||
fill_opacity?: number,
|
||||
line_color?: string,
|
||||
line_width?: number,
|
||||
];
|
||||
show_geometry_label: [
|
||||
geometry_id: string,
|
||||
text?: string,
|
||||
color?: string,
|
||||
size?: number,
|
||||
];
|
||||
follow_geometry_path: [
|
||||
geometry_id: string,
|
||||
duration?: number,
|
||||
zoom?: number,
|
||||
pitch?: number,
|
||||
];
|
||||
follow_geometries_path: [
|
||||
geometry_ids: string[],
|
||||
duration?: number,
|
||||
zoom?: number,
|
||||
pitch?: number,
|
||||
];
|
||||
dim_other_geometries: [
|
||||
geometry_ids: string[],
|
||||
opacity?: number,
|
||||
];
|
||||
};
|
||||
|
||||
export type ReplayNarrativeParamTupleDocs = {
|
||||
set_title: [title: string];
|
||||
set_descriptions: [text: string];
|
||||
show_dialog_box: [
|
||||
avatar: string,
|
||||
text: string,
|
||||
side?: "left" | "right",
|
||||
speaker?: string,
|
||||
];
|
||||
display_historical_image: [
|
||||
url: string,
|
||||
caption?: string,
|
||||
];
|
||||
set_step_subtitle: [subtitle: string | null];
|
||||
};
|
||||
|
||||
export type ReplayParamTupleDocs =
|
||||
& ReplayUiParamTupleDocs
|
||||
& ReplayMapFunctionParamTupleDocs
|
||||
& ReplayGeoFunctionParamTupleDocs
|
||||
& ReplayNarrativeParamTupleDocs;
|
||||
|
||||
export type ReplayActionTupleDoc<T extends keyof ReplayParamTupleDocs> = {
|
||||
function_name: T;
|
||||
params: ReplayParamTupleDocs[T];
|
||||
};
|
||||
|
||||
// ---- Snapshot root ----
|
||||
|
||||
export type EditorSnapshot = {
|
||||
// Legacy snapshots có thể còn field project embedded.
|
||||
project?: {
|
||||
id: string;
|
||||
title: string;
|
||||
};
|
||||
editor_feature_collection?: FeatureCollection;
|
||||
entities?: EntitySnapshot[];
|
||||
geometries?: GeometrySnapshot[];
|
||||
geometry_entity?: GeometryEntitySnapshot[];
|
||||
wikis?: WikiSnapshot[];
|
||||
entity_wiki?: EntityWikiLinkSnapshot[];
|
||||
replays?: BattleReplay[];
|
||||
};
|
||||
|
||||
export type CommitSnapshot = EditorSnapshot;
|
||||
|
||||
@@ -164,7 +164,6 @@ Có thể do:
|
||||
- timeline filter
|
||||
- geometry visibility theo type
|
||||
- binding filter
|
||||
- replay hide outside
|
||||
|
||||
Không phải lúc nào cũng là bug render layer.
|
||||
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
# UHM Editor - state replay hiện tại
|
||||
|
||||
Tài liệu này mô tả đúng flow replay mode hiện tại của `/editor/[id]`.
|
||||
|
||||
Nguồn thật:
|
||||
|
||||
- `src/app/editor/[id]/page.tsx`
|
||||
- `src/uhm/lib/editor/state/useEditorState.ts`
|
||||
- `src/uhm/lib/editor/project/useProjectCommands.ts`
|
||||
- `src/uhm/lib/editor/snapshot/editorSnapshot.ts`
|
||||
|
||||
## 1. Kết luận ngắn
|
||||
|
||||
Replay mode hiện tại có 2 lớp state:
|
||||
|
||||
- `activeReplayDraft`
|
||||
- là `BattleReplay` đang chỉnh
|
||||
- chỉ chứa `geometry_id`, `target_geometry_ids`, `detail`
|
||||
- `replayDraft`
|
||||
- là `FeatureCollection` local, được FE hydrate lại từ `mainDraft + target_geometry_ids`
|
||||
- chỉ dùng để map/render/select trong replay mode
|
||||
|
||||
Điểm quan trọng:
|
||||
|
||||
- `replayDraft` không còn được persist vào commit/API
|
||||
- commit chỉ lưu `replays[]` với `target_geometry_ids`
|
||||
- snapshot cũ còn `replay_features` sẽ được FE migrate sang `target_geometry_ids` khi load
|
||||
|
||||
## 2. Shape replay hiện tại
|
||||
|
||||
```ts
|
||||
type BattleReplay = {
|
||||
geometry_id: string;
|
||||
target_geometry_ids: string[];
|
||||
detail: ReplayStage[];
|
||||
};
|
||||
```
|
||||
|
||||
Ý nghĩa:
|
||||
|
||||
- `geometry_id`
|
||||
- MAIN geo của replay
|
||||
- cũng là key để tìm replay tương ứng
|
||||
- `target_geometry_ids`
|
||||
- toàn bộ geo được đưa vào replay
|
||||
- phần tử đầu nên luôn là MAIN geo
|
||||
- `detail`
|
||||
- stage/step/actions của kịch bản
|
||||
|
||||
## 3. Replay được mở như thế nào
|
||||
|
||||
Khi vào replay từ UI:
|
||||
|
||||
1. editor lấy `triggerId`
|
||||
- ưu tiên `selectedFeatureIds[0]`
|
||||
- nếu chưa có selection thì dùng `featureId` vừa click
|
||||
2. gọi `editor.switchReplayContext(triggerId, selectedFeatureIds)`
|
||||
3. `switchReplayContext()` sẽ:
|
||||
- flush replay cũ nếu đang mở replay khác
|
||||
- tìm replay đã tồn tại theo `geometry_id`
|
||||
- nếu chưa có thì tạo seed mới
|
||||
|
||||
## 4. Seed replay được tạo ra sao
|
||||
|
||||
Replay seed mới có dạng:
|
||||
|
||||
```ts
|
||||
{
|
||||
geometry_id: triggerId,
|
||||
target_geometry_ids: [...],
|
||||
detail: []
|
||||
}
|
||||
```
|
||||
|
||||
`target_geometry_ids` được build từ:
|
||||
|
||||
- MAIN geo
|
||||
- toàn bộ bulk selection hiện tại
|
||||
- toàn bộ `binding` của MAIN geo trong `mainDraft`
|
||||
|
||||
Rule hiện tại:
|
||||
|
||||
- MAIN geo luôn đứng đầu
|
||||
- geo trùng sẽ được dedupe
|
||||
- nếu replay đã tồn tại sẵn, FE giữ `detail` cũ và chỉ append thêm geo mới còn thiếu vào `target_geometry_ids`
|
||||
|
||||
## 5. `replayDraft` được hydrate thế nào
|
||||
|
||||
`replayDraft` không còn nằm trong snapshot.
|
||||
|
||||
Mỗi lần:
|
||||
|
||||
- mở replay
|
||||
- undo replay session
|
||||
- restore `activeReplayDraft`
|
||||
|
||||
FE sẽ hydrate lại:
|
||||
|
||||
```ts
|
||||
replayDraft = hydrate(mainDraft, activeReplayDraft.target_geometry_ids)
|
||||
```
|
||||
|
||||
Hydrate hiện tại:
|
||||
|
||||
- lấy feature từ `mainDraft` theo đúng thứ tự `target_geometry_ids`
|
||||
- clone ra `FeatureCollection` mới
|
||||
- flatten `binding` thành `[]` để các geo trong replay bình đẳng với nhau
|
||||
|
||||
## 6. Trong replay mode map đang đọc gì
|
||||
|
||||
`useEditorState()` vẫn switch active draft như cũ:
|
||||
|
||||
```ts
|
||||
const activeDraft = mode === "replay" ? replayDraft : mainDraft;
|
||||
```
|
||||
|
||||
Nên khi `mode === "replay"`:
|
||||
|
||||
- `editor.draft` trỏ vào `replayDraft`
|
||||
- `editor.draftRef` trỏ vào `replayDraftRef`
|
||||
- map chỉ render tập geo đang nằm trong `target_geometry_ids`
|
||||
|
||||
## 7. Replay mode còn sửa geometry không
|
||||
|
||||
Không.
|
||||
|
||||
Hiện tại state layer đã chặn toàn bộ nhánh mutate geometry trong replay mode:
|
||||
|
||||
- `createFeature`
|
||||
- `createFeatureWithSnapshotEntities`
|
||||
- `patchFeatureProperties`
|
||||
- `patchFeaturePropertiesBatch`
|
||||
- `updateFeature`
|
||||
- `deleteFeature`
|
||||
|
||||
Nghĩa là:
|
||||
|
||||
- replay mode chỉ còn là nơi viết script replay
|
||||
- không còn persist hay commit geometry edit riêng của replay
|
||||
|
||||
## 8. Cái gì vẫn được sửa trong replay mode
|
||||
|
||||
Replay sidebar vẫn sửa:
|
||||
|
||||
- `detail[]`
|
||||
- `stage`
|
||||
- `step`
|
||||
- các action `UI / map / geo / narrative`
|
||||
|
||||
Các thay đổi đó đi qua:
|
||||
|
||||
- `editor.mutateActiveReplay`
|
||||
- `applyReplaySessionMutation()`
|
||||
|
||||
Undo replay vẫn riêng ở:
|
||||
|
||||
- `replayUndoStack`
|
||||
|
||||
## 9. Khi nào replay được flush về `replays[]`
|
||||
|
||||
`activeReplayDraft` chỉ là session đang mở.
|
||||
|
||||
Nó được flush về `replays[]` khi:
|
||||
|
||||
- thoát replay mode
|
||||
- chuyển sang replay khác
|
||||
|
||||
Hàm chịu trách nhiệm là:
|
||||
|
||||
- `finalizeActiveReplaySession()`
|
||||
|
||||
## 10. Commit lấy replay từ đâu
|
||||
|
||||
Commit không lấy `activeReplayDraft` trực tiếp.
|
||||
|
||||
Nó lấy:
|
||||
|
||||
- `editor.effectiveReplays`
|
||||
|
||||
`effectiveReplays` là:
|
||||
|
||||
- `replays`
|
||||
- cộng thêm overlay của `activeReplayDraft` nếu session hiện tại đã thay đổi nhưng chưa flush
|
||||
|
||||
Vì vậy:
|
||||
|
||||
- đang còn ở replay mode vẫn commit được replay mới nhất
|
||||
- không cần thoát replay mode mới lưu được script
|
||||
|
||||
## 11. Replay đi qua API ra sao
|
||||
|
||||
Payload commit hiện tại chỉ gửi:
|
||||
|
||||
- `geometry_id`
|
||||
- `target_geometry_ids`
|
||||
- `detail`
|
||||
|
||||
Không gửi:
|
||||
|
||||
- `replayDraft`
|
||||
- `replay_features`
|
||||
- `FeatureCollection` local của replay mode
|
||||
|
||||
## 12. Migrate dữ liệu cũ
|
||||
|
||||
Snapshot cũ nếu còn:
|
||||
|
||||
```ts
|
||||
replay_features?: FeatureCollection
|
||||
```
|
||||
|
||||
thì FE sẽ:
|
||||
|
||||
- đọc `replay_features.features[].properties.id`
|
||||
- chuyển chúng thành `target_geometry_ids`
|
||||
- bỏ `replay_features` khỏi runtime replay mới
|
||||
|
||||
Nên dữ liệu cũ vẫn mở được, nhưng commit mới sẽ ra schema mới.
|
||||
@@ -0,0 +1,246 @@
|
||||
# Replay Export JSON
|
||||
|
||||
Tài liệu này mô tả đúng payload mà nút `Export JSON` của replay đang xuất ra hiện tại.
|
||||
|
||||
Nguồn thật:
|
||||
|
||||
- `src/uhm/components/editor/ReplayTimelineSidebar.tsx`
|
||||
|
||||
## 1. Kết luận ngắn
|
||||
|
||||
Export hiện tại có dạng:
|
||||
|
||||
```json
|
||||
{
|
||||
"exported_at": "2026-05-17T12:34:56.000Z",
|
||||
"geometry_id": "geo-main-id",
|
||||
"current_replay": { "...": "BattleReplay hiện tại" },
|
||||
"snapshot_fragment": {
|
||||
"replays": [
|
||||
{ "...": "chính current_replay" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Trong đó:
|
||||
|
||||
- `current_replay` là replay đang edit
|
||||
- `snapshot_fragment.replays[0]` là cùng replay đó, nhưng đặt vào đúng chỗ trong commit snapshot
|
||||
|
||||
## 2. Root payload
|
||||
|
||||
```ts
|
||||
type ReplayExportPayload = {
|
||||
exported_at: string;
|
||||
geometry_id: string;
|
||||
current_replay: BattleReplay;
|
||||
snapshot_fragment: {
|
||||
replays: BattleReplay[];
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Ý nghĩa:
|
||||
|
||||
- `exported_at`
|
||||
- timestamp ISO lúc bấm export
|
||||
- chỉ để debug
|
||||
- `geometry_id`
|
||||
- copy nhanh từ `current_replay.geometry_id`
|
||||
- `current_replay`
|
||||
- replay draft hiện tại
|
||||
- `snapshot_fragment`
|
||||
- fragment để test replay này nếu đặt vào commit snapshot thật
|
||||
|
||||
## 3. Shape của `current_replay`
|
||||
|
||||
```ts
|
||||
type BattleReplay = {
|
||||
geometry_id: string;
|
||||
target_geometry_ids: string[];
|
||||
detail: ReplayStage[];
|
||||
};
|
||||
```
|
||||
|
||||
Ý nghĩa:
|
||||
|
||||
- `geometry_id`
|
||||
- MAIN geo của replay
|
||||
- `target_geometry_ids`
|
||||
- toàn bộ geo thuộc replay
|
||||
- phần tử đầu nên luôn là MAIN geo
|
||||
- `detail`
|
||||
- stage/step/actions của replay script
|
||||
|
||||
## 4. `target_geometry_ids` là gì
|
||||
|
||||
Đây là phần thay thế cho `replay_features` cũ.
|
||||
|
||||
FE không còn export/persist cả `FeatureCollection` riêng của replay nữa. Thay vào đó chỉ lưu:
|
||||
|
||||
- geo MAIN
|
||||
- các geo được đưa vào replay từ bulk select
|
||||
- binding của MAIN geo
|
||||
|
||||
Khi mở replay, FE sẽ hydrate lại `replayDraft` từ:
|
||||
|
||||
- `mainDraft`
|
||||
- `target_geometry_ids`
|
||||
|
||||
## 5. Shape của `detail`
|
||||
|
||||
```ts
|
||||
type ReplayStage = {
|
||||
id: number;
|
||||
title?: string;
|
||||
detail_time_start: string;
|
||||
detail_time_stop: string;
|
||||
steps: ReplayStep[];
|
||||
};
|
||||
```
|
||||
|
||||
```ts
|
||||
type ReplayStep = {
|
||||
duration: number;
|
||||
use_UI_function: ReplayAction<UIOptionName>[];
|
||||
use_map_function: ReplayAction<MapFunctionName>[];
|
||||
use_geo_function: ReplayAction<GeoFunctionName>[];
|
||||
use_narrow_function: ReplayAction<NarrativeFunctionName>[];
|
||||
};
|
||||
```
|
||||
|
||||
Ý nghĩa:
|
||||
|
||||
- `stage` là cụm lớn theo mốc thời gian hoặc nhịp kể chuyện
|
||||
- `step` là đơn vị phát nhỏ hơn trong một stage
|
||||
- `duration` là trọng số thời gian của step
|
||||
- action hiện tách thành 4 nhóm
|
||||
|
||||
## 6. Ví dụ JSON gần thực tế
|
||||
|
||||
```json
|
||||
{
|
||||
"exported_at": "2026-05-17T12:34:56.000Z",
|
||||
"geometry_id": "019e13ab-4823-76c5-afde-2391c0cf311d",
|
||||
"current_replay": {
|
||||
"geometry_id": "019e13ab-4823-76c5-afde-2391c0cf311d",
|
||||
"target_geometry_ids": [
|
||||
"019e13ab-4823-76c5-afde-2391c0cf311d",
|
||||
"019e13ab-6063-713d-a28f-98a1556817a7",
|
||||
"019e13ab-5896-713a-111111111111"
|
||||
],
|
||||
"detail": [
|
||||
{
|
||||
"id": 0,
|
||||
"title": "Mở đầu chiến dịch",
|
||||
"detail_time_start": "1939",
|
||||
"detail_time_stop": "1940",
|
||||
"steps": [
|
||||
{
|
||||
"duration": 1000,
|
||||
"use_UI_function": [
|
||||
{
|
||||
"function_name": "timeline",
|
||||
"params": [false]
|
||||
}
|
||||
],
|
||||
"use_map_function": [
|
||||
{
|
||||
"function_name": "set_time_filter",
|
||||
"params": [1939]
|
||||
}
|
||||
],
|
||||
"use_geo_function": [
|
||||
{
|
||||
"function_name": "fly_to_geometries",
|
||||
"params": [
|
||||
[
|
||||
"019e13ab-4823-76c5-afde-2391c0cf311d",
|
||||
"019e13ab-6063-713d-a28f-98a1556817a7"
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"use_narrow_function": [
|
||||
{
|
||||
"function_name": "set_title",
|
||||
"params": ["Chiến dịch bắt đầu"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"snapshot_fragment": {
|
||||
"replays": [
|
||||
{
|
||||
"geometry_id": "019e13ab-4823-76c5-afde-2391c0cf311d",
|
||||
"target_geometry_ids": [
|
||||
"019e13ab-4823-76c5-afde-2391c0cf311d",
|
||||
"019e13ab-6063-713d-a28f-98a1556817a7",
|
||||
"019e13ab-5896-713a-111111111111"
|
||||
],
|
||||
"detail": [
|
||||
{
|
||||
"id": 0,
|
||||
"title": "Mở đầu chiến dịch",
|
||||
"detail_time_start": "1939",
|
||||
"detail_time_stop": "1940",
|
||||
"steps": [
|
||||
{
|
||||
"duration": 1000,
|
||||
"use_UI_function": [
|
||||
{
|
||||
"function_name": "timeline",
|
||||
"params": [false]
|
||||
}
|
||||
],
|
||||
"use_map_function": [
|
||||
{
|
||||
"function_name": "set_time_filter",
|
||||
"params": [1939]
|
||||
}
|
||||
],
|
||||
"use_geo_function": [
|
||||
{
|
||||
"function_name": "fly_to_geometries",
|
||||
"params": [
|
||||
[
|
||||
"019e13ab-4823-76c5-afde-2391c0cf311d",
|
||||
"019e13ab-6063-713d-a28f-98a1556817a7"
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"use_narrow_function": [
|
||||
{
|
||||
"function_name": "set_title",
|
||||
"params": ["Chiến dịch bắt đầu"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Cách đọc file export
|
||||
|
||||
Khi nhìn file export:
|
||||
|
||||
- nếu cần biết replay bám vào geo nào, xem `geometry_id`
|
||||
- nếu cần biết replay gồm những geo nào, xem `target_geometry_ids`
|
||||
- nếu cần biết script sẽ làm gì, xem `detail[].steps[]`
|
||||
- nếu cần so với commit snapshot, xem `snapshot_fragment.replays`
|
||||
|
||||
## 8. Ghi chú quan trọng
|
||||
|
||||
- Export hiện tại không còn chứa `replay_features`
|
||||
- Nếu mở replay cũ từng dùng `replay_features`, FE sẽ migrate sang `target_geometry_ids` trước khi export
|
||||
- `current_replay` và `snapshot_fragment.replays[0]` hiện vẫn là cùng một replay, chỉ khác góc nhìn
|
||||
@@ -157,7 +157,6 @@ Binding hiện tại:
|
||||
Nó là mode hiển thị tập trung vào một geometry:
|
||||
|
||||
- có nút thoát replay
|
||||
- có toggle `Hide Outside`
|
||||
- có thể ẩn geometry ngoài danh sách `binding`
|
||||
|
||||
## 8. Đồng bộ selection và feature state
|
||||
|
||||
Reference in New Issue
Block a user