把 Studio 主机协议改成单向端口,并让「从视频重算」走分析入口

控制器经 AcquireGate / ImageSink / ShellNotes 与信号接线,采集 tick 用 acquire_presenter 上屏;三维测试证明分析结果已喂 3D 后,重算并入 on_analysis_requested。
验证:apps/studio 下相关 pytest 与 tools/ui_check.py(634 checks)通过。
This commit is contained in:
ykcheungab
2026-10-05 14:00:24 +08:00
parent 25ccffd814
commit 28e955a817
27 changed files with 1764 additions and 745 deletions
+70 -26
View File
@@ -3220,11 +3220,57 @@ def main() -> int:
app.processEvents()
other_window.brand_bar.btn_playback.click()
app.processEvents()
check(
other_window._load_replay_video(bare_video) is True,
"a playable video with no recording beside it was refused",
)
app.processEvents()
# A video that is not a recording is asked *how* to analyse it before anything runs
# (the setup prompt). These checks answer it and read its defaults back; the prompt is
# modal, so a check that opens a bare video without answering would wait for a person.
from orisys_studio.ui import video_analysis_dialog as _prompt_module
from PySide6.QtWidgets import QDialog as _QDialog
_prompts: list = []
_original_exec = _prompt_module.VideoAnalysisDialog.exec
def _answer_prompt(dialog_self):
_prompts.append(dialog_self)
dialog_self.select_profile("shipped/box")
return _QDialog.DialogCode.Rejected
_prompt_module.VideoAnalysisDialog.exec = _answer_prompt
try:
check(
other_window._load_replay_video(bare_video) is True,
"a playable video with no recording beside it was refused",
)
app.processEvents()
check(bool(_prompts), "opening a bare video did not ask how to analyse it")
if _prompts:
_prompt = _prompts[-1]
check(
_prompt.profile_combo.count() >= 1,
"the setup prompt offers no processing profile",
)
check(
"flow" in _prompt.capability_label.text(),
f"the profile row does not say what it can do: "
f"{_prompt.capability_label.text()!r}",
)
check(
_prompt.calibration_combo.currentData() == "default",
f"the calibration did not default to the profile's own: "
f"{_prompt.calibration_combo.currentData()!r}",
)
check(
_prompt.reference_spin.value() == 1,
f"the reference frame did not default to the first: "
f"{_prompt.reference_spin.value()}",
)
check(
_prompt.select_profile("shipped/box") and _prompt.choice() is not None,
"the shipped box profile cannot be set up for a video",
)
finally:
_prompt_module.VideoAnalysisDialog.exec = _original_exec
check(
other_window.replay.has_video() and other_window.replay.is_video_only(),
"the video is not playable as a video",
@@ -3268,7 +3314,7 @@ def main() -> int:
other_window.act_recompute.isEnabled(),
"a chosen video cannot be recomputed either: the operator has no way to view it",
)
other_window._stop_replay_activity()
other_window.replay_ctl.stop_activity()
app.processEvents()
other_window.close()
app.processEvents()
@@ -3569,41 +3615,39 @@ def main() -> int:
f"{replay_window.arrow_panel.view_modes()}",
)
# The recompute mode: off (default) the charts read the recorded CSV; on, the
# recorded video is run through the pipeline instead. The run itself is not
# started here (that needs the SDK), only the wiring around it.
# 「从视频重算」 shares on_analysis_requested with opening a recording — no
# acquisition chain. Charts keep the recorded CSV; analysis feeds 2D/3D.
started: list = []
asked: list = []
replay_window._start_run = lambda: started.append(True)
original_analysis = replay_window.replay_ctl.on_analysis_requested
def _spy_analysis() -> None:
asked.append(True)
return original_analysis()
replay_window.replay_ctl.on_analysis_requested = _spy_analysis
replay_window.act_recompute.setChecked(True)
for _ in range(3):
app.processEvents()
check(asked == [True], "the recompute switch did not ask for analysis")
check(started == [], "the recompute switch opened an acquisition chain")
check(
started == [True],
"the recompute mode did not hand the video to the pipeline",
replay_plots.is_recording_source(),
"analysis must not replace the recorded CSV on the charts",
)
check(
not replay_plots.is_recording_source(),
"the charts still read the CSV while recomputing",
)
check(
replay_window.toolbar.source_mode() == "video"
and replay_window.toolbar.video_path().endswith(".mp4"),
f"the recompute mode did not select the video: "
f"{replay_window.toolbar.source_mode()!r} / "
f"{replay_window.toolbar.video_path()!r}",
)
replay_window._recompute_running = True
replay_window.act_recompute.setChecked(False)
for _ in range(3):
app.processEvents()
check(
replay_plots.is_recording_source(),
"turning the recompute off did not restore the recorded curves",
"turning the recompute off did not keep the recorded curves",
)
check(
replay_window.toolbar.source_mode() == "camera" or not replay_window._running,
"the recompute pass was left running",
not replay_window._running,
"an acquisition session was left running after recompute",
)
replay_window.replay_ctl.on_analysis_requested = original_analysis
# 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.