complete replay editor v1

This commit is contained in:
taDuc
2026-05-17 21:45:33 +07:00
parent 3808086529
commit 047f662736
23 changed files with 4658 additions and 490 deletions
+315 -145
View File
@@ -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;