feat(studio): 三维视图接入产品(阶段 4:界面、生命周期与记录隔离)
- What: `Scene3DView` 装配(widget/相机/渲染器/合成器/协调器/适配器),`asset_source` 只查找并后台加载已编译的 `.oryscene` 包,`scene3d/prefs` 持久化并**修复**显示设置, `ui/mesh3d_panel` 用真实容器替换占位标签(无 GL/无包/包被拒/传感器不匹配各显示一行原因), `main.py` 在 `QApplication` 之前选定 GL profile,`app_window` 接线:3D 作为采集线程上的 第二个 sink、`_on_run_tick` 驱动 `tick()`、视图切换与 `closeEvent` 释放、preferences 落盘。 - Why: 阶段 4 的验收要求"三维开关不影响采集/记录/力曲线",并把阶段 1 留下的 "开/关三维 CSV 一致"这条补上;同时把"没有 context"从"看起来像在工作"变成可显示的终态。 - Verification: app 侧 test_scene3d_view 28 项、test_scene3d_gl_smoke 14 项(真实 GPU)、 adapters 17 项、skeleton 4 项、stage1 24 项、coordinator 13 项、ui_smoke 86 项; shared 侧 contract 26 / assets 33 / package 19 / live 25;`ui_check.py` 566 项检查 0 失败 (新增 "3D pane" 段 12 项);`locale_check.py` 302 个字面 key 0 缺失、中英各 377 key 对齐; 记录隔离:同一 96 帧会话挂/不挂 3D 消费者,`forces.csv` 逐字节相同、有效样本数与记录问题数相同; 生产者预算:合成器放慢到 50 ms 时 `submit_packet` p99 < 1 ms。 - Not in this change: 示意位移(`displacement_sensor_m`)仍未接入(需 SDK lift 与已确认尺度); ARM 板卡适配(按用户要求暂缓);包签名/AES-GCM 与长时性能报告(阶段 5); 仓库中仍未提供 `.oryscene` 包(发布产物,且其单位与变换必须由人声明)。
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
配套文档:`studio-3d-design.md`(规格)、`studio-3d-contracts.pyi`(接口骨架)。
|
||||
本文只回答一件事:**按什么顺序做,每一阶段做完拿什么验收**。
|
||||
|
||||
阶段 0 已经完成(骨架,见下);阶段 1 已完成(开发机 PoC,见下);阶段 2 起逐段实现。
|
||||
阶段 0 已经完成(骨架,见下);阶段 1 已完成(开发机 PoC,见下);阶段 2、3、4 已完成(见各节);阶段 5 待做。
|
||||
每个阶段结束时必须同时交出:实现、公共接口单测、输入样例、失败样例、兼容说明、平台诊断报告(规格 §13)。
|
||||
|
||||
原则(写下来是因为后面每阶段都会被诱惑违反一次):
|
||||
@@ -75,7 +75,7 @@ python tools/scene_assets/probe_gl.py --profile desktop33 --output gl-report.jso
|
||||
7. `grabFramebuffer()` 是 **BGRA**(小端)+ 有 stride,测试里读像素必须转换与裁剪,否则所有颜色通道判断都是错的。
|
||||
|
||||
**记录隔离**:本阶段尚未接入 `app_window`,因此三维对采集/记录完全惰性(无模块被应用导入);
|
||||
"开/关三维的 CSV 行序一致"这条验收随阶段 4 的接线一起做。
|
||||
"开/关三维的 CSV 行序一致"这条验收随阶段 4 的接线一起做(见阶段 4,已通过)。
|
||||
|
||||
**明确不做**:ARM 板卡适配(按用户要求暂缓),但 `probe_gl.py` 可直接在板上跑出报告并据此定 profile。
|
||||
|
||||
@@ -153,26 +153,101 @@ sha256 + 原子写入 + 读取时先验签再相信)、两个工具(`validat
|
||||
4. `assets.py` 里还留着与 `package.py`/`legacy.py` 同名的**桩类**——阶段 2 实现后它们就成了会误导集成者的重复定义;
|
||||
现在 `assets.py` 是门面(re-export + `SceneCompiler`,加密请求明确拒绝并指向阶段 5)。
|
||||
|
||||
**明确未做**:手势/面板(阶段 4);**示意位移的适配器接入**留到阶段 4——需要 SDK 的 lift 与一个已确认的尺度,
|
||||
现在适配器一律 `displacement=None`(宁可只着色,也不给未标定的毫米)。
|
||||
**明确未做**:手势/面板(阶段 4);**示意位移的适配器接入**仍未做——需要 SDK 的 lift 与一个已确认的尺度,
|
||||
现在适配器一律 `displacement=None`(宁可只着色,也不给未标定的毫米)。留到阶段 5,
|
||||
理由见阶段 4"明确未做"。
|
||||
|
||||
---
|
||||
|
||||
## 阶段 4:Qt 产品接入(界面与生命周期)
|
||||
## 阶段 4:Qt 产品接入(界面与生命周期)—— 已完成
|
||||
|
||||
**做**:`Sensor3DWidget` 完整、`ThreeDPanel`(开关/预设/复位/外壳/倍率/色标/状态)、
|
||||
`OrbitCamera` 全部交互、preferences 持久化、`app_window` 装配(视图切换、`closeEvent`)、
|
||||
`main.py` 在 `QApplication` 之前的 profile 选择。
|
||||
**做**:`scene3d/view.py`(`Scene3DView`:widget + 相机 + 渲染器 + 合成器 + 协调器 + 适配器的装配)、
|
||||
`scene3d/asset_source.py`(场景包查找与**后台线程**加载)、`scene3d/prefs.py`(显示设置的持久化与**修复**)、
|
||||
`ui/mesh3d_panel.py`(真实容器,替换占位标签)、`main.py` 在 `QApplication` 之前的 profile 选择、
|
||||
`app_window` 装配(视图切换、采集线程 sink、运行定时器、`closeEvent`、preferences)、`gl_backend` 的自动色标。
|
||||
|
||||
**验收**:
|
||||
**接线方式(决定性能与隔离的三件事)**:
|
||||
|
||||
| 用例 | 通过标准 |
|
||||
1. **生产者侧只有一次装箱**:3D 是 worker 上的**第二个 sink**(`add_sink(self._on_packet_for_3d)`),
|
||||
在**采集线程**上把一帧适配成只读 `SurfaceFieldFrame` 并投进容量 1 的 mailbox 后立刻返回。
|
||||
开关三维不增删 sink(增删本身就是状态机),只翻 `view._enabled`——所以"关掉三维"与"从不打开"在采集线程上
|
||||
是同一条路径。
|
||||
2. **没有第二个定时器**:`_on_run_tick()`(16 ms)调用 `mesh3d_panel.tick()`;`Scene3DView` 自己不建
|
||||
线程、不建定时器。合成跑在 view 自己的单线程 executor 上,且同时只有一次在飞。
|
||||
3. **资产只读一次、只读一个包**:`asset_source` 只找**已编译**的 `.oryscene`(顺序:`prefs["scene3d_package"]`
|
||||
→ `config/sensors/<sensor>/scene/*.oryscene`)。**不在运行时导入 Blender 导出**:那需要人给出
|
||||
`meters_per_source_unit` 与刚体变换,程序替人猜一个"1 源单位 = 1 毫米"会画出一个看着对、实际错一个系数的
|
||||
模型。找不到包时面板显示查找目录与 `tools/scene_assets/compile_scene.py` 的用法,二维照常。
|
||||
|
||||
**验收(已通过)**:
|
||||
|
||||
| 用例 | 结果 |
|
||||
|---|---|
|
||||
| 交互 | 旋转/平移/缩放/复位/预设;观察相机变化不改算法相机、标定与力轴(矩阵断言) |
|
||||
| 单位与质量显示 | `approximate` 位移显示为"相对形变示意 ×N",不出现在毫米文案里 |
|
||||
| 视图切换 | 三维开关不改变采集、记录与力曲线状态;切换 20 次无泄漏 |
|
||||
| 关停 | `closeEvent` → `release_resources` 幂等;重复关闭不抛异常 |
|
||||
| 界面文案 | 新增 key 中英齐全(沿用 `tools`/locale 校验方式) |
|
||||
| 交互 | `test_scene3d_view.py::test_panel_controls_do_not_touch_the_algorithm_side`:旋转/平移/缩放后 view 矩阵变化,且仍是刚体(det=+1、R·Rᵀ=I);单位、二维视图模式、箭头旋转、视图预设、传感器、splitter 尺寸、样本数**逐项不变** |
|
||||
| 单位与质量显示 | 面板图例由**帧自带的** descriptor 生成:`flow_magnitude [px] · 相对形变示意`;面板里没有任何毫米文案,`approximate` 位移只以"×N"出现(`display_illustrative` 诊断由合成器给出) |
|
||||
| 视图切换 | 菜单项与面板复选框是**同一个 flag**:切换 20 次后菜单、窗口、面板三者一致,且 `pane.view` 仍是同一个对象(未重建);`ui_check` 的 "3D pane" 段同样断言 |
|
||||
| 关停 | `closeEvent → mesh3d_panel.close_view() → view.close()`:`release_resources` 幂等、`renderer.part_ids` 清空、重复 close 不抛异常(真实 context 与 offscreen 各一例) |
|
||||
| 界面文案 | 新增 `scene3d.*`(29 项)与 `panels.mesh3d_*`(5 项):`locale_check.py` 302 个字面 key **0 缺失**,中英各 377 key 完全对齐 |
|
||||
| **记录隔离** | `test_the_3d_consumer_cannot_change_the_record`:同一 96 帧会话跑两遍(挂一个会读 packet 的消费者 / 不挂),`forces.csv` **逐字节相同**、有效样本数相同、记录问题数相同 |
|
||||
| 生产者预算 | `test_scene3d_gl_smoke.py::test_the_producer_side_never_waits_for_the_gpu`:合成器放慢到 50 ms,`submit_packet`(含适配器的 `|flow|` 计算)p99 **< 1 ms**、峰值 < 5 ms,显示丢帧 > 0 |
|
||||
| 装配后的真实渲染 | 同一文件:真 context 下 `View` 装配完成后,packet → 画面(`state` 进入 `live/no_data`、有非背景像素)、自动色标用帧内范围(与固定 0–1000 的"整片同色"对比) |
|
||||
| 无 context 的机器 | `test_a_platform_without_a_context_says_so_instead_of_showing_an_empty_view`:Qt 在 offscreen 下**从不调用** `initializeGL`,widget 在首次 `paintEvent` 问 `context() is None` → `gl_unsupported` → 面板显示原因而不是空画面 |
|
||||
|
||||
数量:`test_scene3d_view.py` 28 项(无 GPU)、`test_scene3d_gl_smoke.py` 14 项(真实 context;offscreen 自动 skip)、
|
||||
`test_scene3d_adapters.py` 17 项、`test_scene3d_skeleton.py` 4 项、`test_scene3d_stage1.py` 24 项、
|
||||
`test_scene3d_coordinator.py` 13 项、`test_ui_smoke.py` 86 项(含阶段 4 的开关与隔离用例)——
|
||||
apps/studio 全部 **191 项**;shared 侧 contract 26 / assets 33 / package 19 / live 25(3D 共 103 项),
|
||||
shared 全部 **262 项**。`ui_check.py` **566 项检查 0 失败**(新增 "3D pane" 段 12 项)。
|
||||
|
||||
**这一阶段抓到的**:
|
||||
|
||||
1. **"没有 context"在 Qt 里不是一个错误,是"什么都没发生"**:offscreen 插件下 `initializeGL` 根本不会被调用,
|
||||
于是 widget 一直处于"可用但还没数据"的状态,面板显示参考模型——**看起来像在工作**。现在首次
|
||||
`paintEvent` 之后检查 `context() is None`,把"没有上下文"变成一个可显示的终态诊断(`gl_unsupported`),
|
||||
`Mesh3dPanel` 只在 `gl_unsupported`/`shader_compile_failed` 时用文字替换画面,单帧被拒(域/几何不匹配)
|
||||
时仍然保留画面。
|
||||
2. **手指的 flow 已经在 UV 域里,不需要(也不能)再 remap**:实测 `camera.resolution_stitch=[280,300]`
|
||||
→ `Sensor.flow` 形状 `(300,280)`,与场景域 `(300,280)` 完全相同(flow 是对**展开后**图像算的)。
|
||||
而 `unroll_map_uv_s0.4.npz` 的 `map_x/map_y` 是**源图像**像素(形状同为 300×280,但 x 最大 510.98)。
|
||||
把它套在域网格的 flow 上就是双重 remap:x 方向会越界读、y 方向**静默读错行**。已被单测钉住
|
||||
(`test_the_shipped_map_cannot_be_applied_to_the_shipped_flow`),view 一律传 `map_x=None`。
|
||||
3. **手指适配器忽略了设备自己的 `valid_mask`**:UV 岛之外追踪器看到的是黑边,那些值不是测量。现在与
|
||||
box 适配器共用一处 `_intersect_device_mask`(形状不符的 mask 被忽略而不是被拉伸)。
|
||||
4. **`ColorScale.mode="automatic"` 在合同里存在、在渲染器里没人用**:固定 0–1 的色标画"px"量级会整片饱和。
|
||||
现在 `update_surface` 记下**有效且有限**样本的范围,`draw` 在 automatic 模式下用它;固定模式仍按操作者的
|
||||
上下限(跨帧可比)。同时修好"常量场/NaN/上限倒挂"三类色标:范围恒可用,绝不出现 NaN 或零跨度。
|
||||
5. **持久化修复本身会把值弄出界**:`scene3d.prefs` 把 `upper<=lower` 修成 `lower+1`,当 `lower` 恰好被
|
||||
夹在 `MAX_SCALE_BOUND` 上时就变成 `上限+1`。现在加宽后再夹一次(并在参数化用例里覆盖 9 种手改坏值)。
|
||||
6. **面板布局会把自己的控件挤掉**:内容最小宽度 317 px,而窗口为 3D 槽预留 240 px;面板变矮时 GL widget
|
||||
被分到 **0 高度**(等于什么都不画)。现在字段紧凑化、色标上下限单独一行(最小宽度降到 232 px,英文界面
|
||||
加样式表后 241 px——所以预留宽度定为 **260** 并由测试钉住),并给画面 120 px 最小高度。
|
||||
7. **外壳复选框的初值与 `DisplaySettings.housing_visible` 的默认值不一致**(未勾选 vs `True`):面板
|
||||
"静息状态"和它上报的设置必须是一件事。
|
||||
8. **一个 flag 两个控件必须防环**:菜单动作与面板复选框互相驱动,靠"值相同就返回"断环;
|
||||
20 次切换后三者一致、view 不重建(`ui_check` 与单测各一处)。
|
||||
9. `preferences.default.json` 与 `prefs/store.py::_DEFAULTS` 的 key 必须逐一相同(`test_locales.py` 检查):
|
||||
在 `_DEFAULTS` 里写**嵌套字面量**会让内层 key 被当成顶层偏好,因此 `scene3d_display` 的默认值放在
|
||||
模块级常量里。
|
||||
|
||||
**兼容说明**:
|
||||
|
||||
- 运行时**只**读 `.oryscene` 包;旧的 Blender 导出目录仍由 `LegacyFingerImporter`(离线工具/测试)导入,
|
||||
单位与变换必须显式给出。包缺失/被拒/无 GL 时:面板一行文字说明原因,采集、记录、力曲线、标定、回放全部照常
|
||||
(`ui_check` 566 项与 `test_ui_smoke.py` 全绿即为证)。
|
||||
- `gl_profile`(`desktop33` 默认 / `gles30`)在 `QApplication` 之前生效,一进程一个;未知取值退回默认而不是
|
||||
让窗口起不来。
|
||||
- 3D 的显示设置(放大、色标、外壳、显示帧率)持久化在 `scene3d_display`;**色标的 descriptor 不恢复**,
|
||||
永远来自当前帧,因此一个旧文件不可能把测量标成另一种量。
|
||||
|
||||
**明确未做(留给阶段 5 或按用户要求暂缓)**:
|
||||
|
||||
- **示意位移(`displacement_sensor_m`)仍未接入**:需要 SDK 的 lift 与一个已确认的空间尺度;在拿到
|
||||
"这个尺度是怎么标定出来的"之前,适配器继续只着色不位移(阶段 3 的原则不变)。
|
||||
- ARM 板卡适配:按用户要求暂缓;`probe_gl.py` 可在板上直接跑出报告,据此定 `gl_profile` 与
|
||||
`MIN_*`/预算(阶段 5 的两平台报告)。
|
||||
- `session.json` 的 `visualization3d` 能力块、包签名/AES-GCM、性能与长时报告:阶段 5。
|
||||
- 3D 包尚未随仓库提供(仓库里没有 `.oryscene`):这是**有意的**,包是发布产物(阶段 5 管签名与平台锁定),
|
||||
并且它必须携带只能由人声明的单位与变换。
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user