# 地图编辑交互与状态流 ## 目标 地图编辑采用“视口负责空间意图、Inspector 负责精确属性、`MapEditSession` 负责草稿事实”的单向数据流: - 顶部 `ViewerToolDock` 决定当前 Viewer 交互域;地图上下文激活时与视口浮动工具条共享状态。 - `MapViewportToolbar` 负责移动/旋转/缩放、网格吸附以及贴地策略,不再由右侧属性面板持有。 - 右侧 Inspector 仅显示当前对象的位姿、尺寸、表面材质和摩擦力。 - `MapDraftStatusOverlay` 常驻视口底部,统一提交或丢弃场景级草稿。 - 左侧资产库通过标准化拖放载荷把几何原语直接放到 3D 地面。 ## 状态所有权 | 状态 | 所有者 | 消费方 | | --- | --- | --- | | `EditorSelection` | `App` | 场景树、Viewer、Inspector、地图工具条 | | `mapTransformMode` | `App` | `ViewerToolDock`、`MapViewportToolbar`、`MapEditorLayer` | | `mapSnapping` | `App` | `MapViewportToolbar`、`MapEditorLayer`、参数地形预览层 | | `assetPlacementMode` | `App` | 左侧资产库、视口贴地策略 | | 编辑文档与 undo/redo | `MapEditSession` | `MapEditorPanel`、视口命令端口 | | `editorDrafts` | `App` | 场景预览、事务提交、草稿状态浮窗 | | `editorSessionStates` | `App` | 未保存改动计数 | ## MapEditSession 命令端口 `MapEditorInteractionCallbacks` 是 Viewer 与编辑会话之间的边界: ```ts interface MapEditorInteractionCallbacks { onSelect(id: string | null): void; onTransform(transform: MapEditorTransform): void; onAddAsset(type, position?, placementMode?, externalSupportTop?): void; onSetPlacementMode(id: string, mode: MapObjectPlacementMode): void; onAlignToSurface(id: string): void; onDelete(id: string): void; onDiscard(): void; } ``` Viewer 和浮动工具条只发送命令,不直接修改 `EditableMapDocument`。每次会话变更都按以下顺序发布: 1. `MapEditSession.addAsset/update/remove` 创建历史记录; 2. `onPreview(session.document)` 更新 `MapEditorLayer`; 3. `onDraftChange(..., dirty)` 写入 App 的 `editorDrafts`; 4. `onSessionStateChange` 上报 dirty、changeCount、undo/redo 能力; 5. React 根据统一选择刷新 Inspector 与底部草稿状态。 ## 关键流转 ### 视口拾取与属性编辑 ```text MapEditorLayer.onSelect(objectId) → App.editorInteraction.onSelect(objectId) → App.EditorSelection = map-object → Inspector 定位同一 draftDocument.objects[objectId] → MapObjectInspector 显示位姿 / 尺寸 / 表面材质 / 摩擦 ``` Inspector 输入不会调用 Viewer API,而是更新 `MapEditSession`,再由草稿预览反向更新 Viewer,避免双写。 ### 浮动变换工具 ```text MapViewportToolbar(W/E/R) → App.mapTransformMode → Viewer interaction mode 自动切到 select → MuJoCoViewer.setMapEditorTransformMode → MapEditorLayer + ParametricMapPreviewLayer 同步 ``` 网格吸附同样由 App 单点设置为移动 `0.1 m`、旋转 `5°`。切换顶部关节拖动或外力模式时,地图工具仍保留上下文,但标记为非激活;点击顶部地图上下文或任一 W/E/R 会恢复选择模式。 贴地策略: - `auto_ground`:对象底部对齐世界 `z=0`; - `gravity`:调用场景承载面查询,沿 `-Z` 落到当前 XY 位置的最高表面; - `locked`:禁止位姿和 Gizmo 变换,但允许尺寸、材质和摩擦编辑。 ### 从资产库拖放到 3D 地面 ```text MapAssetLibrary dragStart → application/x-mujoco-map-library-item → App.dragOver → MuJoCoViewer.mapPlanePoint(clientX, clientY) → MapAssetDropIndicator → drop → editorInteraction.onAddAsset(...) → MapEditSession.addAsset ``` 若当前还没有可编辑地图,`App.createEditableScene` 会创建临时 V3 地图包,再将待放置资产交给新会话。重力放置会把地面射线落点与 `mapSceneSurfaceHeightAt` 的承载高度一起传给会话。 ### 提交与丢弃 提交: ```text MapDraftStatusOverlay.commit → App.commitMapScene(editorDrafts) → materializeEditableMapDrafts(revision 递增) → 一次 MuJoCo 编译 → 成功:更新 appliedMapAssets / committedEditorDocuments / 清理已提交草稿 → 失败:回滚 manifest,保留草稿与上一仿真会话 ``` 丢弃: ```text MapDraftStatusOverlay.discard → MapEditSession.discard(清理本地 history) → App.clearEditorDrafts → restoreAppliedMapScene → 恢复已提交 Viewer 预览与 Inspector 选择 ``` 提交和丢弃均作用于整个场景草稿,因此多个地图实例和编辑文档只触发一次物理重新编译。 ## 组件更新 - `web_platform/src/app/components/MapViewportTools.tsx` - `web_platform/src/app/components/ViewerToolDock.tsx` - `web_platform/src/map/MapObjectInspector.tsx` - `web_platform/src/map/MapEditorPanel.tsx` - `web_platform/src/map/PhysicalMapPanel.tsx` - `web_platform/src/map/MapAssetLibrary.tsx` - `web_platform/src/app/App.tsx`