feat(studio-2d): 回放可一键重算;实时界面不再有任何回放入口
- What:
1. **回放里一键重算**:`ui/toolbar.py` 的 replay 组新增 `btn_recompute`(可选中的"由视频重算…",
信号 `recompute_clicked`、`set_recompute_enabled/checked`、tooltip 说明"2D 面板会显示处理后的图像,
录像本身只存了原始相机图像");`app_window` 把它接到 `act_recompute`(同一个开关,所以菜单、rail 与
`_on_recompute_toggled` 不会各走一套),并在 `_refresh_replay_state`/`_sync_replay_actions` 里同步
可用与选中状态。
2. **实时界面禁止回放入口**:新增 `_sync_replay_actions()`——`act_open_video`/`act_playback`/
`act_playback_latest` 只在 `_view_preset == "replay"` 时可用,否则禁用并把原因写进 tooltip
(`toolbar.replay_view_only` = "请在「历史回放」视图里打开录像");`_load_replay_video()` 与
`_open_latest_recording()` 在非 replay 预设下**直接拒绝并提示**,不再改动 `source_mode`/`video_path`
(原来打开一段没有 forces.csv 的视频会走到 `_load_replay_video` 的裸视频分支,把采集源切成文件而 2D
面板什么都没变)。rail 的回放按钮本来就在 replay 分组(`toolbar.py:476/598`),不改动;空格键原本就按
`run_role == "playback"` 收口,加了断言。
3. locale:新增 `toolbar.recompute`/`toolbar.recompute_tip`/`toolbar.replay_view_only`(中英各一套)。
- Why: 用户报的第二个 bug——「实时检测界面还是可以回放视频,建议回放会自动切到历史回放上面」,并在两个
选项里选了"实时界面禁止回放入口(反向做法)"。核对确认:`_on_browse_video`/`_load_replay_video` 在实时
预设下可用且只设 `source_mode="video"` + `video_path`,不切视图预设;随后 `_render_replay_frame()` 因
`_view_preset != "replay"` 直接返回、`_can_recompute()` 同因返回 False,于是界面停在"实时"却已经把采集源
换成了视频——按运行键就"在实时界面跑视频"。配套地,回放里"看不到处理后的图像"这一个用户期待,由
「由视频重算」给出:它现在就在回放视图的 rail 上,点一下即可,2D 面板与实时共用同一个后端(见上一提交)。
注意区分:实时视图用视频**当采集源**(`source_mode == "video"` 显示"回到摄像机")是另一项既有能力,保留
不变,只是不再由回放路径隐式设置。
- Verification:
· `test_ui_smoke.py` +1(`test_the_replay_entries_exist_only_in_the_replay_view`):实时预设下三个文件
菜单动作禁用且 tooltip 带原因、rail 无回放/重算按钮、`_load_replay_video()` 返回 False 且
`replay.has_video()`/`source_mode()`/播放定时器都不变、空格不启动回放;切到历史回放后三者可用、裸视频
仍能打开(`plan_modes() == ("recorded",)`)、rail 的重算按钮可点并同步 `act_recompute`。
· 既有用例继续通过:`test_a_bare_video_plays_even_though_it_is_not_a_recording`(先切到历史回放再打开,
播放与逐帧解码不受影响)、`test_depth_view_...`、三维与记录相关用例。
· 全量:apps/studio 237 项 0 失败 14 跳过;ui_check 587 项 0 失败;locale_check 0 缺失(zh/en 键集合
仍一致)。
- Not in this change: 没有改成"打开视频时自动切视图"(按用户选择,改为禁止 + 说明);`source_mode == "video"`
在实时视图的既有行为未动("由视频当相机"仍可用,只是入口不再是回放路径);阶段 8(界面与有效控制)与
阶段 9(限速/统计/保护包)未开始。
This commit is contained in:
@@ -790,6 +790,7 @@ class StudioMainWindow(QMainWindow):
|
||||
self.toolbar.sensor_clicked.connect(self._open_sensor_dialog)
|
||||
self.toolbar.use_camera_clicked.connect(self._on_use_camera)
|
||||
self.toolbar.replay_video_clicked.connect(self._on_open_recording)
|
||||
self.toolbar.recompute_clicked.connect(self._on_recompute_button)
|
||||
self.toolbar.run_toggled.connect(self._on_run_toggled)
|
||||
self.toolbar.record_toggled.connect(self._on_record_toggled)
|
||||
self.toolbar.reset_clicked.connect(self._on_reset)
|
||||
@@ -842,6 +843,29 @@ class StudioMainWindow(QMainWindow):
|
||||
return
|
||||
self._load_replay_video(Path(chosen))
|
||||
|
||||
def _on_recompute_button(self) -> None:
|
||||
"""The replay view's own entry for the processed picture (see ``recompute``)."""
|
||||
self.act_recompute.setChecked(not self.act_recompute.isChecked())
|
||||
|
||||
def _sync_replay_actions(self) -> None:
|
||||
"""Replay entries exist in the replay view, and nowhere else.
|
||||
|
||||
Opening a recording from the *live* view used to leave the window in a state
|
||||
nothing described: the source became a video file, the 2D panel did not change
|
||||
(the replay renderer only draws in the replay view) and the Run button acquired
|
||||
from that file. The entries are therefore disabled outside the replay view, with
|
||||
the reason in the tooltip, instead of quietly doing half of it.
|
||||
"""
|
||||
allowed = self._view_preset == "replay"
|
||||
reason = "" if allowed else t("toolbar.replay_view_only")
|
||||
for action in (self.act_open_video, self.act_playback, self.act_playback_latest):
|
||||
action.setEnabled(allowed)
|
||||
action.setToolTip(reason)
|
||||
# The rail's entries are in the replay group already; the recompute button also
|
||||
# has to follow whether there is anything to recompute *from*.
|
||||
self.toolbar.set_recompute_enabled(allowed and self._can_recompute())
|
||||
self.toolbar.set_recompute_checked(self.act_recompute.isChecked())
|
||||
|
||||
def _load_replay_video(self, video: Path) -> bool:
|
||||
"""Open what the chosen video *is*: a recording, or just a video.
|
||||
|
||||
@@ -855,6 +879,12 @@ class StudioMainWindow(QMainWindow):
|
||||
that is the only way to get *numbers* out of a video that has no recording.
|
||||
"""
|
||||
video = Path(video)
|
||||
if self._view_preset != "replay":
|
||||
# Defensive: the entries are disabled outside the replay view, so this only
|
||||
# happens programmatically — and refusing is better than leaving the window
|
||||
# with a video source and no replay renderer.
|
||||
self._notify(t("toolbar.replay_view_only"))
|
||||
return False
|
||||
folder = video.parent if video.is_file() else video
|
||||
if not (folder / "forces.csv").is_file():
|
||||
if video.is_file() and self.replay.open_video(video):
|
||||
@@ -905,6 +935,9 @@ class StudioMainWindow(QMainWindow):
|
||||
"""Switch to the replay view on the most recent recording."""
|
||||
from orisys_app_shared.record import list_recordings
|
||||
|
||||
if self._view_preset != "replay":
|
||||
self._notify(t("toolbar.replay_view_only"))
|
||||
return
|
||||
found = list_recordings(recordings_dir())
|
||||
if not found:
|
||||
self._notify(t("playback.none_found"))
|
||||
@@ -940,6 +973,7 @@ class StudioMainWindow(QMainWindow):
|
||||
# chosen video file is enough to recompute *from*, even when it is not a
|
||||
# recording: that is the way to look at a video that has no forces.csv.
|
||||
self.act_recompute.setEnabled(self._can_recompute())
|
||||
self._sync_replay_actions()
|
||||
self._update_run_enabled()
|
||||
self._refresh_status_rail()
|
||||
|
||||
@@ -1477,6 +1511,7 @@ class StudioMainWindow(QMainWindow):
|
||||
self._on_recompute_toggled(False)
|
||||
self.brand_bar.set_active_view(preset if preset != "custom" else None)
|
||||
self._sync_menu_checks()
|
||||
self._sync_replay_actions()
|
||||
self._update_run_enabled()
|
||||
self._refresh_status_rail()
|
||||
if persist:
|
||||
|
||||
@@ -79,6 +79,8 @@ class ToolbarWidget(QWidget):
|
||||
use_camera_clicked = pyqtSignal()
|
||||
#: Replay view: pick the recorded video to play.
|
||||
replay_video_clicked = pyqtSignal()
|
||||
#: Replay view: re-run the pipeline over that video (the only processed picture).
|
||||
recompute_clicked = pyqtSignal()
|
||||
run_toggled = pyqtSignal(bool)
|
||||
reset_clicked = pyqtSignal()
|
||||
units_changed = pyqtSignal(str)
|
||||
@@ -210,6 +212,17 @@ class ToolbarWidget(QWidget):
|
||||
self.btn_replay_video.setMinimumHeight(BUTTON_HEIGHT)
|
||||
self.btn_replay_video.clicked.connect(self.replay_video_clicked.emit)
|
||||
replay_layout.addWidget(self.btn_replay_video)
|
||||
# The processed picture of a recording is what the recompute pass produces, and
|
||||
# that is the only way to see it: the recording stores the raw camera image.
|
||||
# Putting the entry here (rather than only in the View menu) is what makes the
|
||||
# replay view able to answer "why is this picture unprocessed" in one click.
|
||||
self.btn_recompute = QPushButton()
|
||||
self.btn_recompute.setProperty("compact", "true")
|
||||
self.btn_recompute.setMinimumHeight(BUTTON_HEIGHT)
|
||||
self.btn_recompute.setCheckable(True)
|
||||
self.btn_recompute.setEnabled(False)
|
||||
self.btn_recompute.clicked.connect(self.recompute_clicked.emit)
|
||||
replay_layout.addWidget(self.btn_recompute)
|
||||
slot.addWidget(self.camera_group)
|
||||
slot.addWidget(self.video_group)
|
||||
slot.addWidget(self.replay_group)
|
||||
@@ -475,6 +488,15 @@ class ToolbarWidget(QWidget):
|
||||
# same rule as the camera picker: not while a session is running.
|
||||
self.btn_replay_video.setEnabled(bool(enabled) and not live)
|
||||
|
||||
def set_recompute_enabled(self, enabled: bool) -> None:
|
||||
"""The recompute entry lives in the replay view; its checked state follows."""
|
||||
self.btn_recompute.setEnabled(bool(enabled))
|
||||
|
||||
def set_recompute_checked(self, checked: bool) -> None:
|
||||
self.btn_recompute.blockSignals(True)
|
||||
self.btn_recompute.setChecked(bool(checked))
|
||||
self.btn_recompute.blockSignals(False)
|
||||
|
||||
def set_source_controls_enabled(self, enabled: bool) -> None:
|
||||
self._source_controls_enabled = bool(enabled)
|
||||
self.set_camera_controls_enabled(self._source_controls_enabled)
|
||||
@@ -607,6 +629,7 @@ class ToolbarWidget(QWidget):
|
||||
self.source_summary.setToolTip(path or self.source_summary.text())
|
||||
self.btn_use_camera.setToolTip(t("toolbar.use_camera_tip"))
|
||||
self.btn_replay_video.setToolTip(t("toolbar.choose_video_tip"))
|
||||
self.btn_recompute.setToolTip(t("toolbar.recompute_tip"))
|
||||
|
||||
def _on_preview_toggled(self, checked: bool) -> None:
|
||||
self._refresh_preview_label()
|
||||
@@ -719,6 +742,7 @@ class ToolbarWidget(QWidget):
|
||||
self.btn_search.setText(t("toolbar.search_cameras"))
|
||||
self.btn_use_camera.setText(t("toolbar.use_camera"))
|
||||
self.btn_replay_video.setText(t("toolbar.choose_video"))
|
||||
self.btn_recompute.setText(t("toolbar.recompute"))
|
||||
self.refresh_labels()
|
||||
self._refresh_reset_label()
|
||||
self.units_label.setText(t("toolbar.units"))
|
||||
|
||||
Reference in New Issue
Block a user