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:
@@ -37,6 +37,7 @@ from orisys_app_shared.scene3d.contracts import (
|
||||
UNAVAILABLE_DESCRIPTOR,
|
||||
)
|
||||
from orisys_app_shared.scene3d.package import write_package
|
||||
from orisys_studio.app_window import MIN_PANEL_WIDTHS
|
||||
from orisys_studio.scene3d import prefs as scene3d_prefs
|
||||
from orisys_studio.scene3d.asset_source import (
|
||||
PACKAGE_SUFFIX,
|
||||
@@ -45,7 +46,12 @@ from orisys_studio.scene3d.asset_source import (
|
||||
load_package,
|
||||
no_asset_diagnostic,
|
||||
)
|
||||
from tests.test_ui_smoke import _finish, _running_window
|
||||
from tests.test_ui_smoke import ( # noqa: F401 (fixtures are imported by name)
|
||||
_finish,
|
||||
_running_window,
|
||||
record_root,
|
||||
replay_recording,
|
||||
)
|
||||
|
||||
#: Scratch root for the two-session comparison. One subdirectory per run, because
|
||||
#: the point of the test is that two recordings are identical, not that one file
|
||||
@@ -112,9 +118,18 @@ def test_a_box_sensor_reports_that_it_has_no_3d_scene(qt_app):
|
||||
panel.deleteLater()
|
||||
|
||||
|
||||
def test_a_missing_package_names_where_it_looked(tmp_path, qt_app):
|
||||
def test_a_missing_package_names_where_it_looked(tmp_path, qt_app, monkeypatch):
|
||||
"""With no package anywhere, the pane names the directory it looked in.
|
||||
|
||||
The search is stubbed rather than relied on: the repository may or may not carry
|
||||
a compiled package (the application's own config directory is where one belongs),
|
||||
and a test that only passes on a machine without one is a test that fails the
|
||||
first time somebody installs the feature.
|
||||
"""
|
||||
import orisys_studio.ui.mesh3d_panel as panel_module
|
||||
from orisys_studio.ui.mesh3d_panel import Mesh3dPanel
|
||||
|
||||
monkeypatch.setattr(panel_module, "find_package", lambda *a, **k: None)
|
||||
panel = Mesh3dPanel()
|
||||
try:
|
||||
panel.set_sensor("finger", package_override=tmp_path / "nope.oryscene")
|
||||
@@ -164,17 +179,21 @@ def test_the_pane_fits_the_width_the_window_reserves_for_it(qt_app):
|
||||
view.close()
|
||||
|
||||
|
||||
def test_a_platform_without_a_context_says_so_instead_of_showing_an_empty_view(qt_app):
|
||||
def test_a_platform_without_a_context_says_so_instead_of_showing_an_empty_view(
|
||||
write_poc, qt_app
|
||||
):
|
||||
"""The offscreen plugin cannot create a context; that must not look like "no data".
|
||||
|
||||
Qt reports this by never calling ``initializeGL``, so the widget asks Qt the one
|
||||
question that has an answer — is there a context at all — on its first paint.
|
||||
question that has an answer — is there a context at all — on its first paint. The
|
||||
scene comes from a package written for this test, so the case does not depend on
|
||||
whether the repository happens to carry a compiled one.
|
||||
"""
|
||||
from orisys_studio.ui.mesh3d_panel import Mesh3dPanel
|
||||
|
||||
panel = Mesh3dPanel()
|
||||
try:
|
||||
panel.set_sensor("finger")
|
||||
panel.set_sensor("finger", package_override=write_poc())
|
||||
view = panel.ensure_view()
|
||||
if view is None:
|
||||
pytest.skip("the pane did not build a view here")
|
||||
@@ -184,7 +203,10 @@ def test_a_platform_without_a_context_says_so_instead_of_showing_an_empty_view(q
|
||||
panel.resize(240, 400)
|
||||
panel.show()
|
||||
view.show()
|
||||
qt_app.processEvents()
|
||||
panel.set_enabled(True)
|
||||
assert _pump(qt_app, lambda: view.assets is not None, timeout_s=15.0), (
|
||||
"the test package never loaded"
|
||||
)
|
||||
widget = view.gl_widget
|
||||
for _ in range(5):
|
||||
widget.repaint()
|
||||
@@ -584,6 +606,73 @@ def test_a_recording_says_what_the_3d_pane_was(
|
||||
window.close()
|
||||
|
||||
|
||||
def test_the_replay_view_shows_the_pane_and_says_where_the_field_comes_from(
|
||||
make_window, replay_recording, qt_app
|
||||
):
|
||||
"""History browsing gets the pane, fed by the recompute pass — and says so.
|
||||
|
||||
A recording stores the raw camera image and the forces, not a per-frame
|
||||
deformation field, so the pane in replay is either the surface model (no field) or
|
||||
what a recompute pass produces from the video. Those are two different claims and
|
||||
the label has to distinguish them.
|
||||
"""
|
||||
from tests.test_ui_smoke import _show
|
||||
|
||||
window = _show(make_window(sensor_type="finger"), (1440, 880))
|
||||
try:
|
||||
assert window._load_recording(Path(replay_recording))
|
||||
assert window._view_preset == "replay"
|
||||
pane = window.mesh3d_panel
|
||||
assert pane.isVisible(), "the 3D pane is hidden in the replay view"
|
||||
assert pane.enabled(), "the pane is visible but not switched on"
|
||||
assert window.content_splitter.sizes()[0] >= MIN_PANEL_WIDTHS["mesh3d"], (
|
||||
"the replay layout left the pane at zero width"
|
||||
)
|
||||
view = pane.view
|
||||
if view is None or view.panel is None:
|
||||
pytest.skip("no 3D view on this platform")
|
||||
# Wait for the scene to load, then read what the pane claims.
|
||||
assert _pump(qt_app, lambda: view.assets is not None, timeout_s=15.0), (
|
||||
"the scene never loaded: " + str(pane.last_diagnostic)
|
||||
)
|
||||
label = view.panel.source_label.text()
|
||||
assert "回放" in label or "Replay" in label, label
|
||||
|
||||
# A running pass means the pixels are recomputed, and the label must say so.
|
||||
window._recompute_running = True
|
||||
window._apply_panel_visibility()
|
||||
label = view.panel.source_label.text()
|
||||
assert "重算" in label or "Recomputed" in label, label
|
||||
|
||||
# Stopping the pass must clear the picture, not leave the last frame up.
|
||||
window._recompute_running = False
|
||||
pane.show_reference_only()
|
||||
qt_app.processEvents()
|
||||
assert view.coordinator.state in ("no_data", "ready", "disabled")
|
||||
assert "没有测量值" in view.panel.legend_label.text() or (
|
||||
"No measurement" in view.panel.legend_label.text()
|
||||
), view.panel.legend_label.text()
|
||||
window._apply_panel_visibility()
|
||||
label = view.panel.source_label.text()
|
||||
assert "回放" in label or "Replay" in label, label
|
||||
finally:
|
||||
window.close()
|
||||
|
||||
|
||||
def test_the_live_view_still_says_live(make_window, qt_app):
|
||||
window = make_window(sensor_type="finger")
|
||||
try:
|
||||
window._apply_panel_visibility()
|
||||
view = window.mesh3d_panel.view
|
||||
if view is None or view.panel is None:
|
||||
pytest.skip("no 3D view on this platform")
|
||||
label = view.panel.source_label.text()
|
||||
assert "实时" in label or "Live" in label, label
|
||||
assert view.panel.source_label.toolTip()
|
||||
finally:
|
||||
window.close()
|
||||
|
||||
|
||||
def test_a_platform_locked_scene_is_refused_in_the_application(qt_app):
|
||||
"""A lock violation is a *pane* failure: the view refuses the scene and says why."""
|
||||
from orisys_app_shared.scene3d.contracts import PlatformInfo, PlatformLock
|
||||
|
||||
Reference in New Issue
Block a user