feat: implement comprehensive map editing system with advanced geometry tools, replay management, and project session state modules.
Build and Release / release (push) Successful in 55s

This commit is contained in:
taDuc
2026-06-11 22:05:18 +07:00
parent 0d6599015b
commit 5a8dfc4b50
180 changed files with 43408 additions and 119 deletions
+348
View File
@@ -0,0 +1,348 @@
# UHM Editor - state và vòng đời dữ liệu
Tài liệu này mô tả state thật đang được dùng bởi editor hiện tại.
Entry point chính là `useEditorSessionState()``useEditorState()`.
## 1. Hai lớp state chính
Editor đang tách làm hai khối:
- `useEditorSessionState()`
- state UI, session, form, project, timeline, background, wiki
- `useEditorState(baselineFeatureCollection, snapshotUndo)`
- state draft hình học, diff và undo
Nói ngắn gọn:
- `session state` quyết định editor đang nhìn cái gì và panel đang thao tác gì
- `editor state` quyết định geometry nào đang tồn tại trong draft và khác baseline ra sao
## 2. State geometry trung tâm
### `baselineFeatureCollection`
- Nằm ở `useEditorSessionState()`
-`FeatureCollection` baseline được nạp vào editor khi mở project hoặc restore commit
- Khi thay đổi, `useEditorState()` sẽ reset toàn bộ draft và baseline tương ứng
### `mainDraft`
- Nằm trong `useEditorState()`
- Là working copy geometry chính dùng cho edit/commit
- Mọi thao tác create/update/delete geometry đều đi qua đây
### `editor.draft`
- Là draft đang active theo mode
- Ở mode thường trỏ tới `mainDraft`
- Ở mode `replay` trỏ tới `replayDraft`
- Panel metadata/selection đọc từ đây, không đọc từ `mapRenderDraft`
### `draftRef`
- Ref nội bộ tương ứng với draft trong `useEditorState()`
- Được dùng để luôn đọc được state mới nhất mà không phải rebind callback liên tục
- Không nhầm với `renderDraftRef` trong `Map.tsx`, vốn là dữ liệu đang render/interact trên map
### `initialMapRef`
- `Map<featureId, Feature>` tạo từ `baselineFeatureCollection`
- Là baseline để tính diff giữa draft hiện tại và dữ liệu gốc của session
### `changes`
- Kết quả `diffDraftToInitial(draft, initialMapRef.current)`
- Map theo `feature.properties.id`
- Mỗi phần tử có thể là:
- `create`
- `update`
- `delete`
Lưu ý: diff hiện chỉ là cơ chế nhận biết geometry nào đã thay đổi so với baseline. Snapshot commit thực tế vẫn được build từ toàn bộ `draft` cộng với các snapshot bảng phụ.
### `changeCount`
- Số lượng geometry thay đổi hiện tại
- Được cộng thêm dirty state của wiki/entity/entity-wiki/replay để tạo `pendingSaveCount`
## 3. Undo state
Undo được quản lý bởi `useUndoStack()`.
Kiểu action hiện có:
- `create`
- `delete`
- `update`
- `properties`
- `snapshot_entities`
- `snapshot_wikis`
- `snapshot_entity_wiki`
- `replay`
- `replays`
- `replay_session`
- `group`
Ý nghĩa:
- geometry create/delete/update/properties undo được trực tiếp trên `draft`
- snapshot entity/wiki/link undo được apply qua `snapshotUndo` API truyền vào `useEditorState`
- `replay`/`replays` undo các thay đổi script replay đã flush vào collection chính
- `replay_session` undo các thay đổi stage/step/action khi đang ở mode `replay`
- `group` dùng để gom nhiều thay đổi thành một thao tác undo logic
Editor hiện có `undo`, nhưng chưa có redo.
## 4. Session state theo nhóm
### 4.1. Mode và selection
- `mode: EditorMode`
- `selectedFeatureIds`
- `selectedGeometryEntityIds`
`selectedFeatureIds` là state gốc cho:
- panel metadata geometry
- bind entity
- bind geometry
- focus geometry từ search/binding panel
### 4.2. Form state
- `entityForm`
- dùng cho form tạo entity local
- `geometryMetaForm`
- `type_key`
- `time_start`
- `time_end`
Geometry-geometry bound state không nằm trong `geometryMetaForm`; `GeometryBindingPanel` chỉnh trực tiếp `feature.properties.bound_with` của geometry con.
### 4.3. Replay state
Replay state nằm trong `useEditorState()`:
- `replays`
- collection script đã flush vào state chính
- `activeReplayDraft`
- `BattleReplay` đang sửa trong mode `replay`
- `replayDraft`
- `FeatureCollection` hydrate từ `mainDraft + activeReplayDraft.target_geometry_ids`
- `effectiveReplays`
- `replays` cộng overlay của `activeReplayDraft` nếu session hiện tại đã đổi nhưng chưa flush
Undo của replay session dùng stack riêng khi `mode === "replay"`.
`replay_preview` là session preview trong page, dùng `previewSession`/`useReplayPreview()` và không persist. Khi thoát các chế độ preview, editor sẽ dọn dẹp hoàn toàn các map effects, highlight, và khôi phục camera view state & projection (Globe/Flat) ban đầu trước khi vào preview.
### 4.4. Project/session task state
`useProjectSessionState()` gom các cờ async vào một state machine nhỏ:
- `sectionTask: "idle" | "saving" | "submitting" | "opening-project"`
Từ đó sinh ra:
- `isSaving`
- `isSubmitting`
- `isOpeningSection`
Ngoài ra còn có:
- `activeSection`
- `projectState`
- `sectionCommits`
- `baselineSnapshot`
- `commitTitle`
### 4.5. Timeline state
`useTimelineState()` giữ:
- `timelineYear`
- `timelineDraftYear`
- `isTimelineLoading`
- `timelineStatus`
Trong page hiện tại, timeline filter đang dùng `timelineDraftYear`.
Không có fetch dữ liệu project theo `timelineYear`; timeline đang là client-side visibility filter.
### 4.6. Background/session UI
`useBackgroundSessionState()` giữ:
- `backgroundVisibility`
- `isBackgroundVisibilityReady`
Giá trị thật được load từ `localStorage` key `uhm.backgroundLayerVisibility.v1`.
### 4.7. Wiki/session state
`useWikiSessionState()` giữ:
- `snapshotWikis`
- `snapshotEntityWikiLinks`
Đây là single source of truth cho phần wiki trong snapshot commit.
### 4.8. Preview session states và refs (Viewer / Replay Preview)
Các states và refs điều khiển preview được khai báo trực tiếp trong `page.tsx`:
- `previewSession: ReplayPreviewSession | null`
- Đóng băng toàn bộ snapshot collections (replays, draft, entities, wikis, links) cùng timeline, filter và camera view state khi chạy preview.
- `previewAutoplayMode: "start" | "selection" | null`
- Trạng thái autoplay (bắt đầu từ đầu hay từ step được chọn) của Replay Preview.
- `previewWikiCache`, `previewWikiError`, `isPreviewWikiLoading`
- Cache và status để hiển thị nội dung Wiki tương tác trong sidebar preview.
- `previewFeaturePopupAnchor: MapFeaturePayload | null`
- Neo tọa độ/payload của popup hiển thị thông tin geometry khi click trên map ở preview mode.
- `previewActiveEntityId`, `isPreviewEntitySidebarOpen`
- Sidebar hiển thị chi tiết entity được chọn trong preview.
- `previewLinkEntityPopup: PreviewLinkEntityPopupState | null`
- Trạng thái popup điều hướng sang entity khác khi click vào link wiki trong preview.
- `editorOriginalMapViewStateRef: ReturnType<MapHandle["getViewState"]> | null`
- Ref lưu giữ camera view state và projection (Globe/Flat) ban đầu của editor trước khi bắt đầu preview, phục vụ việc khôi phục hoàn toàn bản đồ khi exit.
- `replayPreviewReturnRef: { mode: "replay" | "preview"; session: ReplayPreviewSession | null }`
- Ref ghi nhận session và mode trước đó khi chuyển tiếp từ Viewer Preview sang Replay Preview, cho phép quay trở lại đúng Viewer Preview khi click thoát Replay Preview.
## 5. Snapshot state
Editor đang làm việc với các snapshot collection chính ngoài geometry:
- `snapshotEntityRows`
- `snapshotWikis`
- `snapshotEntityWikiLinks`
- `replays` / `effectiveReplays`
Chúng đại diện cho "current session snapshot", không phải danh sách delta thô.
Ví dụ:
- entity ref được giữ bằng `operation: "reference"`
- entity/wiki local mới tạo có thể mang `operation: "create"`
- link entity-wiki mới tạo dùng `operation: "binding"`
Khi commit, `buildEditorSnapshot()` sẽ so với `baselineSnapshot` để chuyển các collection này thành snapshot đúng semantic cho backend.
## 6. Baseline snapshot là gì
`baselineSnapshot` là snapshot đang được xem như gốc của session hiện tại.
Nó được cập nhật khi:
- mở project
- commit thành công
- restore từ một commit
`baselineSnapshot` được dùng để:
- biết link nào là `reference`, link nào là `binding`, link nào là `delete`
- biết wiki/entity nào là thay đổi thực sự so với snapshot trước
- giữ lại inline entity/wiki từ snapshot trước nếu user chưa xóa chúng
## 7. Derived state quan trọng trong page
### `mapRenderDraft`
-`FeatureCollection` duy nhất trong page quyết định geometry nào được truyền xuống map
- nguồn có thể là `mainDraft`, `replayDraft`, hoặc preview draft tùy mode
- đã qua filter timeline nếu `timelineFilterEnabled = true`
- đã qua replay preview hidden ids nếu đang preview
- geometry mới tạo trong session cũng bị timeline filter xử lý như geometry baseline
### `labelContextBaseDraft` và `mapLabelContextDraft`
- chỉ dùng để enrich/lookup label entity cho map
- có thể chứa geometry bị `mapRenderDraft` lọc ra
- không được dùng để quyết định geometry nào render trên map
### `geometryChoices`
- nguồn dữ liệu cho `GeometryBindingPanel`
- thêm trạng thái derived như orphan entity, time completeness, timeline visibility, hidden/bound/new
- ID geometry không phải label chính của row, nhưng vẫn nằm trong tooltip/title
### `snapshotEntityRowsVisible`
- loại bỏ các row `delete`
- dedupe theo `id`
### `selectedFeatures`
- map từ `selectedFeatureIds` sang feature thật trong `editor.draft.features`
### `isMultiEditValid`
- chỉ `true` khi tất cả geometry đang chọn cùng `geometry.type`
- một số thao tác bind sẽ chặn nếu giá trị này là `false`
### `pendingSaveCount`
Được tính như sau:
- `editor.changeCount`
- `+1` nếu wiki dirty
- `+1` nếu entities dirty
- `+1` nếu entity-wiki dirty
- `+1` nếu replay dirty
Đây là con số dùng trong UI commit, không phải số record backend chắc chắn sẽ thay đổi.
## 8. Dirty detection
Dirty check của:
- `snapshotWikis`
- `snapshotEntityRows`
- `snapshotEntityWikiLinks`
- `editor.effectiveReplays`
đều đang làm bằng cách normalize trước rồi so `JSON.stringify`.
Điều này đủ thực dụng cho snapshot cỡ vừa, nhưng cần lưu ý:
- không tối ưu cho dữ liệu rất lớn
- phụ thuộc vào tính ổn định của thứ tự mảng sau normalize
## 9. State được persist vào localStorage
Hiện editor chỉ persist hai nhóm nhỏ:
- background layer visibility
- key: `uhm.backgroundLayerVisibility.v1`
- map projection
- key: `uhm:mapProjection`
Editor hiện không persist toàn bộ draft/project snapshot vào localStorage.
Nếu cần autosave local draft, đó là tính năng phải làm thêm, không phải behavior hiện tại.
## 10. Khi nào state bị reset
### Reset toàn phần
Xảy ra khi:
- mở project khác
- mở lại project
- restore commit
Hiệu ứng:
- `baselineFeatureCollection` đổi
- `useEditorState()` reset `draft`
- `undoStack` bị clear
- baseline map được build lại
### Reset cục bộ
- đổi selection có thể reset `geometryMetaForm`
- đóng/mở wiki modal không reset snapshot wiki, chỉ reset form local của modal
## 11. Một số giới hạn hiện tại cần nhớ khi đọc code
-`undo`, chưa có `redo`
- timeline state có `timelineYear`, nhưng page hiện dùng `timelineDraftYear` cho filtering
- dirty count của commit không tương ứng một-một với số mutation backend
- map selection, bound_with filter và timeline filter đều là state client-side
- trạng thái orphan/time/timeline trong `GeometryBindingPanel` là derived từ draft + visibility, không phải field persist riêng