From b1cd37148c50b374e5400f1c770dd33f0fc59954 Mon Sep 17 00:00:00 2001 From: ykcheungab Date: Wed, 7 Oct 2026 12:25:48 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=A0=E6=8E=89=E5=9B=9E=E6=94=BE=E7=9A=84?= =?UTF-8?q?=E4=BA=94=E4=B8=AA=E8=BD=AC=E5=8F=91=E3=80=81=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E7=9A=84=E7=A9=BA=E6=A0=BC=E5=BF=AB=E6=8D=B7=E9=94=AE=E4=B8=8E?= =?UTF-8?q?=E9=87=8D=E5=A4=8D=E7=9A=84=E7=AD=89=E5=BE=85=E5=BE=AA=E7=8E=AF?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 回放调用点直接走 replay_ctl,场景工具的路径引导收敛到 _paths,panel_snapshot 复用 panel_check 的 _wait_for,report_from 收成模块私有的 _report_from。 --- apps/autocal/tools/panel_snapshot.py | 18 ++++++----------- apps/studio/docs/shell-lifecycle.md | 4 ++-- .../src/orisys_studio/analysis_worker.py | 4 ++-- apps/studio/tests/test_replay_analysis.py | 4 ++-- apps/studio/tests/test_scene3d_view.py | 2 +- apps/studio/tools/ui_check.py | 12 +++++------ apps/studio/tools/ui_snapshot.py | 2 +- docs/architecture/studio-3d-stages.md | 10 +++++----- tools/scene_assets/_paths.py | 20 +++++++++++++++++++ tools/scene_assets/bench_scene.py | 12 +++++------ tools/scene_assets/compile_scene.py | 12 +++++------ tools/scene_assets/probe_gl.py | 16 +++++++-------- tools/scene_assets/soak_scene.py | 12 +++++------ tools/scene_assets/validate_scene.py | 12 +++++------ 14 files changed, 72 insertions(+), 68 deletions(-) create mode 100644 tools/scene_assets/_paths.py diff --git a/apps/autocal/tools/panel_snapshot.py b/apps/autocal/tools/panel_snapshot.py index d0b49ff..7e3fac2 100644 --- a/apps/autocal/tools/panel_snapshot.py +++ b/apps/autocal/tools/panel_snapshot.py @@ -15,6 +15,12 @@ import os import sys from pathlib import Path +# The wait loop lives with the checker; this tool used to carry its own copy. +if str(Path(__file__).resolve().parent) not in sys.path: + sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from panel_check import _wait_for # noqa: E402 + APP_ROOT = Path(__file__).resolve().parents[1] for path in (APP_ROOT / "src", APP_ROOT.parents[1] / "shared" / "src"): if str(path) not in sys.path: @@ -151,17 +157,5 @@ def _render_every_tab(window, app, args, width: int, height: int) -> int: return 1 if failures else 0 -def _wait_for(app, predicate, timeout: float) -> bool: - import time - - deadline = time.monotonic() + timeout - while time.monotonic() < deadline: - app.processEvents() - if predicate(): - return True - time.sleep(0.02) - return False - - if __name__ == "__main__": raise SystemExit(main()) diff --git a/apps/studio/docs/shell-lifecycle.md b/apps/studio/docs/shell-lifecycle.md index 85388ff..cbd7b3f 100644 --- a/apps/studio/docs/shell-lifecycle.md +++ b/apps/studio/docs/shell-lifecycle.md @@ -69,7 +69,7 @@ 重算是否在跑)与 2D 渲染属于窗口级的 `ReplayController`(见 `docs/architecture/studio-architecture.md` 建议 1)。 - **选视频即选录制**:状态轨的「选择录制视频…」(`toolbar.btn_replay_video`)与 - 「文件 → 打开视频…」都走 `_load_replay_video(path)`——视频所在目录就是录制目录, + 「文件 → 打开视频…」都走 `ReplayController.load_video(path)`——视频所在目录就是录制目录, 于是曲线自动配对同目录的 `forces.csv`。视频与 CSV 仍然同时记录(录制格式不变)。 - 曲线:`ForcePlotsPanel.set_recording_source(reader)` 用 `RecordingCurveSource` 直接读 录制目录(区间按需读盘 + 缓存包络),所以全程总览、拖动、缩放、框选都在同一套曲线上; @@ -92,7 +92,7 @@ - **数值卡读游标处的那一帧**(`ForcePlotsPanel._readout_at_cursor`)。浏览历史或回放时, 三个大数字是播放头所指的样本;跟随实时时"最新样本就是游标",不额外读盘。旧写法在回放 时显示实时采集的最后一个样本,于是一张 7.5e5 原始计数的曲线旁边写着实时的 `0.0`。 -- **2D 面板归当前视图的来源所有**(`_replay_owns_image()`):回放视图打开录制后,采集线程 +- **2D 面板归当前视图的来源所有**(`ReplayController.owns_image()`):回放视图打开录制后,采集线程 仍然在跑,但 `_on_run_tick` / `_on_preview_tick` 不再把实时帧画进 2D 面板, `_render_run_image` 直接返回,否则回放画面会被摄像头帧一帧一帧盖掉。切回实时视图才把 面板交还给实时帧。 diff --git a/apps/studio/src/orisys_studio/analysis_worker.py b/apps/studio/src/orisys_studio/analysis_worker.py index c9caf18..5a04b17 100644 --- a/apps/studio/src/orisys_studio/analysis_worker.py +++ b/apps/studio/src/orisys_studio/analysis_worker.py @@ -65,7 +65,7 @@ class AnalysisReport: ) -def report_from(outcome: AnalysisOutcome | None, *, cancelled: bool) -> AnalysisReport: +def _report_from(outcome: AnalysisOutcome | None, *, cancelled: bool) -> AnalysisReport: """Turn a service outcome into the surface's report.""" if outcome is None: return AnalysisReport( @@ -178,7 +178,7 @@ class AnalysisWorker(QObject): with self._lock: self._service = None cancelled = self._cancel.is_set() - report = report_from(outcome, cancelled=cancelled) + report = _report_from(outcome, cancelled=cancelled) if outcome is None and not cancelled: report = AnalysisReport( outcome=None, diff --git a/apps/studio/tests/test_replay_analysis.py b/apps/studio/tests/test_replay_analysis.py index 41087a4..87e7c28 100644 --- a/apps/studio/tests/test_replay_analysis.py +++ b/apps/studio/tests/test_replay_analysis.py @@ -65,7 +65,7 @@ from orisys_app_shared.scene3d.visual_policy import ( policy_for_settings, select_flow_samples, ) -from orisys_studio.analysis_worker import AnalysisWorker, report_from +from orisys_studio.analysis_worker import AnalysisWorker, _report_from from orisys_studio.replay import MODE_ANALYSIS, MODE_RECORDING from orisys_studio.replay_analysis import ( AnalysisSession, @@ -1342,7 +1342,7 @@ def test_the_worker_reports_progress_and_a_cancelled_outcome(): report = reports[-1][1] assert report.cancelled is True and report.complete is False assert report.frames_written == 2 - assert report_from(None, cancelled=True).cancelled is True + assert _report_from(None, cancelled=True).cancelled is True # ------------------------------------------------------------------ the depth view diff --git a/apps/studio/tests/test_scene3d_view.py b/apps/studio/tests/test_scene3d_view.py index 7133984..eb47bcf 100644 --- a/apps/studio/tests/test_scene3d_view.py +++ b/apps/studio/tests/test_scene3d_view.py @@ -1109,7 +1109,7 @@ def test_the_replay_view_shows_the_pane_and_says_where_the_field_comes_from( """ window = _show(make_window(sensor_type="finger"), (1440, 880)) try: - assert window._load_recording(Path(replay_recording)) + assert window.replay_ctl.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" diff --git a/apps/studio/tools/ui_check.py b/apps/studio/tools/ui_check.py index d60e333..18fdffb 100644 --- a/apps/studio/tools/ui_check.py +++ b/apps/studio/tools/ui_check.py @@ -2084,9 +2084,9 @@ def main() -> int: app.processEvents() pane_window.brand_bar.btn_playback.click() app.processEvents() - check(pane_window._load_recording(recording), "the replay could not open the recording") + check(pane_window.replay_ctl.load_recording(recording), "the replay could not open the recording") app.processEvents() - check(pane_window._replay_owns_image(), "the replay does not own the 2D pane") + check(pane_window.replay_ctl.owns_image(), "the replay does not own the 2D pane") # The recording has no calibrated axis, so newton would be an empty chart. check( pane_window.force_plots.unit() == "raw", @@ -2139,7 +2139,7 @@ def main() -> int: pane_window.brand_bar.btn_live.click() app.processEvents() check( - not pane_window._replay_owns_image(), + not pane_window.replay_ctl.owns_image(), "the live view still hands the 2D pane to the replay", ) pane_window._on_run_tick() @@ -3187,7 +3187,7 @@ def main() -> int: "the replay view still offers the camera picker", ) check( - replay_window._load_recording(recording), + replay_window.replay_ctl.load_recording(recording), "the replay view could not open the recording", ) app.processEvents() @@ -3238,7 +3238,7 @@ def main() -> int: _prompt_module.VideoAnalysisDialog.exec = _answer_prompt try: check( - other_window._load_replay_video(bare_video) is True, + other_window.replay_ctl.load_video(bare_video) is True, "a playable video with no recording beside it was refused", ) app.processEvents() @@ -3652,7 +3652,7 @@ def main() -> int: # Leaving the replay view returns the charts to the live session and hides # the bar: the replay stays available, it just stops being on screen. check( - replay_window._load_recording(recording), + replay_window.replay_ctl.load_recording(recording), "the replay view could not be re-opened", ) app.processEvents() diff --git a/apps/studio/tools/ui_snapshot.py b/apps/studio/tools/ui_snapshot.py index f785a14..f27a284 100644 --- a/apps/studio/tools/ui_snapshot.py +++ b/apps/studio/tools/ui_snapshot.py @@ -91,7 +91,7 @@ def _run_scene(window, app, scene: str): for _ in range(4): app.processEvents() recording = _replay_data(window, app) - window._load_recording(recording) + window.replay_ctl.load_recording(recording) # Park the playhead where the bar and the charts show a real position. window.replay.set_position(window.replay.duration_s() * 0.4) window.arrow_panel.set_view_mode("recorded") diff --git a/docs/architecture/studio-3d-stages.md b/docs/architecture/studio-3d-stages.md index 70b0a8f..93dcd72 100644 --- a/docs/architecture/studio-3d-stages.md +++ b/docs/architecture/studio-3d-stages.md @@ -518,7 +518,7 @@ shared 全部 **262 项**。`ui_check.py` **566 项检查 0 失败**(新增 "3 `act_recompute` 同一个开关),2D 面板的 note 直接指路。 - **实时界面禁止回放入口**:新增 `_sync_replay_actions()`——`act_open_video`/`act_playback`/ `act_playback_latest` 只在历史回放视图可用,否则禁用 + tooltip 说明原因; - `_load_replay_video()`/`_open_latest_recording()` 在非 replay 预设**拒绝并提示**,不再把采集源偷偷 + `load_video()`/`open_latest_recording()` 在非 replay 预设**拒绝并提示**,不再把采集源偷偷 切成文件。实时视图用视频**当采集源**("回到摄像机")是另一项既有能力,保留不变。 - locale:`panels.replay_raw_note`/`frame_missing_note`/`field_flow`/`field_depth`/`recompute_pipeline(_tip)`、 `toolbar.recompute(_tip)`/`replay_view_only`(中英各一套)。 @@ -529,7 +529,7 @@ shared 全部 **262 项**。`ui_check.py` **566 项检查 0 失败**(新增 "3 |---|---| | 后端单一 | `shared/tests/test_image_backend.py` 15 项:三类来源的 mode 计划、`missing`/note、模式回退、**与旧渲染逐像素一致**(arrows 对 `draw_arrow_display`、depth 对 `depth_map_to_bgr` 带主题色带)、原始帧不缩放不替图、缺 depth → None、缺 flow → 底图 | | 路径一致 | `test_ui_smoke.py`:同一段视频在**回放**路径只出 `recorded` 且有说明行、在**重算/实时**路径出三种派生视图且都经过同一个 `render_image`(spy 断言来源与模式);"全应用只有一处 BGR→QImage"的结构断言 | -| 回放入口收口 | 实时预设下三个文件菜单动作禁用且 tooltip 带原因、rail 无回放/重算按钮、`_load_replay_video()` 返回 False 且 `source_mode()`/播放定时器不变、空格不启动回放;切到历史回放后三者可用、裸视频仍能打开、rail 的重算按钮可点并同步 `act_recompute` | +| 回放入口收口 | 实时预设下三个文件菜单动作禁用且 tooltip 带原因、rail 无回放/重算按钮、`ReplayController.load_video()` 返回 False 且 `source_mode()`/播放定时器不变、空格不启动回放;切到历史回放后三者可用、裸视频仍能打开、rail 的重算按钮可点并同步 `act_recompute` | | 不回归 | 缺深度仍报"本帧没有深度数据"(不回退显示普通形变图);只导入视频仍可播放(既有用例) | --- @@ -666,9 +666,9 @@ shared 全部 **262 项**。`ui_check.py` **566 项检查 0 失败**(新增 "3 `bench_scene.py --package …`、`soak_scene.py --no-window --minutes 10` 报告。板卡不在手边时按"未跑" 记录,**不得**写成通过。 3. **真机待验证一项**(阶段 6 遗留):算法链失败时 SDK 的 `VRAW`(光流)是否也过期。当前取保守解释。 -4. **阶段 10 的过渡转发**:窗口里那约 40 个与旧名同名的成员(`_apply_view_preset`、`_set_display_unit`、 - `_play_timer`、`_recompute_running`、`_load_recording` …)是给 tests/`ui_check` 的桥,不是最终形态。 - 下一次改动落在哪个控制器,就把该控制器的转发与测试调用点改成 `self..*`。 +4. **阶段 10 的过渡转发**:窗口里三十余个与旧名同名的成员(`_apply_view_preset`、`_set_display_unit`、 + `_play_timer`、`_recompute_running` …)是给 tests/`ui_check` 的桥,非最终形态。下一次改动落在哪个 + 控制器,就把它的转发与测试调用点改成 `self..*`。 5. **仍留在窗口的胶水**:预览(`_start_preview`/`_stop_preview`/`_on_preview_tick`)与 2D 渲染 (`_show_image_frame`/`_render_run_image`/`_depth_range`)。它们同时属于面板与相机,暂时没有第二个 变更驱动;硬拆只会把耦合搬家。 diff --git a/tools/scene_assets/_paths.py b/tools/scene_assets/_paths.py new file mode 100644 index 0000000..91b62a4 --- /dev/null +++ b/tools/scene_assets/_paths.py @@ -0,0 +1,20 @@ +"""Put the studio and shared source trees on ``sys.path`` for the scene tools. + +These tools are run as scripts out of this directory, but the tests load them by +file location (``spec_from_file_location`` + ``exec_module``), which leaves this +directory off ``sys.path``. Each tool therefore adds it before importing here. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + + +def bootstrap_paths() -> None: + """Make ``orisys_studio`` and ``orisys_app_shared`` importable from the repo.""" + here = Path(__file__).resolve() + repo = here.parents[2] + for path in (repo / "apps" / "studio" / "src", repo / "shared" / "src"): + if path.is_dir() and str(path) not in sys.path: + sys.path.insert(0, str(path)) diff --git a/tools/scene_assets/bench_scene.py b/tools/scene_assets/bench_scene.py index 8d242c2..3c363bb 100644 --- a/tools/scene_assets/bench_scene.py +++ b/tools/scene_assets/bench_scene.py @@ -37,13 +37,11 @@ import sys import time from pathlib import Path +# Tests load this tool by file location, so add our own directory to the path. +if str(Path(__file__).resolve().parent) not in sys.path: + sys.path.insert(0, str(Path(__file__).resolve().parent)) -def _bootstrap_paths() -> None: - here = Path(__file__).resolve() - repo = here.parents[2] - for path in (repo / "apps" / "studio" / "src", repo / "shared" / "src"): - if path.is_dir() and str(path) not in sys.path: - sys.path.insert(0, str(path)) +from _paths import bootstrap_paths # noqa: E402 def _percentiles(values: list[float]) -> dict[str, float]: @@ -604,7 +602,7 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--json", action="store_true") args = parser.parse_args(argv) - _bootstrap_paths() + bootstrap_paths() width, _, height = args.size.partition("x") if args.cpu_only: report, code = measure_cpu( diff --git a/tools/scene_assets/compile_scene.py b/tools/scene_assets/compile_scene.py index c94a2e0..81af055 100644 --- a/tools/scene_assets/compile_scene.py +++ b/tools/scene_assets/compile_scene.py @@ -43,13 +43,11 @@ import json import sys from pathlib import Path +# Tests load this tool by file location, so add our own directory to the path. +if str(Path(__file__).resolve().parent) not in sys.path: + sys.path.insert(0, str(Path(__file__).resolve().parent)) -def _bootstrap_paths() -> None: - here = Path(__file__).resolve() - repo = here.parents[2] - for path in (repo / "apps" / "studio" / "src", repo / "shared" / "src"): - if path.is_dir() and str(path) not in sys.path: - sys.path.insert(0, str(path)) +from _paths import bootstrap_paths # noqa: E402 def _platform_lock_from_args(args): @@ -189,7 +187,7 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--json", action="store_true") args = parser.parse_args(argv) - _bootstrap_paths() + bootstrap_paths() import numpy as np from orisys_app_shared.scene3d.contracts import ThreeDError from orisys_app_shared.scene3d.legacy import LegacyFingerImporter diff --git a/tools/scene_assets/probe_gl.py b/tools/scene_assets/probe_gl.py index 2ff8b8d..061ef4d 100644 --- a/tools/scene_assets/probe_gl.py +++ b/tools/scene_assets/probe_gl.py @@ -26,20 +26,18 @@ import sys import time from pathlib import Path +# Tests load this tool by file location, so add our own directory to the path. +if str(Path(__file__).resolve().parent) not in sys.path: + sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _paths import bootstrap_paths # noqa: E402 + #: Exit codes: distinct values so a CI script can tell the two failures apart. EXIT_OK = 0 EXIT_SOFTWARE = 3 EXIT_NO_CONTEXT = 4 -def _bootstrap_paths() -> None: - here = Path(__file__).resolve() - repo = here.parents[2] - for path in (repo / "apps" / "studio" / "src", repo / "shared" / "src"): - if path.is_dir() and str(path) not in sys.path: - sys.path.insert(0, str(path)) - - def collect(profile: str, *, keep_open_s: float) -> dict: """Create a real window, let Qt initialise it, and report what it got.""" from orisys_studio.scene3d.gl_backend import ( @@ -189,7 +187,7 @@ def main(argv: list[str] | None = None) -> int: ) args = parser.parse_args(argv) - _bootstrap_paths() + bootstrap_paths() report = collect(args.profile, keep_open_s=0.6 if args.windowed else 0.3) context = report.get("context") if not context or not context.get("valid"): diff --git a/tools/scene_assets/soak_scene.py b/tools/scene_assets/soak_scene.py index 1cd6ba3..0b8070c 100644 --- a/tools/scene_assets/soak_scene.py +++ b/tools/scene_assets/soak_scene.py @@ -37,13 +37,11 @@ import threading import time from pathlib import Path +# Tests load this tool by file location, so add our own directory to the path. +if str(Path(__file__).resolve().parent) not in sys.path: + sys.path.insert(0, str(Path(__file__).resolve().parent)) -def _bootstrap_paths() -> None: - here = Path(__file__).resolve() - repo = here.parents[2] - for path in (repo / "apps" / "studio" / "src", repo / "shared" / "src"): - if path.is_dir() and str(path) not in sys.path: - sys.path.insert(0, str(path)) +from _paths import bootstrap_paths # noqa: E402 def resident_bytes() -> int: @@ -519,7 +517,7 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--json", action="store_true") args = parser.parse_args(argv) - _bootstrap_paths() + bootstrap_paths() report, code = run( minutes=args.minutes, fps=args.fps, diff --git a/tools/scene_assets/validate_scene.py b/tools/scene_assets/validate_scene.py index 2c9bb1b..204b7ea 100644 --- a/tools/scene_assets/validate_scene.py +++ b/tools/scene_assets/validate_scene.py @@ -25,13 +25,11 @@ import json import sys from pathlib import Path +# Tests load this tool by file location, so add our own directory to the path. +if str(Path(__file__).resolve().parent) not in sys.path: + sys.path.insert(0, str(Path(__file__).resolve().parent)) -def _bootstrap_paths() -> None: - here = Path(__file__).resolve() - repo = here.parents[2] - for path in (repo / "apps" / "studio" / "src", repo / "shared" / "src"): - if path.is_dir() and str(path) not in sys.path: - sys.path.insert(0, str(path)) +from _paths import bootstrap_paths # noqa: E402 def _legacy_report(args) -> tuple[int, dict]: @@ -308,7 +306,7 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--json", action="store_true", help="machine-readable output") args = parser.parse_args(argv) - _bootstrap_paths() + bootstrap_paths() if args.legacy_dir is not None: if args.map is None: print("--legacy-dir also needs --map", file=sys.stderr)