feat(studio-3d): 历史回放里的三维面板 —— 由「重算」通道喂它,并标注来源
- What: `VIEW_SHOWS_3D` 加入 `replay`;`app_window` 新增 `_shows_3d()`(可见性与布局共用一个判据) 与 `_splitter_sizes_for()`(预设表里的 3D 槽是"二维布局"的描述,面板打开时从最宽的面板借 `DEFAULT_3D_PANE_WIDTH=340`);`ThreeDPanel` 新增第三个来源标签(实时采集 / 回放:录像不含三维场 / 重算通道:由录像重算,非记录值)与 `set_source_kind()`;`ThreeDCoordinator.show_rest()` 把参考模型 放回屏幕并停止主张测量值(重算停止时调用,否则最后一帧会留在屏幕上);`Mesh3dPanel` 转发这两者, 并新增"加载中"状态的修正:切换传感器再切回来时若该包已加载,直接显示已就绪的画面而不是一条永远 不会被替换的"正在加载"。 - Why: 录像里没有三维场(raw_*.mp4 是原始相机图像,`depth.stored=false`,CSV 只有力值),所以纯回放只能 显示参考模型;但「从视频重算力值」本来就把同一个运行路径指向录像的原始视频,而采集线程上的三维 sink 早已注册——因此重算一开始面板就会收到真实的形变帧。这两件事必须由界面区分:一个是"没有测量", 一个是"当前算法对这段录像会说什么",把后者当成记录值展示就是另一种 `display_illustrative`。 - Verification: apps/studio 全量通过(test_scene3d_view.py 32 项,含"回放里面板可见且标注正确""重算时 标注切换""停止重算后清回参考模型""实时视图标为实时";test_scene3d_coordinator.py 15 项,含 show_rest 与"停止时在飞的帧不会随后被显示");ui_check 570 项 0 失败(新增"已安装包必须被加载并标注来源"); locale_check 0 缺失;仓库内工程包经 validate_scene 全部 ok。 - Not in this change: 逐帧保存展开场/深度(记录格式变更,设计文档明确否掉);重算通道本身(已存在,本次 只是把它的帧接到面板上);示意位移(仍需 SDK lift + 已确认尺度)。
This commit is contained in:
@@ -70,8 +70,12 @@ class Mesh3dPanel(QFrame):
|
||||
self._source: Optional[SceneSource] = None
|
||||
self._sensor_type = ""
|
||||
self._expected: Optional[ExpectedGeometry] = None
|
||||
self._source_kind = "live"
|
||||
self._enabled = False
|
||||
self._loaded = False
|
||||
#: The package whose scene is in the view, so switching sensor away and back
|
||||
#: does not reload it (and does not pretend to be loading it either).
|
||||
self._loaded_path: Optional[Path] = None
|
||||
self._reported: Optional[Diagnostic] = None
|
||||
self._pending_settings = None
|
||||
self._message_kind = _UNAVAILABLE
|
||||
@@ -121,11 +125,22 @@ class Mesh3dPanel(QFrame):
|
||||
return
|
||||
self._source = find_package(self._sensor_type, override=package_override)
|
||||
if self._source is None:
|
||||
self._loaded_path = None
|
||||
self._reported = no_asset_diagnostic(self._sensor_type, override=package_override)
|
||||
self._show_message(
|
||||
_NO_ASSET, directory=str(self._reported.details["directory"])
|
||||
)
|
||||
return
|
||||
if (
|
||||
self._view is not None
|
||||
and self._loaded_path is not None
|
||||
and self._source.path == self._loaded_path
|
||||
):
|
||||
# This scene is already loaded (the sensor was switched away and back):
|
||||
# showing "loading…" again would leave a line nothing ever replaces, while
|
||||
# the view behind it is ready.
|
||||
self._stack.setCurrentWidget(self._view)
|
||||
return
|
||||
self._show_message(_LOADING, name=self._source.path.name)
|
||||
|
||||
def ensure_view(self) -> Optional[QWidget]:
|
||||
@@ -141,6 +156,10 @@ class Mesh3dPanel(QFrame):
|
||||
view.enabledChanged.connect(self._on_view_enabled)
|
||||
if self._pending_settings is not None and hasattr(view, "restore_settings"):
|
||||
view.restore_settings(self._pending_settings)
|
||||
# The kind was decided before the view existed (the window sets it when the
|
||||
# view preset changes), so it is pushed again here rather than lost.
|
||||
if hasattr(view, "set_source_kind"):
|
||||
view.set_source_kind(self._source_kind)
|
||||
if not view.available:
|
||||
self._show_message(_UNAVAILABLE, reason=view.reason)
|
||||
return view
|
||||
@@ -186,6 +205,7 @@ class Mesh3dPanel(QFrame):
|
||||
return
|
||||
if not view.use_scene(assets, source_id=description, expected=self._expected):
|
||||
return
|
||||
self._loaded_path = None if self._source is None else self._source.path
|
||||
self._stack.setCurrentWidget(view)
|
||||
# Adopting a scene is not the same as showing one: the pane stays off until
|
||||
# the operator (or the window, restoring a preference) turns it on.
|
||||
@@ -193,6 +213,7 @@ class Mesh3dPanel(QFrame):
|
||||
|
||||
def _on_load_failed(self, item: Diagnostic) -> None:
|
||||
self._reported = item
|
||||
self._loaded_path = None
|
||||
self._show_message(_REJECTED, message=item.message)
|
||||
|
||||
def _on_diagnostic(self, item: Diagnostic) -> None:
|
||||
@@ -239,6 +260,17 @@ class Mesh3dPanel(QFrame):
|
||||
def scene_path(self) -> Optional[Path]:
|
||||
return None if self._source is None else self._source.path
|
||||
|
||||
def set_source_kind(self, kind: str) -> None:
|
||||
"""Where the field comes from: live, replay (model only) or recompute."""
|
||||
self._source_kind = str(kind)
|
||||
if self._view is not None and hasattr(self._view, "set_source_kind"):
|
||||
self._view.set_source_kind(self._source_kind)
|
||||
|
||||
def show_reference_only(self) -> None:
|
||||
"""Clear a measurement that is no longer being produced (a stopped pass)."""
|
||||
if self._view is not None and hasattr(self._view, "show_reference_only"):
|
||||
self._view.show_reference_only()
|
||||
|
||||
def tick(self) -> None:
|
||||
"""Advance the pane. Called by the window's run timer, never by itself."""
|
||||
if self._view is not None and hasattr(self._view, "tick"):
|
||||
|
||||
Reference in New Issue
Block a user