feat(web-platform): release V0.8.3 常规界面优化
This commit is contained in:
@@ -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 同时使用绿色/青色圆环、十字中心和高度位置表达真实接触面。
|
||||
@@ -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`
|
||||
@@ -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 时无需兼容层。
|
||||
Reference in New Issue
Block a user