feat(web-platform): release V0.8.3 常规界面优化
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled

This commit is contained in:
2026-09-04 15:34:24 +08:00
parent 63d67a645b
commit 72e27b5a6c
59 changed files with 4779 additions and 1585 deletions
+94
View File
@@ -0,0 +1,94 @@
# 编辑器快捷键与微交互样式规范
## 快捷键矩阵
`useMapEditorShortcuts` 在模型快照可用时以 capture 阶段统一处理;`F` 可聚焦机器人或地图选择,其余编辑命令受地图上下文约束,避免与浏览器保存、仿真重置以及输入框 Delete 冲突。
| 快捷键 | 行为 | 约束 |
| --- | --- | --- |
| `W` | 平移 Gizmo | 自动切回 Viewer `select` 模式 |
| `E` | 旋转 Gizmo | 仅世界 Z 轴 |
| `R` | 缩放 Gizmo | 参数地形及锁定对象会消费按键但不执行缩放 |
| `F` | 聚焦选中对象 | 320ms ease-out 相机过渡;用户拖动相机会中止过渡 |
| `Delete` | 删除选中地图对象 | 输入框和对话框内不拦截;忽略 key repeat |
| `Ctrl/Cmd + S` | 保存并编译场景草稿 | 阻止浏览器“保存网页”;编译失败保留草稿 |
| `G` | 切换网格吸附 | 移动 0.1m / 旋转 5° |
| `Ctrl/Cmd + Z` | 撤销 MapEditSession 操作 | 已有会话行为 |
| `Ctrl/Cmd + Y` | 重做 MapEditSession 操作 | 已有会话行为 |
| `Escape` | 取消对象选择 | 已有会话行为 |
典型接入:
```tsx
useMapEditorShortcuts({
enabled: Boolean(snapshot),
mapEditing: mapEditingActive,
dirty: mapSceneDirty,
loading,
hasSelection: Boolean(selection),
canDelete: selection?.kind === 'map-object',
canScale: selectedObject?.placementMode !== 'locked',
onTransformMode: changeMapTransformMode,
onFocusSelection: () => viewer.focusSelection(),
onDeleteSelection: () => sessionPort.onDelete(objectId),
onToggleSnapping: () => setSnapping((value) => !value),
onSave: commitMapScene,
});
```
## Scrubbable Number Input
`ScrubbableNumberInput` 同时保留标准 `<input type="number">` 与标签拖拽:
- 在标签上左右拖拽:`每 4px = 1 step`;
- `Shift`:0.1 倍步长;
- `Alt`:0.01 倍步长;
- `Ctrl/Cmd`:10 倍步长;
- 标签获得焦点时可用左右方向键调节;
- min/max 在组件边界统一约束;
- 拖拽过程中使用等宽数字、accent 边框和低强度辉光。
适用于地图位姿、尺寸、材质、摩擦、出生点,以及仿真 Actuator 参数。
## 视觉层级
### 色彩与材质
- 页面底色:`--ui-bg`,暗色模式保持蓝黑而非纯黑;
- 一级面板:`--ui-panel`;
- 二级表面:`--ui-surface`;
- 浮层玻璃:`--ui-glass`,透明度约 78%;
- 强调色只用于选择、有效落点与可提交状态;
- 警告色只用于未保存草稿和物理诊断。
### 公共样式类
```css
.workbench-header /* 顶栏横向工程渐变 */
.engineering-glass /* 16px blur、微渐变、内高光、细边框 */
.engineering-card /* Inspector 二级属性组 */
.engineering-badge /* 微渐变 Badge 与细阴影 */
.tool-active-glow /* 激活工具的低强度 accent 光晕 */
.map-tool-enter /* 浮动工具条入场 */
.draft-dirty-dot /* 未保存状态呼吸提示 */
.scrubbable-number-* /* 数值拖拽完整状态 */
```
优先使用这些语义类,组件内部再组合 Tailwind 布局类。例如:
```tsx
<div className="engineering-glass map-tool-enter rounded-xl border p-1.5" />
<section className="engineering-card rounded-lg border p-2.5" />
```
## 动效时长
| 场景 | 时长 | 曲线 |
| --- | ---: | --- |
| Button hover / press | 150ms | `ease-out` |
| 浮动工具条出现 | 220ms | `cubic-bezier(.22,1,.36,1)` |
| 相机聚焦 | 320ms | cubic ease-out |
| Snap to Surface 3D 指示器 | 720ms | 双脉冲衰减 |
| 草稿状态呼吸 | 1800ms | ease-in-out |
所有动画必须受 `prefers-reduced-motion: reduce` 约束,不以动画作为唯一状态表达。Snap to Surface 同时使用绿色/青色圆环、十字中心和高度位置表达真实接触面。
+129
View File
@@ -0,0 +1,129 @@
# 地图编辑交互与状态流
## 目标
地图编辑采用“视口负责空间意图、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`
+64
View File
@@ -0,0 +1,64 @@
# 3D 编辑器侧边栏信息架构
## 目标布局
```text
┌ 场景大纲与资产中心 ┐ ┌──────── 3D Viewport ────────┐ ┌ 右侧工作区 ──┐
│ 场景大纲(常驻) │ │ │ │ 检查器/控制/数据│
│ ├─ Robot │ │ │ │ 上下文属性 │
│ │ ├─ Body │ │ │ │ │
│ │ └─ Joint │ │ │ │ │
│ └─ Maps │ │ │ │ │
│ └─ Object │ │ │ │ │
├────────────────────┤ │ │ │ │
│ 资产库 | 工程文件 │ │ │ │ │
└────────────────────┘ └──────────────────────────────┘ └──────────────┘
```
- 场景大纲常驻,资产库与工程文件共享下半区,放置地图时无需在“结构/资产”之间切换。
- 右侧使用“检查器 / 控制台 / 数据录制”三个顶层视图;检查器内部根据统一选择自动路由到 Body、Joint、地图实例或地图对象。
- Python/ONNX 控制器、全局 Actuator/Joint 控制和数据录制不是对象属性,放在右侧独立视图中,不使用模态窗口,因此操作时始终能看到并交互仿真画面。
- 控制与训练等重型组件仅在首次进入对应右侧视图时懒加载。
- 控制台分区默认全部折叠,按“执行器控制 → 关节调试 → 外力交互 → Python 控制 → ONNX 推理 → 强化学习训练”排列,并在标题右侧显示“控制 / 调试 / 交互 / 推理 / 训练”功能标记。
## 状态与依赖边界
`EditorSelection` 是大纲高亮和检查器路由的唯一选择语义,只保存稳定 ID:
```ts
type EditorSelection =
| { kind: 'body'; bodyId: number }
| { kind: 'joint'; jointId: number; bodyId: number }
| { kind: 'map'; mapAssetId: string }
| { kind: 'map-object'; mapAssetId: string; objectId: string };
```
Viewer 的射线命中详情仍保留为临时 `ViewerSelection`,仅用于展示 geom 和世界位置,不再决定检查器类型。
组件职责:
- `ProjectSidebar`:只编排常驻 `SceneOutliner`、`MapAssetLibrary` 和 `ProjectTree`。
- `SceneOutliner`:把 simulation 与 map 数据投影成一棵可选择的场景树;不修改业务状态。
- `MapAssetLibrary`:只负责资产发现、点击添加和拖放,不再保存已放置实例树。
- `ModelControlsSidebar`:持有右侧栏壳层、顶层视图切换,并在检查器视图中按 `EditorSelection.kind` 路由。
- `RobotInspector`:承载模型摘要、Body 和 Joint 的领域 UI。
- `PhysicalMapPanel`:承载地图实例参数与地图对象精确属性,不再持有视口工具状态。
- `MapViewportToolbar`:在视口内统一承载 Transform、网格吸附与贴地检测,并与顶部 `ViewerToolDock` 共享交互状态。
- `MapDraftStatusOverlay`:常驻视口底部,统一显示改动计数以及场景级提交/丢弃入口。
- `WorkspaceToolsPanel`:懒加载并在右侧栏内承载控制器、全局仿真控制和数据录制。
所有选择入口遵循同一事件流:
```text
场景树点击 / Viewer 拾取
↓
App 更新 EditorSelection(稳定 ID)
↓
SceneOutliner 高亮 + Inspector 自动路由
↓
领域面板发出 command callback
↓
App / MapEditSession / SimulationSession 执行修改
```
该实现只使用 React hooks、受控属性和 Tailwind class,兼容 React 18;当前工程使用 React 19 时无需兼容层。