Make the cached analysis views reachable over a recording
Replay moved the cursor onto the result's timeline, so the recording's own picture was not offered at all and ``use_recording()`` had no caller. Adopting a result now keeps the cursor on the recording, opens the panel on a derived view, and serves scrubs, playback and display changes from the cache (Part B §9). Verified: apps/studio 455 items / 0 failed / 18 skipped, shared 427 / 0 / 1, autocal 358, ui_check 628 checks / 0 failures, lint gate clean.
This commit is contained in:
@@ -29,6 +29,7 @@ from typing import Any, Protocol
|
||||
|
||||
import numpy as np
|
||||
from orisys_app_shared.acquisition.image_backend import (
|
||||
ARROW_PURE,
|
||||
DERIVED_MODES,
|
||||
SOURCE_ANALYSIS,
|
||||
SOURCE_REPLAY,
|
||||
@@ -45,6 +46,7 @@ from orisys_studio.replay_analysis import (
|
||||
AnalysisSession,
|
||||
ResultLibrary,
|
||||
ResultPolicyError,
|
||||
policy_divergence,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -620,7 +622,13 @@ class ReplayController(QObject):
|
||||
|
||||
# ---------------------------------------------------------------- analysis
|
||||
def analysis_session(self) -> AnalysisSession | None:
|
||||
"""The stored result the timeline is reading, if any."""
|
||||
"""Whatever result is open: the cursor's own, else the library's.
|
||||
|
||||
Not the question a view asks — that is :meth:`available_result`, which answers "is
|
||||
there a measurement for *this recording*" and is what the app's timeline (always
|
||||
the recording's) is served from. This one is the raw pair, kept for a caller that
|
||||
wants the session itself.
|
||||
"""
|
||||
session = self._engine.analysis()
|
||||
if session is not None:
|
||||
return session
|
||||
@@ -658,19 +666,25 @@ class ReplayController(QObject):
|
||||
``test_replay_analysis`` that the result directory set is unchanged is a test of
|
||||
this method, not of the panel.
|
||||
|
||||
It is asked of ``available_result``, not of ``analysis_active``: a result is a
|
||||
property of the recording, and the cursor is normally on the recording's own
|
||||
timeline while the arrows and the depth map are drawn from it. Gating on the
|
||||
timeline instead would make a threshold change stop mattering in exactly the
|
||||
state this view is served in.
|
||||
|
||||
A policy that moves the sample *grid* cannot be served by the stored candidate
|
||||
locations; :meth:`AnalysisSession.serve_policy` says so, the message names the
|
||||
key, and the frame is still redrawn from the result's own grid, because
|
||||
locations; :meth:`AnalysisSession.serve_policy` refuses it, the message names the
|
||||
keys, and the frame is still redrawn from the result's own grid, because
|
||||
re-gridding it would draw samples nothing measured.
|
||||
"""
|
||||
session = self.analysis_session()
|
||||
if session is None or not self.analysis_active():
|
||||
session = self.available_result()
|
||||
if session is None:
|
||||
return
|
||||
from orisys_app_shared.scene3d.visual_policy import policy_for_settings
|
||||
|
||||
policy = policy_for_settings(settings)
|
||||
moved = not session.serve_policy(policy)
|
||||
if moved:
|
||||
if not session.serve_policy(policy):
|
||||
moved = policy_divergence(session.stored_policy(), policy)
|
||||
self._host._notify(t("playback.analysis_grid_mismatch", keys="·".join(moved)))
|
||||
self._engine.redraw()
|
||||
|
||||
@@ -765,15 +779,54 @@ class ReplayController(QObject):
|
||||
return True
|
||||
|
||||
def _adopt_session(self, session: AnalysisSession) -> None:
|
||||
"""One timeline: the cursor, the charts and both figures read this result."""
|
||||
"""One timeline, one cache behind it: the cursor, the charts and both figures.
|
||||
|
||||
**The engine stays on the recording.** The stored result is adopted *behind* the
|
||||
cursor — :meth:`available_result` answers "is there a measurement for this
|
||||
recording?" — and ``ReplayEngine.use_recording()`` is what keeps the cursor's own
|
||||
timeline the recording's: its signal time, its own frame ids, and its whole span,
|
||||
so a position the pass did not cover is still a position the operator can be at.
|
||||
Moving the cursor onto the result instead (``use_analysis``) is what made the
|
||||
recording's own picture unreachable: with the cursor on a result, the recorded
|
||||
view has nothing to decode.
|
||||
"""
|
||||
self.results.open_session(session)
|
||||
self._engine.use_analysis(session)
|
||||
self._engine.use_recording()
|
||||
self._charts.set_recording_source(self._engine.reader())
|
||||
self._image_panel.set_source_kind("analysis")
|
||||
# The timeline is the recording's, so the panel is told "replay" — the same word
|
||||
# ``apply_source`` uses for an open recording. It used to be told "analysis",
|
||||
# which is not a 2D source kind at all: ``ArrowPanel`` knows live/recompute/replay
|
||||
# and coerced it, so a stored result was worded as the live pipeline.
|
||||
self._image_panel.set_source_kind("replay")
|
||||
self._mesh_panel.set_source_kind("analysis")
|
||||
if self._actions is not None:
|
||||
self._actions.recompute.setEnabled(True)
|
||||
self.render_frame()
|
||||
# A result just became readable, so it is what the operator is shown — unless a
|
||||
# derived view was already on screen (a depth view chosen while the pass ran).
|
||||
self.serve_stored_view()
|
||||
|
||||
def serve_stored_view(self) -> bool:
|
||||
"""Put the cached view on screen when a result becomes readable.
|
||||
|
||||
The trigger for "draw the cache without being asked": a stored result is adopted
|
||||
— on opening a recording that has one, or when a pass commits — and the panel is
|
||||
showing the recording's own picture, so the operator would otherwise be left
|
||||
looking at the camera while the measurement sits on disk. The recording's own
|
||||
picture stays in the same selector, one step away, and the recompute switch is
|
||||
not involved.
|
||||
|
||||
Answers whether the selection moved. A derived selection is left alone: the mode
|
||||
the operator chose is not this method's to override.
|
||||
"""
|
||||
current = str(getattr(self._image_panel, "view_mode", lambda: "")() or "")
|
||||
if current in DERIVED_MODES:
|
||||
return False
|
||||
# The plan for the frame just drawn offers the derived modes (the frame carries
|
||||
# ``result_available``), so this selection is one the panel can honour.
|
||||
self._image_panel.set_view_mode(ARROW_PURE)
|
||||
self.render_frame()
|
||||
return True
|
||||
|
||||
def _display_policy(self) -> Any:
|
||||
from orisys_app_shared.scene3d.visual_policy import policy_for_settings
|
||||
@@ -907,6 +960,10 @@ class ReplayController(QObject):
|
||||
map can be drawn at all. The session the library holds is checked against the
|
||||
recording that is open, so a previous recording's result cannot answer for this
|
||||
one.
|
||||
|
||||
This is the normal state of a replay, not a fallback: the cursor stays on the
|
||||
recording's own timeline (``_adopt_session``) and the result is the cache behind
|
||||
it, so the answers and the view modes come from here.
|
||||
"""
|
||||
session = self._engine.analysis()
|
||||
if session is not None:
|
||||
@@ -950,12 +1007,13 @@ class ReplayController(QObject):
|
||||
def render_frame(self) -> None:
|
||||
"""Draw what the 2D panel should show for the current replay cursor.
|
||||
|
||||
One method for both timeline modes, because it is one cursor: in analysis mode
|
||||
the picture is the stored frame's own flow field and depth map (the pass's
|
||||
candidate locations, on the domain grid), and in recording mode it is the
|
||||
decoded video frame. The window's single 2D sink draws both through the *same*
|
||||
backend, so the arrows, the idle dots, the colour range and the domain boundary
|
||||
behave identically in the two modes.
|
||||
One method for both timeline modes, because it is one cursor: with the cursor on
|
||||
a stored result (``analysis_active``) the picture is the stored frame's own flow
|
||||
field and depth map, and with the cursor on the recording's own timeline it is the
|
||||
decoded video frame — unless the *view* the operator asked for is a measurement,
|
||||
in which case it is read from the result for the same cursor. The window's single
|
||||
2D sink draws all of them through the *same* backend, so the arrows, the idle
|
||||
dots, the colour range and the domain boundary behave identically.
|
||||
|
||||
**A mode that needs derived data is served from the result, never faked.** The
|
||||
arrows and the depth map are the pass's measurements; with a result in hand they
|
||||
|
||||
Reference in New Issue
Block a user